Skip to content

xpuz.pages.editor ¤

GUI page for editing and making crosswords and their words. Only allows for the creation and editing of new crosswords, not pre-installed ones.

CrosswordPane ¤

CrosswordPane(container: CTkFrame, master: EditorPage)

Bases: CTkFrame, Addons

The left half of EditorPage. Contains a preview of all the user's crosswords, providing the ability to edit them or add new ones.

Source code in src/xpuz/pages/editor.py
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def __init__(self, container: CTkFrame, master: EditorPage) -> None:
    super().__init__(
        container,
        fg_color=(Colour.Light.MAIN, Colour.Dark.MAIN),
        width=master._height * 0.5,
        height=master._width * 0.85,
        corner_radius=0,
    )
    self.pane_name: str = "crossword"
    self.mode: str = ""  # "edit" or "add"
    self.master = master
    # Updated by ``UserCrosswordBlock``
    self.crossword_block: Union[None, UserCrosswordBlock] = None
    self.difficulty = ""
    self.grid(row=0, column=0, sticky="nsew", padx=(0, 1))
    # This is done not to remove any blocks, but remove the empty info label
    # from the UserCrosswordBlock.blocks array if it was there when the user
    # exited ``EditorPage`` (in the same instance)
    UserCrosswordBlock._set_all(UserCrosswordBlock._remove_block)

    self._set_fonts()
    self._make_content()
    self._place_content()
    self._toggle_forms("disabled", Form.crossword_forms)

_add ¤

_add() -> None

Default all forms to empty, and allow the user to define the data for a new crossword.

Source code in src/xpuz/pages/editor.py
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
def _add(self) -> None:
    """Default all forms to empty, and allow the user to define the data for
    a new crossword.
    """
    if Form._any_nondefault_values(Form.crossword_forms):
        if not GUIHelper.confirm_with_messagebox(
            self.pane_name, confirm_cword_or_word_add=True
        ):
            return

    intvar = UserCrosswordBlock.selected_block
    if intvar:
        intvar.set(-1)  # Remove existing crossword selection

    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") + " [↵]",
    )
    Form.crossword_forms[0].focus()
    self._toggle_forms("normal", Form.crossword_forms)
    self.master._reset_forms(Form.crossword_forms, set_invalid=True)
    # Default the difficulty to Easy
    self.opts_difficulty.set(self.difficulties[0])
    self.difficulty = DIFFICULTIES[0]
    self.master._set_form_defaults("", "", forms=Form.crossword_forms)
    self.focus_force()

_export ¤

_export() -> None

Export all of the user's crosswords into a json file.

Source code in src/xpuz/pages/editor.py
659
660
661
662
663
664
def _export(self) -> None:
    """Export all of the user's crosswords into a json file."""
    if isinstance(UserCrosswordBlock.blocks[0], CTkLabel):
        return GUIHelper.show_messagebox(no_crosswords_to_export_err=True)

    Export(UserCrosswordBlock.blocks).start()

_get_cword_dirname ¤

_get_cword_dirname(name: str) -> str

Return the final name of the updated/new crossword's directory, appending a hyphen (if the user specified name doesn't end in one) and the casefolded difficulty.

Source code in src/xpuz/pages/editor.py
753
754
755
756
757
758
759
760
761
762
763
764
765
766
def _get_cword_dirname(self, name: str) -> str:
    """Return the final name of the updated/new crossword's directory,
    appending a hyphen (if the user specified name doesn't end in one) and
    the casefolded difficulty.
    """
    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

_import ¤

_import() -> None

Allow the user to add new crosswords from a JSON file that was exported through xpuz.

