From f449ece1968230d74e7d153a24b023a7c0e16072 Mon Sep 17 00:00:00 2001 From: ShawnSpitzel <158654739+ShawnSpitzel@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:08:40 -0500 Subject: [PATCH 1/3] v1.0.1, macos release --- .github/workflows/release.yml | 20 +++---- codeshuffler/gui/icons/codeshuffler-icon.icns | Bin 0 -> 2688 bytes codeshuffler/version.py | 2 +- packaging/mac.spec | 55 ++++++++++++++++++ scripts/build_mac.sh | 2 +- 5 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 codeshuffler/gui/icons/codeshuffler-icon.icns create mode 100644 packaging/mac.spec diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a04e423..716eca1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,11 +46,11 @@ jobs: shell: bash if: runner.os == 'Windows' - # - name: Build (PyInstaller) - # run: | - # pyinstaller packaging/macos.spec - # shell: bash - # if: runner.os == 'macOS' + - name: Build (PyInstaller) + run: | + pyinstaller packaging/mac.spec + shell: bash + if: runner.os == 'macOS' - name: Package artifact (Windows) if: runner.os == 'Windows' @@ -58,11 +58,11 @@ jobs: run: | Compress-Archive -Path dist\CodeShuffler\* -DestinationPath CodeShuffler-windows.zip - # - name: Package artifact (macOS) - # if: runner.os == 'macOS' - # shell: bash - # run: | - # ditto -c -k --sequesterRsrc --keepParent "dist/CodeShuffler.app" "CodeShuffler-macos.zip" + - name: Package artifact (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + ditto -c -k --sequesterRsrc --keepParent "dist/CodeShuffler.app" "CodeShuffler-macos.zip" - name: Upload artifacts to workflow uses: actions/upload-artifact@v4 diff --git a/codeshuffler/gui/icons/codeshuffler-icon.icns b/codeshuffler/gui/icons/codeshuffler-icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..6ecebb2e8ad1ad521c2779ec1341eadbf844781c GIT binary patch literal 2688 zcmbVOS!`5Q7(OCU=B_i8V(BtnW}nX7xzk2yOBY&bX(^rQbnkT50x=k35JhDPi$owH z5Je!hqKH0_kQjZzjbIb7B1|_+Kw{AH1V|z(NTuz}J^trTr<;XDntVCS+;hI~Ur#Ab zB~qlXn8li#Qkbk}0bpg!%>o;nlQqm)bF&rp$Xp)DoXu*ovgU^8p5@I*p)N&F^S7!O zBVO<{^Y-8XpB=mvgA(QhWF4E&INAf3IH9#{e(nY|chS=SEdlf=EA7D_l{sth(}K4L z&eGb|G_9+C8h#ttb(?VW*=={g4_D#784$dx9N#DN;5B7-+aK~dIKvog7t%)8jQsb% zPDE0l7KOVrDXB-3DD`9{MnY*UzMqR&bG-<6{RVg4<1Fq9TJKC_^iC6_cT7MxX)f=} zzo&9G?|R^m5Y05UfrwZX-6Yy(1 zxLZFAz*0zeF>*ecXHE4YoG1J@7-M}QCkAgOqYJ0z`|$Itns9z~qYPVl3!b0T zCg%x!-_4jCSAw1!prw;G)W*+b|7aIacv0kXdZyH^`v$m2aJOyyA<9quPxvWA zePK$jckuAu0=#oeiDW8qOM7<~Nk{hQiYrA?cbOYu2DiL*nl*aN+k-kAjYi=Yl` zIcw`;UT78gtOi~wdXXaDhDN?)k<_h4kx+UDaGwS4Fsu)Qp5{nMvm@M<6bJq%@t5sa z7#a%I`JTb!2lMdu*GnXQajW#k=2B_T_G0PHZOf$HTbD_@02Pfj7-G~1_9jA3o`M+s zt;}hA0D1hE((eCMDY(@tyBD}MfQ)ovH^e*)K0g9};P9O)H1vJ)tcP79BK%-W)c>Zs z0`UJ}+EO2G@Kxi?LXR8=XR8y(Py=(ZyqWNW{jPZ4=4n%kz7GK%s%-z4s+{16%I-fC zXAGp^D{3`xen#xUt zOxQopzlpPY+Nmt>F&K|A7Vi{rg6A0Y%XVIHuVb`TvzbXfCr%qgNV%Tb_)@uJ^pc!w zqfqaM!q=1N$ko&-cE=VI@ekZ!jZyz6E(zeW`dXZl=fjp2EwaCf&q@9#<@&KQ8+s+= zAJnS~>Q26DsEb0lKLhGaoep+?LGt|Wx26va#Z9lq8NFGQum;iU8mWX?_r=$6$AC`a@@CdXV!UJjsrF!P6MBgqrGfXsa zmKH0~z?tgi@a6`NN?kD)|ET>j|EnSYU9Ao||0|nnaE;r6t6(JY;EC_aTLu14usRpq zKBq7H^St5Z@MYbGNQgjF9S)__VaPS;1&A}&%!7Wg8|qWm0PnU4{c0oB%SW`wBN{|@wy2hhXTfe!@MSZn_;ToZeyT03Wk zx5DP#1HFgz!fUj?ZXvCY=Hhe_9Qc7Xx)|t9pUJ)>Z|xXA=o288x8(iowtqm!J;3kN z@R+@}5wHdB@?W9#H3_uNIh|TwgN-E4!0-DQ;`S2(_^^Z0u2z7qcz8oPW4b2Bu{?X? z|0VDZv0wCkLZy|?p|z`K Date: Fri, 19 Dec 2025 14:09:19 -0500 Subject: [PATCH 2/3] v1.0.1, macos release --- codeshuffler/backlog.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/codeshuffler/backlog.md b/codeshuffler/backlog.md index 8e70753..6d76110 100644 --- a/codeshuffler/backlog.md +++ b/codeshuffler/backlog.md @@ -1,16 +1,19 @@ -- **Backlog** +- **v1.0.0** - ~~Refactor Exam Shuffling Integration~~ - ~~Improved error handling and robustness~~ - - Keep Exam Code Snippet Integrity + - ~~Keep Exam Code Snippet Integrity~~ - ~~Create a code block from parsed exam, use that formatting as the expected template on inputs~~ - ~~Fully integrate code-block detection into parser, test rigourously~~ - ~~Improve styling on code-blocks~~ - ~~Finalize template & exam outputs~~ - - Two-block exam format - ~~Output 2 Exams, one w/ answer key, one without~~ - ~~The user will input a template where correct MCQs will be highlighted, parser outputs two exam versions~~ + + - ~~Menu Bar Branding & Packaging~~ + - ~~Application Packaging, Executable Wrapping~~ +**v1.0.1** - Save previous session when closing and re-opening - Save settings across sessions (opt for a config rather than settings.py) @@ -19,6 +22,5 @@ - UI Improvements - Abstract more functionality away from the GUI - - - ~~Menu Bar Branding & Packaging~~ - - ~~Application Packaging, Executable Wrapping~~ \ No newline at end of file + + - Allow for template changing through settings menu \ No newline at end of file From b818f2cabf5d8662d8977c9d290d1251e3998042 Mon Sep 17 00:00:00 2001 From: ShawnSpitzel Date: Mon, 22 Dec 2025 19:49:25 -0500 Subject: [PATCH 3/3] macos --- codeshuffler/backlog.md | 5 +- codeshuffler/gui/cache/inputs/mergelists.py | 25 + codeshuffler/gui/interface.py | 514 ------------------ .../__pycache__/examshuffler.cpython-313.pyc | Bin 9215 -> 9208 bytes .../utils/__pycache__/syntax.cpython-313.pyc | Bin 4623 -> 4533 bytes codeshuffler/gui/utils/syntax.py | 2 - .../lib/__pycache__/parser.cpython-313.pyc | Bin 11802 -> 11707 bytes 7 files changed, 28 insertions(+), 518 deletions(-) create mode 100644 codeshuffler/gui/cache/inputs/mergelists.py delete mode 100644 codeshuffler/gui/interface.py diff --git a/codeshuffler/backlog.md b/codeshuffler/backlog.md index d765366..c466cbc 100644 --- a/codeshuffler/backlog.md +++ b/codeshuffler/backlog.md @@ -1,6 +1,7 @@ - **Backlog** - ~~Refactor Exam Shuffling Integration~~ - ~~Improved error handling and robustness~~ + - Keep Exam Code Snippet Integrity - ~~Create a code block from parsed exam, use that formatting as the expected template on inputs~~ - ~~Fully integrate code-block detection into parser, test rigourously~~ @@ -8,8 +9,8 @@ - ~~Finalize template & exam outputs~~ - Two-block exam format - - Output 2 Exams, one w/ answer key, one without - - The user will input a template where correct MCQs will be highlighted, parser outputs two exam versions + - ~~Output 2 Exams, one w/ answer key, one without~~ + - ~~The user will input a template where correct MCQs will be highlighted, parser outputs two exam versions~~ - Save previous session when closing and re-opening - Save settings across sessions (opt for a config rather than settings.py) diff --git a/codeshuffler/gui/cache/inputs/mergelists.py b/codeshuffler/gui/cache/inputs/mergelists.py new file mode 100644 index 0000000..3e31a93 --- /dev/null +++ b/codeshuffler/gui/cache/inputs/mergelists.py @@ -0,0 +1,25 @@ +def merge_sorted(l1, l2): + i = j = 0 + result = [] + while i < len(l1) and j < len(l2): + if l1[i] < l2[j]: + result.append(l1[i]) + i += 1 + else: + result.append(l2[j]) + j += 1 + while i < len(l1): + result.append(l1[i]) + i += 1 + while j < len(l2): + result.append(l2[j]) + j += 1 + return result + + +# Incorrect lines below +incorrect_lines = { + "if l1[i] < l2[j]:": "if l1[i] > l2[j]:", + "while i < len(l1):": "for i in range(len(l1)):", + "return result": "return []", +} diff --git a/codeshuffler/gui/interface.py b/codeshuffler/gui/interface.py deleted file mode 100644 index cbde9a4..0000000 --- a/codeshuffler/gui/interface.py +++ /dev/null @@ -1,514 +0,0 @@ -import os -import random -import sys - -from PyQt5.QtGui import QColor, QDragEnterEvent, QDropEvent -from PyQt5.QtWidgets import ( - QAction, - QFileDialog, - QHBoxLayout, - QLabel, - QListWidget, - QListWidgetItem, - QMainWindow, - QMessageBox, - QPlainTextEdit, - QPushButton, - QTabWidget, - QTextEdit, - QVBoxLayout, - QWidget, -) - -from codeshuffler.gui.utils.settings import SettingsDialog -from codeshuffler.gui.utils.syntax import GenericHighlighter -from codeshuffler.lib.generator import ( - gen_correct_answer, - gen_random_choices_wICinst, - generate_partials, - incorrect_instructions, -) -from codeshuffler.lib.parser import create_exam_docx, parse_exam, shuffle_answers, shuffle_questions -from codeshuffler.lib.utils import download_image, shuffle_sol -from codeshuffler.models.codefile import CodeFile -from codeshuffler.settings import settings - -BASE_PATH = os.path.join(os.getcwd(), "codeshuffler", "gui", "cache") -os.makedirs(BASE_PATH, exist_ok=True) - - -class CodeShufflerGUI(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("CodeShuffler") - self.setGeometry(200, 100, 900, 600) - - menu_bar = self.menuBar() - menu_bar.setStyleSheet( - """ - QMenuBar { - background-color: #252526; - color: white; - font-size: 13px; - padding: 4px; - } - QMenuBar::item:selected { - background-color: #3a3a3a; - } - QMenu { - background-color: #2d2d2d; - color: white; - border: 1px solid #3c3c3c; - } - QMenu::item:selected { - background-color: #3c3c3c; - } - """ - ) - # check to see if we're on mac or windows, pyqt handles menubars differently on each os - if sys.platform == "darwin": - file_menu = menu_bar.addMenu("CodeShuffler") - else: - file_menu = menu_bar.addMenu("&File") - - about_action = QAction("About CodeShuffler", self) - settings_action = QAction("Settings...", self) - clear_cache_action = QAction("Clear Image Cache", self) - quit_action = QAction("Quit CodeShuffler", self) - - about_action.setMenuRole(QAction.AboutRole) - settings_action.setMenuRole(QAction.PreferencesRole) - quit_action.setMenuRole(QAction.QuitRole) - - about_action.triggered.connect( - lambda: QMessageBox.information( - self, - "About CodeShuffler", - "CodeShuffler v1.0\nA code randomization and visualization tool.", - ) - ) - settings_action.triggered.connect(self.open_settings) - clear_cache_action.triggered.connect(self.clear_image_cache) - quit_action.triggered.connect(self.quit_codeshuffler) - - file_menu.addAction(about_action) - file_menu.addAction(settings_action) - file_menu.addSeparator() - file_menu.addAction(clear_cache_action) - file_menu.addSeparator() - file_menu.addAction(quit_action) - - self.tabs = QTabWidget() - self.setCentralWidget(self.tabs) - - # TAB 1: CodeShuffler - self.code_tab = QWidget() - self.tabs.addTab(self.code_tab, "CodeShuffler") - code_tab_layout = QVBoxLayout(self.code_tab) - - self.exam_tab = QWidget() - self.tabs.addTab(self.exam_tab, "Exam Shuffler") - self.init_exam_tab() - - top_bar = QHBoxLayout() - logo_label = QLabel() - title_label = QLabel("CodeShuffler") - title_label.setStyleSheet( - """ - QLabel { - color: #000000; - font-size: 18px; - font-weight: bold; - font-family: 'Segoe UI', 'Roboto', sans-serif; - padding: 5px 10px; - } - """ - ) - top_bar.addWidget(logo_label) - top_bar.addWidget(title_label) - top_bar.addStretch() - - content_layout = QHBoxLayout() - left_layout = QVBoxLayout() - right_layout = QVBoxLayout() - - self.drop_label = QLabel( - "Drop code file here or click to browse\n(JS, TS, Python, Java, C++, etc.)" - ) - - self.code_drop_area = QPlainTextEdit() - self.code_drop_area.setReadOnly(True) - self.code_drop_area.setAcceptDrops(False) - self.code_drop_area.setPlaceholderText("Drop a code file here or click below to browse...") - self.code_drop_area.setStyleSheet( - """ - QPlainTextEdit { - border: 2px solid #aaa; - border-radius: 8px; - background-color: #1e1e1e; - color: #dcdcdc; - font-family: 'Source Code Pro', 'Consolas', monospace; - font-size: 13px; - padding: 10px; - } - """ - ) - - self.shuffle_btn = QPushButton("Shuffle") - self.shuffle_btn.clicked.connect(self.shuffle_code) - - self.settings_btn = QPushButton("Settings") - self.settings_btn.clicked.connect(self.open_settings) - - left_layout.addWidget(QLabel("Original Code Preview")) - left_layout.addWidget(self.code_drop_area) - left_layout.addWidget(self.shuffle_btn) - left_layout.addWidget(self.settings_btn) - - self.code_preview = QTextEdit() - self.code_preview.setReadOnly(True) - self.code_preview.setPlaceholderText("Shuffled code will appear here...") - - self.answer_choices = QListWidget() - self.answer_choices.addItem("Multiple choice options will appear here...") - - self.download_btn = QPushButton("Download PNG") - self.download_btn.clicked.connect(self.download_png) - - right_layout.addWidget(QLabel("Shuffled Code")) - right_layout.addWidget(self.code_preview) - right_layout.addWidget(QLabel("Answer Choices")) - right_layout.addWidget(self.answer_choices) - right_layout.addWidget(self.download_btn) - - content_layout.addLayout(left_layout, 1) - content_layout.addLayout(right_layout, 1) - - code_tab_layout.addLayout(top_bar) - code_tab_layout.addLayout(content_layout) - - self.current_file = None - self.filename = None - self.shuffled_question = None - self.template = "codeshuffler/codefiles/templates/CodeShufflersTemplate.docx" - - def init_exam_tab(self): - layout = QVBoxLayout(self.exam_tab) - - title = QLabel("Exam Shuffler") - title.setStyleSheet("font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - self.exam_drop_area = QPlainTextEdit() - self.exam_drop_area.setReadOnly(True) - self.exam_drop_area.setPlaceholderText("Drop a .docx exam file here...") - self.exam_drop_area.setStyleSheet( - """ - QPlainTextEdit { - border: 2px dashed #888; - border-radius: 8px; - background-color: #1e1e1e; - color: #dcdcdc; - padding: 10px; - font-family: 'Source Code Pro'; - } - """ - ) - - layout.addWidget(self.exam_drop_area) - self.exam_tab.setAcceptDrops(True) - self.exam_tab.dragEnterEvent = self.exam_drag_enter - self.exam_tab.dropEvent = self.exam_drop_event - - button_row = QHBoxLayout() - - self.btn_shuffle_q = QPushButton("Shuffle Questions") - self.btn_shuffle_q.clicked.connect(self.exam_shuffle_questions) - - self.btn_shuffle_a = QPushButton("Shuffle Answers") - self.btn_shuffle_a.clicked.connect(self.exam_shuffle_answers) - - self.btn_shuffle_both = QPushButton("Shuffle Both") - self.btn_shuffle_both.clicked.connect(self.exam_shuffle_both) - - button_row.addWidget(self.btn_shuffle_q) - button_row.addWidget(self.btn_shuffle_a) - button_row.addWidget(self.btn_shuffle_both) - - layout.addLayout(button_row) - - self.save_exam_btn = QPushButton("Save Shuffled Exam") - self.save_exam_btn.clicked.connect(self.save_exam_doc) - layout.addWidget(self.save_exam_btn) - - self.exam_dict = None - self.exam_file_path = None - - def exam_drag_enter(self, event): - if event.mimeData().hasUrls(): - event.acceptProposedAction() - - def exam_drop_event(self, event): - for url in event.mimeData().urls(): - file_path = url.toLocalFile() - if file_path.lower().endswith(".docx"): - self.exam_file_path = file_path - self.exam_dict = parse_exam(file_path) - self.exam_drop_area.setPlainText( - f"Loaded exam: {file_path}\n\nFound {len(self.exam_dict)} questions." - ) - else: - QMessageBox.warning(self, "Invalid File", "Please upload a .docx exam.") - - def exam_shuffle_questions(self): - if not self.exam_dict: - QMessageBox.warning(self, "No Exam", "Upload an exam first.") - return - self.exam_dict = shuffle_questions(self.exam_dict) - self.exam_drop_area.appendPlainText("\nQuestions shuffled.") - - def exam_shuffle_answers(self): - if not self.exam_dict: - QMessageBox.warning(self, "No Exam", "Upload an exam first.") - return - self.exam_dict = shuffle_answers(self.exam_dict) - self.exam_drop_area.appendPlainText("\nAnswers shuffled.") - - def exam_shuffle_both(self): - if not self.exam_dict: - QMessageBox.warning(self, "No Exam", "Upload an exam first.") - return - self.exam_dict = shuffle_answers(shuffle_questions(self.exam_dict)) - self.exam_drop_area.appendPlainText("\nQuestions + Answers shuffled.") - - def save_exam_doc(self): - if not self.exam_dict: - QMessageBox.warning(self, "No Exam", "No shuffled exam to save.") - return - - save_path, _ = QFileDialog.getSaveFileName( - self, "Save Shuffled Exam", "shuffled_exam.docx", "Word Documents (*.docx)" - ) - - if not save_path: - return - - try: - create_exam_docx(self.template, self.exam_dict, save_path) - QMessageBox.information(self, "Saved", f"Shuffled exam saved to:\n{save_path}") - except Exception as e: - QMessageBox.critical(self, "Error Saving", str(e)) - - def dragEnterEvent(self, event: QDragEnterEvent): - if event.mimeData().hasUrls(): - event.acceptProposedAction() - self.code_drop_area.setStyleSheet( - """ - QPlainTextEdit { - border: 2px solid #00bfff; - background-color: #1e1e1e; - color: #dcdcdc; - font-family: 'Source Code Pro', 'Consolas', monospace; - font-size: 13px; - padding: 10px; - } - """ - ) - - def dragLeaveEvent(self, event): - self.code_drop_area.setStyleSheet( - """ - QPlainTextEdit { - border: 2px solid #aaa; - background-color: #1e1e1e; - color: #dcdcdc; - font-family: 'Source Code Pro', 'Consolas', monospace; - font-size: 13px; - padding: 10px; - } - """ - ) - - def dropEvent(self, event: QDropEvent): - for url in event.mimeData().urls(): - file_path = url.toLocalFile() - if os.path.isfile(file_path): - self.save_dropped_file(file_path) - - def save_dropped_file(self, file_path): - filename = os.path.basename(file_path) - self.filename = filename - save_path = os.path.join(BASE_PATH, filename) - print(f"Saving dropped file to: {save_path}") - - with open(file_path, "rb") as source, open(save_path, "wb") as dest: - dest.write(source.read()) - self.current_file = CodeFile(save_path) - try: - self.current_file.load() - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to parse file: {e}") - self.current_file = None - return - - with open(save_path, "r", encoding="utf-8") as f: - code = f.read() - self.code_drop_area.setPlainText(code) - - if self.current_file.warning_msg: - QMessageBox.warning(self, "Duplicate Keys Detected", self.current_file.warning_msg) - file_ext = os.path.splitext(filename)[1].lower() - if file_ext in [".py"]: - lang = "python" - elif file_ext in [".cpp", ".cc", ".h", ".hpp"]: - lang = "cpp" - elif file_ext in [".java"]: - lang = "java" - elif file_ext in [".js", ".ts"]: - lang = "javascript" - else: - lang = "python" # fallback - - self.highlighter = GenericHighlighter(self.code_drop_area.document(), language=lang) - - self.code_drop_area.setStyleSheet( - """ - QPlainTextEdit { - border: 2px solid #555; - border-radius: 8px; - background-color: #1e1e1e; - color: #dcdcdc; - font-family: 'Source Code Pro', 'Consolas', monospace; - font-size: 13px; - padding: 10px; - } - """ - ) - - def browse_file(self, event): - file_path, _ = QFileDialog.getOpenFileName(self, "Select code file", "", "All Files (*.*)") - if file_path: - self.save_dropped_file(file_path) - - def shuffle_code(self): - if not self.current_file: - QMessageBox.warning(self, "No File", "Please upload a file first.") - return - file = self.current_file - correct_sol_w_incorrect = incorrect_instructions(file.correct_sol, file.wrong_inst) - shuffled_code = shuffle_sol(correct_sol_w_incorrect) - self.shuffled_question = shuffled_code - formatted_code = "\n".join(shuffled_code) - self.code_preview.setPlainText(formatted_code) - - # --- generate options --- - correct_answer, remain_lines = gen_correct_answer(file.correct_sol, shuffled_code) - - partial_options = generate_partials( - len(file.wrong_inst_dict), shuffled_code, file.wrong_inst_dict, correct_answer - ) - try: - random_choices = gen_random_choices_wICinst( - correct_answer, settings.no_of_choices, remain_lines - ) - except ValueError as e: - QMessageBox.critical(self, "Invalid Setting", str(e)) - return - num_partials = len(partial_options) - if num_partials + 1 >= settings.no_of_choices: - QMessageBox.warning( - self, - "Warning", - "Number of partial options exceeds or equals number of choices. " - "Some answers might be missing!", - ) - - candidate_indices = [ - i for i, choice in enumerate(random_choices) if choice != correct_answer - ] - num_replacements = min(num_partials, len(candidate_indices)) - - if num_replacements > 0: - replace_indices = random.sample(candidate_indices, num_replacements) - for partial_choice, idx in zip(partial_options[:num_replacements], replace_indices): - random_choices[idx] = partial_choice - self.answer_choices.clear() - letters = ["a", "b", "c", "d", "e", "f", "g"] - - scored_options = [] - - for choice in random_choices: - if choice == correct_answer: - color = QColor("#4CAF50") - score = 1.0 - elif choice in partial_options: - color = QColor("#FFA500") - swap_index = partial_options.index(choice) - swaps = swap_index + 1 - score = max(0, 1 - 0.25 * swaps) - else: - color = QColor("#000000") - score = 0.0 - scored_options.append((choice, color, score)) - scored_options.sort(key=lambda x: x[2], reverse=True) - self.answer_choices.clear() - letters = ["a", "b", "c", "d", "e", "f", "g"] - for i, (choice, color, score) in enumerate(scored_options): - item_text = f"{letters[i]}) {choice} | Score: {score:.2f}" - item = QListWidgetItem(item_text) - item.setForeground(color) - self.answer_choices.addItem(item) - - def download_png(self): - if not self.code_preview.toPlainText(): - QMessageBox.warning(self, "No Code", "No shuffled code to download.") - return - - image_shuffled_sol = [] - - for el in range(len(self.shuffled_question)): - line = self.shuffled_question[el].split(")", 1) - image_shuffled_sol.append(line[0] + ") " + line[-1].strip()) - - file_path, _ = QFileDialog.getSaveFileName( - self, "Save as PNG", "shuffled_code.png", "PNG Files (*.png)" - ) - if file_path: - try: - download_image(image_shuffled_sol, file_path) - QMessageBox.information(self, "Saved", f"Shuffled code saved to {file_path}") - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to save PNG: {e}") - - def open_settings(self): - dialog = SettingsDialog(self) - dialog.exec_() - - def clear_image_cache(self): - cache_folder = os.path.join(BASE_PATH, "inputs") - if not os.path.exists(cache_folder): - QMessageBox.information(self, "Clear Cache", "No image cache found.") - return - - deleted = 0 - for filename in os.listdir(cache_folder): - file_path = os.path.join(cache_folder, filename) - try: - if os.path.isfile(file_path): - os.remove(file_path) - deleted += 1 - except Exception as e: - QMessageBox.warning(self, "Error", f"Failed to delete {filename}: {e}") - - QMessageBox.information(self, "Cache Cleared", f"Deleted {deleted} cached image(s).") - - def quit_codeshuffler(self): - confirm = QMessageBox.question( - self, - "Quit CodeShuffler", - "Are you sure you want to quit?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, - ) - if confirm == QMessageBox.Yes: - sys.exit(0) diff --git a/codeshuffler/gui/tabs/__pycache__/examshuffler.cpython-313.pyc b/codeshuffler/gui/tabs/__pycache__/examshuffler.cpython-313.pyc index 7219c15363fbd2db800faaeabb6997b8f268dab6..d28c2c7cb4f032dbcc0454a3cf509b291cddc43b 100644 GIT binary patch delta 208 zcmV;>05AXlNBBn$^9>CO00000(XK0L`4o{3i2(_bm}VeTX?kT)Z)|B}dD;t6Rzh!h zOksI%b#&SXQCDeXXJvG=9sw5#0Sc2g3fKV&lMf4i0kX4>3#kDE;0O$}JPni*0iKi5 zA0r*m5-QLvNYF}S^8*4H3W*Ge3HAs85EzrucOdftm$PdjECB%`lb9ki1rY^5_a?K@A{+q|Ndf^BkY|{G^#Dl% K0Tgm20001diACN3 delta 217 zcmez2{@FLqz}p diff --git a/codeshuffler/gui/utils/__pycache__/syntax.cpython-313.pyc b/codeshuffler/gui/utils/__pycache__/syntax.cpython-313.pyc index 78ca3f67eb82486afff8da1762e8712988cb2957..33c7c0eaf4bc9c7fd568509954bc52d1419a52c3 100644 GIT binary patch delta 196 zcmeBI*{aO@nU|M~0SNAI)y-s{$h$hw2*`Si~I68p>e87|bNa zkj0V3Si};{EX7d78q89}7Rq4DSj4WvpviVi$UQYLwJ0;$BQrfC2Z&2ji(aw-4ci>b z=*`W@x_KebJ!VF`$qfSOj2x3+2$)V*6ZGK&DJT{J5nU|M~0SLN;R5QaT@~*Zq19IXRrZU7Z6fp)f6fp%e7BL4i6|n@fg)*2h z1~W@BWN~CM7O@7iNHG+#1+x~hhcehQ7ICOBXtGcIAnL;g)L-T1lbN1TQl1JT6^bkK zN)js+GJu>MkU(mYLPjSQ9%)%A$o;H{suQsgU99u z=0og^qMMz$&oeXHZWiWW!NTSNbVsqtl~67IV5K&UFVSfWV}gKgNreBv!=WaE90@rX)1M0eTPqtI@XPh>IOzaVpA#?h+h`hyey>E!TyAUub;1rZ$kMM4*8q9CRfBvSA{R+ zTp&2PWMy~<_eC+&8)CAPKdSr6C^Cp^d{ANF5xu}7^7FGagSc5S8_>{$I(*KIo1-)i zGqY|0a$ikW(s|CfaWa=~s}{)KB9P!ph9XfATNp^(;;_lhPbtkwwJX{VPZAB0pH1r|7eVE|HJ0vI_M%|3}qG0J_gieqFHm?8Xy0YratV&-I&{3IdDDEiqz JkWme6G64DKsw)5h delta 691 zcmdlTJu8OqGcPX}0}!~a)6QI_w2_aOIh99zhSH4a8NL@Jv@Y{#cQD@I;OuAZWbH|s zAl{ul!}tn^JVHdQJ8O>ejHKD>S2&aqqLSUYbAlI$&W^gmp@AYgBlHS~@(q504vx(Y z%!k++MK(KgpJ!&Y*(}Vzf`!c;=#FCh$p?j9SwFBb2#HSS6mez!#>T)SHaSp4OyLU~ zgOu_D^J|jYot_rx{vaYI0L?}x%ICwxoWXc3085lGm6hjJ-@2kte5X}u0V2I|0u|io)86j*K zoyQu@2a^pFm@FtJGI_pgDkJY?RyBXdwUa~DR1`UjCIM9xO$HGuAR-k=+~UeC&de(= zNz6-5o!qB3i)$%R@D30cUz=>CZs5f)IYWJk$#s5>i~Jflc|?GLBrHCqa)J0|am~v@ zS{>{_0Z#sYzAnBA<-nNx$i|>&dPU51Rro^A1%i`HR)%+QUlcP1hM&0H