diff --git a/crossword_puzzle/base.py b/crossword_puzzle/base.py index d5fa76047..2618bfb32 100644 --- a/crossword_puzzle/base.py +++ b/crossword_puzzle/base.py @@ -64,6 +64,10 @@ def _route( All class instances that use ``_route`` must have their content packed and contain 4 content generation methods, as seen below. """ + if kwargs: # The caller of this route has added arguments for confirmation + if not self._confirm_route(**kwargs): + return False # User didn't want to route + try: page_inst = locals()[page_ref](base) except KeyError: @@ -75,10 +79,6 @@ def _route( page_inst = locals()[page_ref](base) - if kwargs: # The caller of this route has added arguments for confirmation - if not self._confirm_route(**kwargs): - return False # User didn't want to route - for widget in Base.base_container.winfo_children(): # Remove content widget.pack_forget() diff --git a/crossword_puzzle/constants.py b/crossword_puzzle/constants.py index 3f80fc232..d06aa4d64 100644 --- a/crossword_puzzle/constants.py +++ b/crossword_puzzle/constants.py @@ -66,7 +66,7 @@ class Dark: ACROSS: str = "ACROSS" DOWN: str = "DOWN" EMPTY: str = "\u25AE" -KEEP_LANGUAGES_PATTERN: Pattern = compile(r"\PL") +NONLANGUAGE_PATTERN: Pattern = compile(r"\PL") QUALITY_MAP: Dict[str, int] = { "terrible": 0.05, "poor": 0.25, diff --git a/crossword_puzzle/pages/editor.py b/crossword_puzzle/pages/editor.py index 87b84d175..6af6d62a5 100644 --- a/crossword_puzzle/pages/editor.py +++ b/crossword_puzzle/pages/editor.py @@ -2,11 +2,13 @@ the creation and editing of new crosswords, not pre-installed ones. """ -from os import mkdir, path, PathLike +from os import mkdir, path, PathLike, rename from tkinter import IntVar, Event -from typing import List, Union, Callable +from typing import List, Union, Callable, Dict from shutil import rmtree +from json import dump +from CTkToolTip import CTkToolTip from customtkinter import ( CTkButton, CTkFrame, @@ -18,6 +20,7 @@ CTkTextbox, ) from pathvalidate import validate_filename, ValidationError +from regex import search from crossword_puzzle.base import Addons, Base from crossword_puzzle.constants import ( @@ -27,6 +30,7 @@ PAGE_MAP, PREV_SCALE_MAP, DIFFICULTIES, + NONLANGUAGE_PATTERN, Colour, ) from crossword_puzzle.utils import ( @@ -42,7 +46,7 @@ class FormParser: @staticmethod - def _parse_name_entry(form) -> None: + def _parse_name(form) -> None: try: if len(form) <= 32 and validate_filename(str(form)) is None: form.set_valid() @@ -57,7 +61,7 @@ def _get_symbol_components(symbol): return [hex(ord(char)) for char in symbol] @staticmethod - def _parse_symbol_entry(form) -> None: + def _parse_symbol(form) -> None: components = FormParser._get_symbol_components(str(form)) if len(components) == 1: form.set_valid() @@ -65,6 +69,27 @@ def _parse_symbol_entry(form) -> None: form.set_invalid() form._check_default() + @staticmethod + def _parse_word(form) -> None: + if ( + len(form) < 1 + or len(form) > 32 + or "\\" in str(form) + or bool(search(NONLANGUAGE_PATTERN, str(form))) + ): + form.set_invalid() + else: + form.set_valid() + form._check_default() + + @staticmethod + def _parse_clue(form) -> None: + if len(form) < 1 or "\\" in str(form): + form.set_invalid() + else: + form.set_valid() + form._check_default() + class Form(CTkFrame, Addons): crossword_forms: List[object] = [] @@ -77,6 +102,7 @@ def __init__( validation_func: Callable, pane: str, b_confirm: CTkButton, + tooltip: str, ) -> None: self._set_fonts() super().__init__( @@ -99,6 +125,10 @@ def __init__( fg_color=(Colour.Light.SUB, Colour.Dark.SUB), bg_color=(Colour.Light.MAIN, Colour.Dark.MAIN), ) + self._tooltip = CTkToolTip( + self._form, message=tooltip, delay=0.2, y_offset=12 + ) + self.b_reset_default = CTkButton( self, text="↺", @@ -106,8 +136,8 @@ def __init__( height=28, width=28, font=self.TEXT_FONT, + state="disabled", ) - self.b_reset_default.configure(state="disabled") self._label.grid(row=0, column=0, sticky="w", padx=(5, 0), pady=(0, 5)) self._form.grid(row=1, column=0, pady=(0, 25)) @@ -126,15 +156,13 @@ def _any_nondefault_values(forms): return any(not form.has_default_value for form in forms) @staticmethod - def _update_confirm_button( - pane, b_confirm, lenient=False - ): + def _update_confirm_button(pane, b_confirm, lenient=False): forms = ( Form.crossword_forms if pane == "crossword" else Form.word_forms ) - if ( - Form._any_nondefault_values(forms) or lenient - ) and all(form.is_valid for form in forms): + if (Form._any_nondefault_values(forms) or lenient) and all( + form.is_valid for form in forms + ): b_confirm.configure(state="normal") else: b_confirm.configure(state="disabled") @@ -166,13 +194,15 @@ def put(self, text: str, is_default=False) -> None: if is_default: self.b_reset_default.configure(state="disabled") self.set_valid() - self.has_default_value = True self._check_default() - Form._update_confirm_button(self.pane, self.b_confirm) def set_state(self, state: str) -> None: self._label.configure(state=state) self._form.configure(state=state) + if state == "disabled": + self._tooltip.hide() + else: + self._tooltip.show() def set_valid(self) -> None: self._form.configure(text_color=("black", "white")) @@ -193,8 +223,8 @@ def _check_default(self) -> None: self.has_default_value = True Form._update_confirm_button(self.pane, self.b_confirm) - def set_default(self, text: str) -> None: - self.default = text + def set_default(self, text: Union[str, int]) -> None: + self.default = str(text) self.put(self.default, is_default=True) self.b_reset_default.configure(state="disabled") self.b_confirm.configure(state="disabled") @@ -214,7 +244,8 @@ def __init__(self, master: Base) -> None: self.master.winfo_height(), ) self.fp = self._get_user_category_path() - + self.scaling = float(Base.cfg.get("m", "scale")) + Form.crossword_forms = [] Form.word_forms = [] @@ -252,7 +283,12 @@ def _make_content(self) -> None: self.header_container, text=_("Back"), command=lambda: self._route( - "HomePage", self.master, _(PAGE_MAP["HomePage"]) + "HomePage", + self.master, + _(PAGE_MAP["HomePage"]), + condition=Form._any_nondefault_values(Form.crossword_forms) + or Form._any_nondefault_values(Form.word_forms), + confirmation={"exiting_with_nondefault_fields": True}, ), height=50, width=100, @@ -273,14 +309,15 @@ def _get_user_category_path(self) -> PathLike: sublevel=DOC_CAT_PATH, ): fp = DOC_CAT_PATH # Success, there is now a user category in - # sys documents, so update ``fp`` + # sys documents, so update ``fp`` _make_category_info_json(fp, "#FFFFFF") return fp def _handle_scroll( - self, event: Event, container: CTkScrollableFrame + self, event: Event ) -> None: + container = self.crossword_pane.preview if event.x_root - self.master.winfo_rootx() <= EDITOR_DIM[0] * self.scaling / 2 else self.word_pane.preview scroll_region = container._parent_canvas.cget("scrollregion") viewable_height = container._parent_canvas.winfo_height() if ( @@ -305,6 +342,10 @@ def _set_form_defaults(self, *args, forms): # len(args) == len(forms) for i, form in enumerate(forms): form.set_default(args[i]) + def _write_data(self, toplevel, data, type_): + file = "info.json" if type_ == "info" else "definitions.json" + with open(path.join(toplevel, file), "w") as f: + dump(data, f, indent=4) class CrosswordPane(CTkFrame, Addons): def __init__(self, container: CTkFrame, master: EditorPage) -> None: @@ -315,7 +356,8 @@ def __init__(self, container: CTkFrame, master: EditorPage) -> None: height=master._width * 0.85, corner_radius=0, ) - self.name = "crossword" + self.pane_name = "crossword" + self.mode = "" self.master = master self.crossword_block: Union[None, UserCrosswordBlock] = None self.grid(row=0, column=0, sticky="nsew", padx=(0, 1)) @@ -349,10 +391,11 @@ def _make_content(self) -> None: * PREV_SCALE_MAP[Base.cfg.get("m", "scale")], fg_color=(Colour.Light.SUB, Colour.Dark.SUB), scrollbar_button_color=(Colour.Light.MAIN, Colour.Dark.MAIN), + width=250, ) self.preview.bind_all( "", - lambda e: self.master._handle_scroll(e, self.preview), + lambda e: self.master._handle_scroll(e), ) self.b_remove = CTkButton( @@ -362,8 +405,8 @@ def _make_content(self) -> None: height=30, font=self.TEXT_FONT, command=self._remove, + state="disabled", ) - self.b_remove.configure(state="disabled") self.b_add = CTkButton( self.b_edit_container, text="+", @@ -379,21 +422,23 @@ def _make_content(self) -> None: font=self.TEXT_FONT, height=50, command=self._write, + state="disabled", ) - self.b_confirm.configure(state="disabled") self.name_form = Form( self.form_container, "name", - FormParser._parse_name_entry, - pane=self.name, + FormParser._parse_name, + pane=self.pane_name, b_confirm=self.b_confirm, + tooltip=_("len >= 1 and valid OS file name"), ) self.symbol_form = Form( self.form_container, "symbol", - FormParser._parse_symbol_entry, - pane=self.name, + FormParser._parse_symbol, + pane=self.pane_name, b_confirm=self.b_confirm, + tooltip=_("len == 1 and code points == 1"), ) self.l_difficulty = CTkLabel( @@ -410,7 +455,7 @@ def _make_content(self) -> None: self.form_container, font=self.TEXT_FONT, values=self.difficulties, - command=self._on_difficulty_click, + command=self._update_difficulty, ) self.opts_difficulty.set("") @@ -432,9 +477,11 @@ def _place_content(self) -> None: self.b_confirm.grid(row=4, column=0, sticky="w") self.form_container.grid(row=1, column=1, sticky="n") - def _on_difficulty_click(self, difficulty): + def _update_difficulty(self, difficulty): self.difficulty = DIFFICULTIES[self.difficulties.index(difficulty)] - Form._update_confirm_button(self.name, self.b_confirm, lenient=True) + Form._update_confirm_button( + self.pane_name, self.b_confirm, lenient=True + ) def _toggle_forms(self, state, forms): self.master._toggle_forms(state, forms) @@ -442,28 +489,35 @@ def _toggle_forms(self, state, forms): self.opts_difficulty.configure(state=state) def _remove(self): - if not GUIHelper.confirm_with_messagebox(self.name, delete_cword_or_word=True): - return - + if not GUIHelper.confirm_with_messagebox( + self.pane_name, delete_cword_or_word=True + ): + return + fp = self.crossword_block.cwrapper.toplevel rmtree(fp) - - UserCrosswordBlock._set_all(UserCrosswordBlock._remove_block) + + self._reset() + self.b_add.configure(state="normal") UserCrosswordBlock._populate(self) - self.master._reset_forms(Form.crossword_forms, set_invalid=True) - self._toggle_forms("disabled", Form.crossword_forms) - self.b_remove.configure(state="disabled") - self.b_confirm.configure(state="disabled") - self.master._set_form_defaults("", "", forms=Form.crossword_forms) - self.opts_difficulty.set("") + self.master.word_pane._reset() def _add(self) -> None: if Form._any_nondefault_values(Form.crossword_forms): - if not GUIHelper.confirm_with_messagebox(self.name, confirm_cword_or_word_add=True): - return + if not GUIHelper.confirm_with_messagebox( + self.pane_name, confirm_cword_or_word_add=True + ): + return - if (intvar := UserCrosswordBlock.selected_block): + if intvar := UserCrosswordBlock.selected_block: intvar.set(-1) + + self.mode = "add" + self.crossword_block = None + self.master.word_pane._reset() + self.l_title.configure( + text=_("Your Crosswords") + " ({})".format(_("Adding")) + ) self.b_remove.configure(state="disabled") self.b_confirm.configure(text=_("Add")) self._toggle_forms("normal", Form.crossword_forms) @@ -473,11 +527,75 @@ def _add(self) -> None: self.master._set_form_defaults("", "", forms=Form.crossword_forms) self.focus_force() + def _get_cword_dirname(self, name): + dir_name = name + if not any( + dir_name.endswith(diff.casefold()) for diff in DIFFICULTIES + ): + hyphen_char = "" if dir_name.endswith("-") else "-" + dir_name += hyphen_char + dir_name += self.difficulty.casefold() + + return dir_name + def _write(self) -> None: - print(str(self.name_form)) - print(str(self.symbol_form)) - print(self.difficulty) - print("writing...") + difficulty = DIFFICULTIES.index(self.difficulty) + symbol = hex(ord(str(self.symbol_form))) + name = str(self.name_form) + translated_name = str(self.name_form) + + dir_name = self._get_cword_dirname(name) + + if self.mode == "add": + info = CrosswordInfo( + total_definitions=0, + difficulty=difficulty, + symbol=symbol, + name=name, + translated_name=translated_name, + category="user", + ) + + toplevel = path.join(self.master.fp, dir_name) + try: + mkdir(toplevel) + except FileExistsError: + return GUIHelper.show_messagebox(crossword_exists_err=True) + self.master._write_data(toplevel, info, "info") + self.master._write_data(toplevel, {}, "definitions") + + elif self.mode == "edit": + cwrapper = self.crossword_block.cwrapper + info = cwrapper.info + info["difficulty"] = difficulty + info["symbol"] = symbol + info["name"] = name + info["translated_name"] = translated_name + + toplevel = cwrapper.toplevel + prior_level = path.dirname(toplevel) + + self.master._write_data(toplevel, info, "info") + rename(toplevel, path.join(prior_level, dir_name)) + + self.master.word_pane._reset() + self._reset() + self.b_add.configure(state="normal") + UserCrosswordBlock._populate(self) + GUIHelper.show_messagebox(wrote_cword=True) + + def _reset(self) -> None: + UserCrosswordBlock._set_all(UserCrosswordBlock._remove_block) + self.l_title.configure(text=_("Your Crosswords")) + self.master._reset_forms(Form.crossword_forms, set_invalid=True) + self._toggle_forms("disabled", Form.crossword_forms) + self.b_confirm.configure(text=_("Save")) + self.b_remove.configure(state="disabled") + self.b_add.configure(state="disabled") + self.b_confirm.configure(state="disabled") + self.opts_difficulty.set("") + self.master._set_form_defaults("", "", forms=Form.crossword_forms) + self.focus_force() class UserCrosswordBlock(CTkFrame, Addons, BlockUtils): @@ -540,7 +658,7 @@ def _make_content(self) -> None: wrap="none", fg_color=(Colour.Light.SUB, Colour.Dark.SUB), height=10, - width=125, + width=165, ) self.tb_name.insert(1.0, self.cwrapper.name) self.tb_name.configure(state="disabled") @@ -556,12 +674,19 @@ def _make_content(self) -> None: def _place_content(self) -> None: self.tb_name.pack(side="left", padx=10, pady=10) - self.rb_selector.pack(side="right", padx=10, pady=10) + self.rb_selector.pack(side="left", padx=10, pady=10) def _on_selection(self) -> None: if Form._any_nondefault_values(Form.crossword_forms): - if not GUIHelper.confirm_with_messagebox(self.master.name, confirm_cword_or_word_add=True): + if not GUIHelper.confirm_with_messagebox( + self.master.pane_name, confirm_cword_or_word_add=True + ): return + + self.master.mode = "edit" + self.master.l_title.configure( + text=_("Your Crosswords") + " ({})".format(_("Editing")) + ) self.master.crossword_block = self self.master.difficulty = self.cwrapper.difficulty self.master._toggle_forms("normal", Form.crossword_forms) @@ -575,6 +700,13 @@ def _on_selection(self) -> None: self.master.opts_difficulty.set(self.localised_difficulty) self.master.b_remove.configure(state="normal") + self.master.master.word_pane._reset() + self.master.master.word_pane.b_add.configure(state="normal") + UserWordBlock._set_all(UserWordBlock._remove_block) + UserWordBlock._populate( + self.master.master.word_pane, self.cwrapper.definitions + ) + class WordPane(CTkFrame, Addons): def __init__(self, container: CTkFrame, master: EditorPage) -> None: @@ -585,13 +717,15 @@ def __init__(self, container: CTkFrame, master: EditorPage) -> None: height=master._width * 0.85, corner_radius=0, ) - self.name = "word" + self.pane_name = "word" self.master = master + self.word = "" self.grid(row=0, column=1, sticky="nsew") self._set_fonts() self._make_content() self._place_content() + self.master._toggle_forms("disabled", Form.word_forms) def _make_content(self): self.container = CTkFrame( @@ -604,10 +738,10 @@ def _make_content(self): self.form_container = CTkFrame( self.container, fg_color=(Colour.Light.MAIN, Colour.Dark.MAIN) ) - self.form_container.grid_rowconfigure((0, 1, 2, 3, 4), weight=1) + self.form_container.grid_rowconfigure((0, 1, 2), weight=1) self.l_title = CTkLabel( - self.container, text=_("Placeholder's Words"), font=self.TITLE_FONT + self.container, text=_("Your Words"), font=self.TITLE_FONT ) self.preview = CTkScrollableFrame( @@ -617,6 +751,7 @@ def _make_content(self): * PREV_SCALE_MAP[Base.cfg.get("m", "scale")], fg_color=(Colour.Light.SUB, Colour.Dark.SUB), scrollbar_button_color=(Colour.Light.MAIN, Colour.Dark.MAIN), + width=250, ) self.b_remove = CTkButton( @@ -625,6 +760,8 @@ def _make_content(self): width=40, height=30, font=self.TEXT_FONT, + state="disabled", + command=self._remove, ) self.b_add = CTkButton( self.b_edit_container, @@ -632,33 +769,34 @@ def _make_content(self): width=40, height=30, font=self.TEXT_FONT, + state="disabled", + command=self._add, ) - self.l_word = CTkLabel( - self.form_container, text=_("Word"), font=self.BOLD_TEXT_FONT - ) - self.e_word = CTkEntry( + self.b_confirm = CTkButton( self.form_container, + text=_("Save"), font=self.TEXT_FONT, - fg_color=(Colour.Light.SUB, Colour.Dark.SUB), - bg_color=(Colour.Light.MAIN, Colour.Dark.MAIN), - ) - - self.l_clue = CTkLabel( - self.form_container, text=_("Clue"), font=self.BOLD_TEXT_FONT + height=50, + state="disabled", + command=self._write, ) - self.e_clue = CTkEntry( + self.word_form = Form( self.form_container, - font=self.TEXT_FONT, - fg_color=(Colour.Light.SUB, Colour.Dark.SUB), - bg_color=(Colour.Light.MAIN, Colour.Dark.MAIN), + "word", + FormParser._parse_word, + pane=self.pane_name, + b_confirm=self.b_confirm, + tooltip=_("len >= 1 and len <= 32 and is a language character"), ) - self.b_confirm = CTkButton( + self.clue_form = Form( self.form_container, - text=_("Save"), - font=self.TEXT_FONT, - height=50, + "clue", + FormParser._parse_clue, + pane=self.pane_name, + b_confirm=self.b_confirm, + tooltip=_("len >= 1"), ) def _place_content(self) -> None: @@ -668,9 +806,169 @@ def _place_content(self) -> None: self.b_add.pack(side="right", anchor="e") self.b_remove.pack(side="right", anchor="e", padx=(81.5, 10)) self.b_edit_container.grid(row=2, column=0, pady=(7.5, 0)) - self.l_word.grid(row=0, column=0, sticky="w", padx=(5, 0), pady=(0, 5)) - self.e_word.grid(row=1, column=0, pady=(0, 25)) - self.l_clue.grid(row=2, column=0, sticky="w", padx=(5, 0), pady=(0, 5)) - self.e_clue.grid(row=3, column=0, pady=(0, 45)) - self.b_confirm.grid(row=4, column=0) + self.word_form.grid(row=0, column=0) + self.clue_form.grid(row=1, column=0) + self.b_confirm.grid(row=2, column=0, sticky="w", pady=(20, 0)) self.form_container.grid(row=1, column=1, sticky="n") + + def _add(self): + if Form._any_nondefault_values(Form.word_forms): + if not GUIHelper.confirm_with_messagebox( + self.pane_name, confirm_cword_or_word_add=True + ): + return + + if intvar := UserWordBlock.selected_block: + intvar.set(-1) + + self.mode = "add" + self.l_title.configure( + text=_("Your Words") + " ({})".format(_("Adding")) + ) + self.b_remove.configure(state="disabled") + self.b_confirm.configure(text=_("Add")) + self.master._toggle_forms("normal", Form.word_forms) + self.master._reset_forms(Form.word_forms, set_invalid=True) + self.master._set_form_defaults("", "", forms=Form.word_forms) + self.focus_force() + + def _remove(self): + if not GUIHelper.confirm_with_messagebox( + self.pane_name, delete_cword_or_word=True + ): + return + + cwrapper = self.master.crossword_pane.crossword_block.cwrapper + definitions = cwrapper.definitions + del definitions[self.word] + self.master._write_data(cwrapper.toplevel, definitions, "definitions") + + self._reset() + self.b_add.configure(state="normal") + UserWordBlock._populate(self, definitions) + + def _write(self): + cwrapper = self.master.crossword_pane.crossword_block.cwrapper + definitions = cwrapper.definitions + + if self.mode == "add": + if str(self.word_form) in definitions.keys(): + return GUIHelper.show_messagebox(word_exists_err=True) + + definitions[str(self.word_form)] = str(self.clue_form) + + elif self.mode == "edit": + del definitions[self.word] + definitions[str(self.word_form)] = str(self.clue_form) + + self.master._write_data(cwrapper.toplevel, definitions, "definitions") + + self._reset() + UserWordBlock._populate(self, definitions) + self.b_add.configure(state="normal") + GUIHelper.show_messagebox(wrote_word=True) + + def _reset(self): + self.l_title.configure(text=_("Your Words")) + UserWordBlock._set_all(UserWordBlock._remove_block) + self.master._reset_forms(Form.word_forms, set_invalid=True) + self.master._toggle_forms("disabled", Form.word_forms) + self.master._set_form_defaults("", "", forms=Form.word_forms) + self.b_confirm.configure(text=_("Save")) + self.b_confirm.configure(state="disabled") + self.b_add.configure(state="disabled") + self.b_remove.configure(state="disabled") + + +class UserWordBlock(CTkFrame, Addons, BlockUtils): + blocks: List[object] = [] + selected_block: Union[None, IntVar] + + def __init__( + self, + master: WordPane, + container: CTkScrollableFrame, + word: str, + clue: str, + value: int, + ): + super().__init__( + container, + corner_radius=10, + fg_color=(Colour.Light.MAIN, Colour.Dark.MAIN), + border_color=(Colour.Light.SUB, Colour.Dark.SUB), + border_width=3, + ) + self.master = master + self.word = word + self.clue = clue + self.value = value + + self._set_fonts() + self._make_content() + self._place_content() + + @classmethod + def _populate(cls, master: WordPane, definitions: Dict[str, str]) -> None: + """Put all base crosswords in the user category into the crossword pane's + scrollable frame. They can have no definitions.json file. + """ + cls.selected_block = IntVar() + cls.selected_block.set(-1) + + for i, (word, clue) in enumerate(definitions.items()): + block: UserWordBlock = cls( + master, + master.preview, + word, + clue, + i, + ) + cls._put_block(block, side="top") + + def _make_content(self) -> None: + self.tb_name = CTkTextbox( + self, + font=self.BOLD_TEXT_FONT, + wrap="none", + fg_color=(Colour.Light.SUB, Colour.Dark.SUB), + height=10, + width=165, + ) + self.tb_name.insert(1.0, self.word) + self.tb_name.configure(state="disabled") + self.rb_selector = CTkRadioButton( + self, + text="", + value=self.value, + variable=UserWordBlock.selected_block, + corner_radius=0, + height=50, + command=self._on_selection, + ) + + def _place_content(self) -> None: + self.tb_name.pack(side="left", padx=10, pady=10) + self.rb_selector.pack(side="left", padx=10, pady=10) + + def _on_selection(self) -> None: + if Form._any_nondefault_values(Form.word_forms): + if not GUIHelper.confirm_with_messagebox( + self.master.pane_name, confirm_cword_or_word_add=True + ): + return + + self.master.mode = "edit" + self.master.word = self.word + self.master.l_title.configure( + text=_("Your Words") + " ({})".format(_("Editing")) + ) + self.master.master._toggle_forms("normal", Form.word_forms) + self.master.b_confirm.configure(text=_("Save")) + self.master.master._reset_forms(Form.word_forms) + self.master.master._set_form_defaults( + self.word, + self.clue, + forms=Form.word_forms, + ) + self.master.b_remove.configure(state="normal") diff --git a/crossword_puzzle/utils.py b/crossword_puzzle/utils.py index 5ce40546f..1762e7989 100644 --- a/crossword_puzzle/utils.py +++ b/crossword_puzzle/utils.py @@ -25,7 +25,7 @@ DOC_DATA_PATH, DOC_PATH, DOWN, - KEEP_LANGUAGES_PATTERN, + NONLANGUAGE_PATTERN, LOCALES_PATH, QUALITY_MAP, TEMPLATE_CFG_PATH, @@ -53,20 +53,25 @@ def confirm_with_messagebox( """Provide confirmations to the user with tkinter messageboxes.""" if "delete_cword_or_word" in kwargs: return messagebox.askyesno( - _("Remove"), _("Are you sure you wish to delete this") + f" {args[0]}? " + _("It will be lost forever!") + _("Remove"), _("Are you sure you want to delete this") + f" {args[0]}? " + _("It will be lost forever!") ) if "confirm_cword_or_word_add" in kwargs: return messagebox.askyesno( - _("Add"), _("Are you sure you wish to add/select a") + f" {args[0]}? " + _("Your modified fields will be reset!") + _("Add or select"), _("Are you sure you want to add/select a") + f" {args[0]}? " + _("Your modified fields will be reset!") + ) + + if "exiting_with_nondefault_fields" in kwargs: + return messagebox.askyesno( + _("Back to home"), _("Are you sure you want to go back to the home screen? Your modified fields will be reset!") ) - if "exit_" in kwargs and "restart" in kwargs: + if "exit_" in kwargs and "restart" not in kwargs: return messagebox.askyesno( _("Restart"), _("Are you sure you want to restart the app?") ) - if "exit_" in kwargs and not "restart" in kwargs: + if "exit_" in kwargs and "restart" in kwargs: return messagebox.askyesno( _("Exit"), _( @@ -106,6 +111,26 @@ def show_messagebox(*args, **kwargs) -> None: return messagebox.showerror( _("Error"), _("This quality is already selected.") ) + + if "crossword_exists_err" in kwargs: + return messagebox.showerror( + _("Error"), _("This crossword already exists. Please choose a new name.") + ) + + if "word_exists_err" in kwargs: + return messagebox.showerror( + _("Error"), _("This word already exists. Please choose a new word.") + ) + + if "wrote_cword" in kwargs: + return messagebox.showinfo( + _("Info"), _("Successfully added/updated crossword!") + ) + + if "wrote_word" in kwargs: + return messagebox.showinfo( + _("Info"), _("Successfully added/updated word!") + ) if "first_time_browser" in kwargs: return messagebox.showinfo( @@ -663,7 +688,7 @@ def _format_definitions( # ``randomly_sampled_definitions``` (the words) and capitalise its values # (the clues/definitions) formatted_definitions = { - sub(KEEP_LANGUAGES_PATTERN, "", k).upper(): v + sub(NONLANGUAGE_PATTERN, "", k).upper(): v for k, v in randomly_sampled_definitions.items() } diff --git a/setup.py b/setup.py index 1a0a2412d..a02974b93 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,8 @@ "Pillow", "regex", "platformdirs", - "pathvalidate" + "pathvalidate", + "CTkToolTip", ], entry_points={ "gui_scripts": [