Source code in src/xpuz/pages/editor.py
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
def _import(self) -> None:
    """Allow the user to add new crosswords from a JSON file that was
    exported through xpuz.
    """
    if Form._any_nondefault_values(Form.crossword_forms):
        if not GUIHelper.confirm_with_messagebox(
            importing_with_nondefault_fields=True
        ):
            return None
    imp = Import(self, self.master.fp)
    imp.start()

    self._reset()
    self.b_add.configure(state="normal")
    UserCrosswordBlock._populate(self, imp.imported_crossword_fullnames)
    self.master.word_pane._reset()
    for block in UserCrosswordBlock.blocks:
        if hasattr(block, "_flash"):
            block._flash()

_remove ¤

_remove() -> None

Remove a crossword from the OS file system.

Source code in src/xpuz/pages/editor.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
def _remove(self) -> None:
    """Remove a crossword from the OS file system."""
    if not GUIHelper.confirm_with_messagebox(  # Provide confirmation
        self.pane_name, delete_cword_or_word=True
    ):
        return

    fp = self.crossword_block.cwrapper.toplevel
    rmtree(fp)

    self._reset()
    self.b_add.configure(state="normal")
    UserCrosswordBlock._populate(self)  # Regenerate new crossword preview
    self.master.word_pane._reset()

_reset ¤

_reset() -> None

Revert all the states of the widgets in CrosswordPane to their default values.

Source code in src/xpuz/pages/editor.py
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
def _reset(self) -> None:
    """Revert all the states of the widgets in ``CrosswordPane`` to their
    default values.
    """
    UserCrosswordBlock._set_all(UserCrosswordBlock._remove_block)
    self.l_title.configure(text=_("Your Crosswords"))
    self.b_explorer.configure(state="disabled")
    self.b_explorer.configure(image=self.explorer_img_states[1])
    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()

_toggle_forms ¤

_toggle_forms(state: str, forms: List[Form]) -> None

An extension to the method in EditorPage of the same name to configure the state of the difficulty optionmenu.

Source code in src/xpuz/pages/editor.py
697
698
699
700
701
702
703
def _toggle_forms(self, state: str, forms: List[Form]) -> None:
    """An extension to the method in ``EditorPage`` of the same name to
    configure the state of the difficulty optionmenu.
    """
    self.master._toggle_forms(state, forms)
    self.l_difficulty.configure(state=state)
    self.opts_difficulty.configure(state=state)

_update_difficulty ¤

_update_difficulty(difficulty: str) -> None

Find the english version of difficulty and update self.difficulty.

Source code in src/xpuz/pages/editor.py
686
687
688
689
690
691
692
693
694
695
def _update_difficulty(self, difficulty: str) -> None:
    """Find the english version of ``difficulty`` and update
    ``self.difficulty``.
    """
    self.difficulty = _get_english_string(
        DIFFICULTIES, self.difficulties, difficulty
    )
    Form.crossword_forms[0]._update_confirm_button(
        self.pane_name, self.b_confirm
    )

_write ¤

_write() -> None

Update/write a crossword based on form data.

Source code in src/xpuz/pages/editor.py
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
def _write(self) -> None:
    """Update/write a crossword based on form data."""
    if not Form._all_valid_values(Form.crossword_forms):
        return
    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":
        # Must make a new info, and not read it from the crossword wrapper
        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)
        # Must also create info and definitions as this is a fresh crossword
        # that doesn't contain them
        self.master._write_data(toplevel, info, "info")
        self.master._write_data(toplevel, {}, "definitions")

    elif self.mode == "edit":
        # Modify existing crossword wrapper info
        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)

        # Info was updated, to write it
        self.master._write_data(toplevel, info, "info")
        # Join toplevel of crossword with new name to rename it
        try:
            rename(toplevel, path.join(prior_level, dir_name))
        except FileExistsError:
            return GUIHelper.show_messagebox(crossword_exists_err=True)

    self.master.word_pane._reset()
    self._reset()
    self.b_add.configure(state="normal")
    UserCrosswordBlock._populate(self)

EditorPage ¤

EditorPage(master: Base)

Bases: CTkFrame, Addons

Base page for editor.py. Consists of some utility methods required only by CrosswordPane and WordPane, as well as the title bar and contains for the aforementioned two classes.

Source code in src/xpuz/pages/editor.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def __init__(self, master: Base) -> None:
    super().__init__(
        Base.base_container,
        fg_color=(Colour.Light.MAIN, Colour.Dark.MAIN),
    )
    self.master = master
    self.master._set_dim(dim=EDITOR_DIM)
    self._set_fonts()
    self.master.update()
    self._width, self._height = (
        self.master.winfo_width(),
        self.master.winfo_height(),
    )
    self.fp: PathLike = self._get_user_category_path()
    self.scaling: float = float(Base.cfg.get("m", "scale"))

    # Reset the form arrays to prevent duplicates when opening ``EditorPage``
    # more than once in a single instance
    Form.crossword_forms = []
    Form.word_forms = []

    self.grid_rowconfigure(0, minsize=self._height * 0.15, weight=1)
    self.grid_rowconfigure(1, minsize=self._height * 0.85, weight=1)
    self.grid_columnconfigure(0, weight=1)

    self.master.bind("<Return>", lambda e: self._handle_enter())

_get_user_category_path ¤

_get_user_category_path() -> PathLike

Find where to access categories from, whether that is in the package or in the system documents.

Source code in src/xpuz/pages/editor.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def _get_user_category_path(self) -> PathLike:
    """Find where to access categories from, whether that is in the package
    or in the system documents.
    """
    fp: PathLike = path.join(BASE_CWORDS_PATH, "user")  # Assume the system
    # documents don't exist
    if _doc_data_routine(
        doc_callback=lambda: mkdir(DOC_CAT_PATH),
        local_callback=lambda: mkdir(fp),
        sublevel=DOC_CAT_PATH,
    ):  # Func returned true, meaning the system documents are accessible,
        # so reassign fp to the document category path
        fp = DOC_CAT_PATH
    _make_category_info_json(fp, "#FFFFFF")
    return fp

_handle_scroll ¤

_handle_scroll(event: Event) -> None

Scroll one of the two scrollable frames depending on the user's cursor position.

Source code in src/xpuz/pages/editor.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def _handle_scroll(self, event: Event) -> None:
    """Scroll one of the two scrollable frames depending on the user's
    cursor position.
    """
    container: CTkFrame = (
        self.crossword_pane.preview
        # User's cursor is on the left half of the screen
        if event.x_root
        - self.master.winfo_rootx()  # Account for window offset
        <= EDITOR_DIM[0] * self.scaling / 2
        # Right half
        else self.word_pane.preview
    )
    scroll_region = container._parent_canvas.cget("scrollregion")
    viewable_height = container._parent_canvas.winfo_height()
    if (
        scroll_region
        and int(scroll_region.split(" ")[3]) > viewable_height
    ):
        container._parent_canvas.yview("scroll", -1 * event.delta, "units")

_reset_forms ¤

_reset_forms(
    forms: List[Form], set_invalid: bool = False
) -> None

Remove all the content from the forms in forms.

Source code in src/xpuz/pages/editor.py
458
459
460
461
462
463
464
465
def _reset_forms(
    self, forms: List[Form], set_invalid: bool = False
) -> None:
    """Remove all the content from the forms in ``forms``."""
    for form in forms:
        form.wipe()
        if set_invalid:  # Set to true when adding a new crossword
            form.set_invalid()

_set_form_defaults ¤

_set_form_defaults(*args, forms: List[Form])

Set the default of each form in forms to the respective value in args. Requires len(args) to be equal to len(forms).

Source code in src/xpuz/pages/editor.py
467
468
469
470
471
472
def _set_form_defaults(self, *args, forms: List[Form]):
    """Set the default of each form in ``forms`` to the respective value
    in ``args``. Requires len(args) to be equal to len(forms).
    """
    for i, form in enumerate(forms):
        form.set_default(args[i])

_toggle_forms ¤

_toggle_forms(state: str, forms: List[Form]) -> None

Enable or disable all the forms in forms.

Source code in src/xpuz/pages/editor.py
451
452
453
454
455
456
def _toggle_forms(self, state: str, forms: List[Form]) -> None:
    """Enable or disable all the forms in ``forms``."""
    for form in forms:
        form.set_state(state)
        form.set_valid()
        form.unfocus()

_write_data ¤

_write_data(toplevel: PathLike, data: Dict, type_: str)

Write data to a crossword's info.json or definitions.json.

Source code in src/xpuz/pages/editor.py
474
475
476
477
478
def _write_data(self, toplevel: PathLike, data: Dict, type_: str):
    """Write ``data`` to a crossword's info.json or definitions.json."""
    file = "info.json" if type_ == "info" else "definitions.json"
    with open(path.join(toplevel, file), "w") as f:
        dump(data, f, indent=4)

unbind_ ¤

unbind_() -> None

Remove bindings which can be detected on different pages.

Source code in src/xpuz/pages/editor.py
391
392
393
394
395
396
def unbind_(self) -> None:
    """Remove bindings which can be detected on different pages."""
    self.crossword_pane.preview.unbind_all("<MouseWheel>")
    for form in [*Form.crossword_forms, *Form.word_forms]:
        form.unbind_()
    self.master.unbind("<Return>")

Form ¤

Form(
    master: CTkFrame,
    name: str,
    validation_func: Callable,
    pane_name: str,
    b_confirm: CTkButton,
    tooltip: str,
    pane_instance: Optional[bool] = None,
)

Bases: Addons

Encapsulation of a CTkEntry, CTkLabel and CTkButton to construct a field to input data pertaining to a crossword's info or its words.

Contains features such as default value detection, text colour modification based on incorrect value, and tooltips to provide constraints for input.

Source code in src/xpuz/pages/editor.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def __init__(
    self,
    master: CTkFrame,
    name: str,
    validation_func: Callable,
    pane_name: str,
    b_confirm: CTkButton,
    tooltip: str,
    pane_instance: Optional[bool] = None,
) -> None:
    self._set_fonts()

    # Have to encapsulate the frame within the instance here. If ``Form``
    # inherits ``CTkFrame``, this causes issues when defining ``__str__``
    # on older python versions.
    self._frame = CTkFrame(
        master, fg_color=(Colour.Light.MAIN, Colour.Dark.MAIN)
    )

    self._label = CTkLabel(
        self._frame,
        text=_(name.title()),
        font=self.BOLD_TEXT_FONT,
        text_color_disabled=(
            Colour.Light.TEXT_DISABLED,
            Colour.Dark.TEXT_DISABLED,
        ),
    )
    self._form = CTkEntry(
        self._frame,
        font=self.TEXT_FONT,
        fg_color=(Colour.Light.SUB, Colour.Dark.SUB),
        bg_color=(Colour.Light.MAIN, Colour.Dark.MAIN),
    )
    self._form._name = name
    self._tooltip = CTkToolTip(
        self._form, message=tooltip, delay=0.2, y_offset=12
    )
    self.b_reset_to_default = CTkButton(
        self._frame,
        text="↺",
        command=lambda: self.put(self.default, is_default=True),
        height=28,
        width=28,
        font=self.TEXT_FONT,
        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))
    self.b_reset_to_default.grid(row=1, column=1, sticky="nw", padx=(5, 0))

    self.master = master
    self.name = name
    self.pane_name = pane_name
    self.is_valid: bool = True
    self.default: str = ""
    self.has_default_value: bool = True
    self.b_confirm = b_confirm
    self.pane_instance = pane_instance
    self._form.bind("<KeyRelease>", lambda e: validation_func(self))

_all_valid_values staticmethod ¤

_all_valid_values(forms: List[Form]) -> bool

Return true if any form in forms contains an invalid value.

Source code in src/xpuz/pages/editor.py
132
133
134
135
@staticmethod
def _all_valid_values(forms: List["Form"]) -> bool:
    """Return true if any form in ``forms`` contains an invalid value."""
    return all(form.is_valid for form in forms)

_any_nondefault_values staticmethod ¤

_any_nondefault_values(forms: List[Form]) -> bool

Return true if any form in forms contains a nondefault value.

Source code in src/xpuz/pages/editor.py
127
128
129
130
@staticmethod
def _any_nondefault_values(forms: List["Form"]) -> bool:
    """Return true if any form in ``forms`` contains a nondefault value."""
    return any(not form.has_default_value for form in forms)

_check_default ¤

_check_default() -> None

Update the reset field button, and check if the confirm button should be updated as well.

Source code in src/xpuz/pages/editor.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def _check_default(self) -> None:
    """Update the reset field button, and check if the confirm button should
    be updated as well.
    """
    if self.default != str(
        self
    ):  # Nondefault value, allow the user to reset
        # it by enabling the reset button
        self.b_reset_to_default.configure(state="normal")
        self.has_default_value = False
    else:
        if self.default == "":  # An empty value is automatically invalid
            self.set_invalid()
        self.b_reset_to_default.configure(state="disabled")
        self.has_default_value = True
    # Since this method was called a result of the user modifying a field,
    # also update the confirm button
    self._update_confirm_button(self.pane_name, self.b_confirm)

_update_confirm_button ¤

_update_confirm_button(
    pane: CTkFrame, b_confirm: CTkButton
) -> None

Enable or disable the confirm button based on the form content.

Source code in src/xpuz/pages/editor.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def _update_confirm_button(
    self, pane: CTkFrame, b_confirm: CTkButton
) -> None:
    """Enable or disable the confirm button based on the form content."""
    forms = (
        Form.crossword_forms if pane == "crossword" else Form.word_forms
    )
    has_selected_block = hasattr(
        getattr(self.pane_instance, "crossword_block", ""), "cwrapper"
    )
    if (
        Form._any_nondefault_values(forms)
        # An instance of the crossword pane has been provided, allowing this
        # method to check if the default difficulty is chosen or not.
        or self.pane_instance
        and has_selected_block
        and self.pane_instance.crossword_block.cwrapper.difficulty
        != self.pane_instance.difficulty
    ) and Form._all_valid_values(forms):
        # There is at least one form that is valid, and all of the forms are
        # valid, so the confirm button can be toggled on
        b_confirm.configure(state="normal")
    else:
        b_confirm.configure(state="disabled")

focus ¤

focus() -> None

Focus self._form. Must be done after a small delay, or else the button press that triggered this method will be handled after it actually sets focus to the form.

Source code in src/xpuz/pages/editor.py
188
189
190
191
192
193
def focus(self) -> None:
    """Focus ``self._form``. Must be done after a small delay, or else the
    button press that triggered this method will be handled after it actually
    sets focus to the form.
    """
    self.master.master.after(5, self._form.focus_set)

grid ¤

grid(*args, **kwargs) -> None

Wrapper method for gridding self._frame.

Source code in src/xpuz/pages/editor.py
155
156
157
def grid(self, *args, **kwargs) -> None:
    """Wrapper method for gridding ``self._frame``."""
    self._frame.grid(*args, **kwargs)

put ¤

put(text: str, is_default: bool = False) -> None

Wipe self._form and insert text.

Source code in src/xpuz/pages/editor.py
199
200
201
202
203
204
205
206
def put(self, text: str, is_default: bool = False) -> None:
    """Wipe ``self._form`` and insert ``text``."""
    self.wipe()
    self._form.insert(0, text)
    if is_default:
        self.b_reset_to_default.configure(state="disabled")
        self.set_valid()  # Most likely already valid.
        self._check_default()

set_default ¤

set_default(text: Union[str, int]) -> None

Update self.default to text, and put it into the entry.

Source code in src/xpuz/pages/editor.py
250
251
252
253
254
255
256
257
def set_default(self, text: Union[str, int]) -> None:
    """Update ``self.default`` to ``text``, and put it into the entry."""
    self.default = str(text)
    self.put(self.default, is_default=True)
    # The value in ``self`` must now be the default value, so prevent the
    # user from resetting to default or pressing the confirm button
    self.b_reset_to_default.configure(state="disabled")
    self.b_confirm.configure(state="disabled")

set_invalid ¤

set_invalid() -> None

Set the text colour of self._form to red, signifying it contains an incorrect value.

Source code in src/xpuz/pages/editor.py
224
225
226
227
228
229
def set_invalid(self) -> None:
    """Set the text colour of ``self._form`` to red, signifying it contains
    an incorrect value.
    """
    self._form.configure(text_color="red")
    self.is_valid = False

set_state ¤

set_state(state: str) -> None

Set the state of self._form and self._label.

Source code in src/xpuz/pages/editor.py
208
209
210
211
212
213
214
215
def set_state(self, state: str) -> None:
    """Set the state of ``self._form`` and ``self._label``."""
    self._label.configure(state=state)
    self._form.configure(state=state)
    if state == "disabled":  # Also hide or show the tooltip
        self._tooltip.hide()
    else:
        self._tooltip.show()

set_valid ¤

set_valid() -> None

Set the text colour of self._form to black or white, depending on the appearance mode, signifying it has a valid value.

Source code in src/xpuz/pages/editor.py
217
218
219
220
221
222
def set_valid(self) -> None:
    """Set the text colour of ``self._form`` to black or white, depending
    on the appearance mode, signifying it has a valid value.
    """
    self._form.configure(text_color=("black", "white"))
    self.is_valid = True

unfocus ¤

unfocus() -> None

Remove focus from the entry by bringing focus to a pane.

Source code in src/xpuz/pages/editor.py
184
185
186
def unfocus(self) -> None:
    """Remove focus from the entry by bringing focus to a pane."""
    self.master.focus_force()

wipe ¤

wipe() -> None

Remove all characters from self._form.

Source code in src/xpuz/pages/editor.py
195
196
197
def wipe(self) -> None:
    """Remove all characters from ``self._form``."""
    self._form.delete(0, "end")

FormParser ¤

_parse_clue staticmethod ¤

_parse_clue(form: Form) -> None

Parse a crossword clue form contents.

Source code in src/xpuz/pages/editor.py
299
300
301
302
303
304
305
306
@staticmethod
def _parse_clue(form: Form) -> None:
    """Parse a crossword clue form contents."""
    if len(form) < 1 or "\\" in str(form):
        form.set_invalid()
    else:
        form.set_valid()
    form._check_default()

_parse_name staticmethod ¤

_parse_name(form: Form) -> None

Parse the crossword name form contents.

Source code in src/xpuz/pages/editor.py
261
262
263
264
265
266
267
268
269
270
271
272
@staticmethod
def _parse_name(form: Form) -> None:
    """Parse the crossword name form contents."""
    try:
        # Detect filename validation errors to prevent OS errors when editing
        if len(form) <= 32 and validate_filename(str(form)) is None:
            form.set_valid()
        else:
            form.set_invalid()
    except ValidationError:
        form.set_invalid()
    form._check_default()

_parse_symbol staticmethod ¤

_parse_symbol(form: Form) -> None

Parse the crossword symbol form contents.

Source code in src/xpuz/pages/editor.py
274
275
276
277
278
279
280
281
@staticmethod
def _parse_symbol(form: Form) -> None:
    """Parse the crossword symbol form contents."""
    if len(form) == 1:
        form.set_valid()
    else:
        form.set_invalid()
    form._check_default()

_parse_word staticmethod ¤

_parse_word(form: Form) -> None

Parse a crossword's word form contents.

Source code in src/xpuz/pages/editor.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
@staticmethod
def _parse_word(form: Form) -> None:
    """Parse a crossword's word form contents."""
    if (
        len(form) < 1
        or len(form) > 32
        or "\\" in str(form)
        or bool(
            search(NONLANGUAGE_PATTERN, str(form))
        )  # Non-lang char present
    ):
        form.set_invalid()
    else:
        form.set_valid()
    form._check_default()

UserCrosswordBlock ¤

UserCrosswordBlock(
    master: CrosswordPane,
    container: CTkScrollableFrame,
    name: str,
    value: int,
    flash: bool = False,
)

Bases: CTkFrame, Addons, BlockUtils

A small frame that displays a crossword's name and provides a radiobutton to select it.

See browser.py for implementations similar to this.

Source code in src/xpuz/pages/editor.py
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
def __init__(
    self,
    master: CrosswordPane,
    container: CTkScrollableFrame,
    name: str,
    value: int,
    flash: bool = False,
):
    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.flash = flash

    # The most useful thing I have decided to code in this project
    self.cwrapper: CrosswordWrapper = CrosswordWrapper(
        "user",
        name,
        language=Base.locale.language,
        value=value,
    )

    self._set_fonts()
    self._make_content()
    self._place_content()

_on_selection ¤

_on_selection() -> None

Apply the information of this block to the crossword forms, and configure the widget states appropriately.

Source code in src/xpuz/pages/editor.py
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
def _on_selection(self) -> None:
    """Apply the information of this block to the crossword forms, and
    configure the widget states appropriately.
    """
    # Currently, there are nondefault values. Ensure the user wants to
    # override these values with a confirmation
    if Form._any_nondefault_values(Form.crossword_forms):
        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.b_explorer.configure(state="normal")
    self.master.b_explorer.configure(
        image=self.master.explorer_img_states[0]
    )
    self.master.difficulty = self.cwrapper.difficulty
    self.master._toggle_forms("normal", Form.crossword_forms)
    self.master.b_confirm.configure(text=_("Save") + " [↵]")
    self.master.master._reset_forms(Form.crossword_forms)
    self.master.master._set_form_defaults(
        self.cwrapper.name,
        chr(int(self.cwrapper.info["symbol"], 16)),
        forms=Form.crossword_forms,
    )
    Form.crossword_forms[0].focus()
    self.master.opts_difficulty.set(self.cwrapper.translated_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
    )

_populate classmethod ¤

_populate(
    master: CrosswordPane,
    flash_these: Optional[List[str]] = None,
) -> None

Put all of the user crosswords into master.preview (CTkScrollableFrame) as user crossword blocks.

Source code in src/xpuz/pages/editor.py
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
@classmethod
def _populate(
    cls, master: CrosswordPane, flash_these: Optional[List[str]] = None
) -> None:
    """Put all of the user crosswords into ``master.preview``
    (CTkScrollableFrame) as user crossword blocks.
    """
    # Ensure the class' IntVar is ready, and remove any existing selection
    cls.selected_block: IntVar = IntVar()
    cls.selected_block.set(-1)

    for i, crossword in enumerate(
        _get_base_crosswords(
            path.join(BASE_CWORDS_PATH, "user"), allow_empty_defs=True
        )
    ):
        block: UserCrosswordBlock = cls(
            master,
            master.preview,
            crossword.name,
            i,
            flash=flash_these and crossword.name in flash_these,
        )
        cls._put_block(block, side="top")

    if len(UserCrosswordBlock.blocks) == 0:
        # Add a label that indicates there are no user crosswords
        l_empty_info = CTkLabel(
            master.preview,
            text=_("Press")
            + ' "+" '
            + _("to add a")
            + "\n"
            + _("new crossword."),
            font=master.ITALIC_TEXT_FONT,
        )
        cls._put_block(l_empty_info, side="top")

UserWordBlock ¤

UserWordBlock(
    master: WordPane,
    container: CTkScrollableFrame,
    word: str,
    clue: str,
    value: int,
)

Bases: CTkFrame, Addons, BlockUtils

A small frame that displays a crossword's word and provides a radiobutton to select it.

See browser.py for implementations similar to this.

Source code in src/xpuz/pages/editor.py
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
def __init__(
    self,
    master: WordPane,
    container: CTkScrollableFrame,
    word: str,
    clue: str,
    value: int,
) -> None:
    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()

_on_selection ¤

_on_selection() -> None

Apply the information of this block to the word forms, and configure the widget states appropriately.

Source code in src/xpuz/pages/editor.py
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
def _on_selection(self) -> None:
    """Apply the information of this block to the word forms, and
    configure the widget states appropriately.
    """
    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"))
    )
    Form.word_forms[0].focus()
    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")

_populate classmethod ¤

_populate(
    master: WordPane, definitions: Dict[str, str]
) -> None

Put all of the words of a user crossword into master.preview (CTkScrollableFrame) as user word blocks.

Source code in src/xpuz/pages/editor.py
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
@classmethod
def _populate(cls, master: WordPane, definitions: Dict[str, str]) -> None:
    """Put all of the words of a user crossword into ``master.preview``
    (CTkScrollableFrame) as user word blocks.
    """
    cls.selected_block = IntVar()
    cls.selected_block.set(-1)

    for i, (word, clue) in enumerate(
        sorted(definitions.items(), key=lambda item: item[0])
    ):
        block: UserWordBlock = cls(
            master,
            master.preview,
            word,
            clue,
            i,
        )
        cls._put_block(block, side="top")

    if len(UserWordBlock.blocks) == 0:
        l_empty_info = CTkLabel(
            master.preview,
            text=_("Press")
            + ' "+" '
            + _("to add a")
            + "\n"
            + _("new word."),
            font=master.ITALIC_TEXT_FONT,
        )
        cls._put_block(l_empty_info, side="top")

WordPane ¤

WordPane(container: CTkFrame, master: EditorPage)

Bases: CTkFrame, Addons

The right half of EditorPage. Contains a preview of all the words present in the currently selected crossword.

Source code in src/xpuz/pages/editor.py
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
def __init__(self, container: CTkFrame, master: EditorPage) -> None:
    super().__init__(
        container,
        fg_color=(Colour.Light.MAIN, Colour.Dark.MAIN),
        width=master._height * 0.5,
        height=master._width * 0.85,
        corner_radius=0,
    )
    self.pane_name = "word"
    self.master = master
    self.word = (
        ""  # The currently selected word, unchanged by editing until
    )
    # the word is saved and reselected
    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)

_add ¤

_add() -> None

Default all forms to empty, and allow the user to define the data for a new/existing word.

Source code in src/xpuz/pages/editor.py
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
def _add(self) -> None:
    """Default all forms to empty, and allow the user to define the data for
    a new/existing word.
    """
    if Form._any_nondefault_values(Form.word_forms):
        if not GUIHelper.confirm_with_messagebox(
            self.pane_name, confirm_cword_or_word_add=True
        ):
            return

    intvar = UserWordBlock.selected_block
    if intvar:
        intvar.set(-1)

    self.mode = "add"
    self.l_title.configure(
        text=_("Your Words") + " ({})".format(_("Adding"))
    )
    Form.word_forms[0].focus()
    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()

_remove ¤

_remove() -> None

Remove a word/clue pair from the crossword's definitions

Source code in src/xpuz/pages/editor.py
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
def _remove(self) -> None:
    """Remove a word/clue pair from the crossword's definitions"""
    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)

_reset ¤

_reset() -> None

Revert all the states of the widgets in WordPane to their default values.

Source code in src/xpuz/pages/editor.py
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
def _reset(self) -> None:
    """Revert all the states of the widgets in ``WordPane`` to their
    default values.
    """
    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")

_write ¤

_write() -> None

Update a word or write a new one to a crossword's definitions.

Source code in src/xpuz/pages/editor.py
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
def _write(self) -> None:
    """Update a word or write a new one to a crossword's definitions."""
    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":
        # For simplicity, the original word is just deleted, and a new pair
        # is created
        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")