Skip to content

Commit 8b891f1

Browse files
committed
Docstrings for MathCATPreferences.py
1 parent faaf98b commit 8b891f1

File tree

1 file changed

+192
-9
lines changed

1 file changed

+192
-9
lines changed

addon/globalPlugins/MathCAT/MathCATPreferences.py

Lines changed: 192 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,19 @@
4242

4343

4444
class UserInterface(MathCATgui.MathCATPreferencesDialog):
45+
"""UI class for the MathCAT Preferences Dialog.
46+
47+
Initializes and manages user preferences, including language, speech, braille,
48+
and navigation settings. Extends MathCATgui.MathCATPreferencesDialog.
49+
"""
4550
def __init__(self, parent):
51+
"""Initialize the preferences dialog.
52+
53+
Sets up the UI, loads preferences, applies defaults and saved settings,
54+
and restores the previous UI state.
55+
56+
:param parent: The parent window for the dialog.
57+
"""
4658
# initialize parent class
4759
MathCATgui.MathCATPreferencesDialog.__init__(self, parent)
4860

@@ -79,16 +91,36 @@ def __init__(self, parent):
7991

8092
@staticmethod
8193
def pathToLanguagesFolder() -> str:
82-
# the user preferences file is stored at: MathCAT\Rules\Languages
94+
r"""Returns the full path to the Languages rules folder.
95+
96+
The language rules are stored in:
97+
MathCAT\Rules\Languages, relative to the location of this script.
98+
99+
:return: Absolute path to the Languages folder as a string.
100+
"""
83101
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "Languages")
84102

85103
@staticmethod
86104
def pathToBrailleFolder() -> str:
87-
# the user preferences file is stored at: MathCAT\Rules\Languages
105+
r"""Returns the full path to the Braille rules folder.
106+
107+
The Braille rules are stored in:
108+
`MathCAT\Rules\Braille`.
109+
110+
:return: Absolute path to the Braille folder as a string.
111+
"""
88112
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "Braille")
89113

90114
@staticmethod
91115
def languagesDict() -> dict[str, str]:
116+
"""Returns a dictionary mapping language codes to their corresponding language names.
117+
118+
This dictionary includes standard language codes, as well as regional variants such as
119+
'en-GB', 'zh-HANT', and others.
120+
121+
:return: A dictionary where the key is the language code (e.g., 'en', 'fr', 'zh-HANS')
122+
and the value is the language name (e.g., 'English', 'Français', 'Chinese, Simplified').
123+
"""
92124
languages = {
93125
"aa": "Afar",
94126
"ab": "Аҧсуа",
@@ -270,6 +302,18 @@ def getRulesFiles(
270302
pathToDir: str,
271303
processSubDirs: Callable[[str, str], list[str]] | None,
272304
) -> list[str]:
305+
"""
306+
Get the rule files from a directory, optionally processing subdirectories.
307+
308+
Searches for files ending with '_Rules.yaml' in the specified directory.
309+
If no rule files are found, attempts to find them inside a corresponding ZIP archive,
310+
including checking any subdirectories inside the ZIP.
311+
312+
:param pathToDir: Path to the directory to search for rule files.
313+
:param processSubDirs: Optional callable to process subdirectories. It should take the subdirectory name
314+
and the language code as arguments, returning a list of rule filenames found in that subdirectory.
315+
:return: A list of rule file names found either directly in the directory or inside the ZIP archive.
316+
"""
273317
language: str = os.path.basename(pathToDir)
274318
ruleFiles: list[str] = [
275319
os.path.basename(file) for file in glob.glob(os.path.join(pathToDir, "*_Rules.yaml"))
@@ -294,6 +338,13 @@ def getRulesFiles(
294338
return ruleFiles
295339

296340
def getLanguages(self) -> None:
341+
"""Populate the language choice dropdown with available languages and their regional variants.
342+
343+
This method scans the language folders and adds entries for each language and its
344+
regional dialects. Language folders use ISO 639-1 codes and regional variants use ISO 3166-1 alpha-2 codes.
345+
346+
It also adds a special "Use Voice's Language (Auto)" option at the top.
347+
"""
297348
def addRegionalLanguages(subDir: str, language: str) -> list[str]:
298349
# the language variants are in folders named using ISO 3166-1 alpha-2
299350
# codes https://en.wikipedia.org/wiki/ISO_3166-2
@@ -334,19 +385,36 @@ def addRegionalLanguages(subDir: str, language: str) -> list[str]:
334385
self._choiceLanguage.Append(language + " (" + language + ")")
335386

336387
def getLanguageCode(self) -> str:
388+
"""Extract the language code from the selected language string in the UI.
389+
390+
The selected language string is expected to contain the language code in parentheses,
391+
for example: "English (en)".
392+
393+
:return: The language code extracted from the selection.
394+
:rtype: str
395+
"""
337396
langSelection: str = self._choiceLanguage.GetStringSelection()
338397
langCode: str = langSelection[langSelection.find("(") + 1 : langSelection.find(")")]
339398
return langCode
340399

341400
def getSpeechStyles(self, thisSpeechStyle: str):
342401
"""Get all the speech styles for the current language.
343-
This sets the SpeechStyles dialog entry"""
402+
This sets the SpeechStyles dialog entry.
403+
404+
:param thisSpeechStyle: The speech style to set or highlight in the dialog.
405+
:return: None
406+
"""
344407
from speech import getCurrentLanguage
345408

346409
def getSpeechStyleFromDirectory(dir: str, lang: str) -> list[str]:
347410
r"""Get the speech styles from any regional dialog, from the main language, dir and if there isn't from the zip file.
348411
The 'lang', if it has a region dialect, is of the form 'en\uk'
349-
The returned list is sorted alphabetically"""
412+
The returned list is sorted alphabetically
413+
414+
:param dir: The directory path to search for speech styles.
415+
:param lang: Language code which may include a regional dialect (e.g., 'en\uk').
416+
:return: A list of speech styles sorted alphabetically.
417+
"""
350418
# start with the regional dialect, then add on any (unique) styles in the main dir
351419
mainLang: str = lang.split("\\")[0] # does the right thing even if there is no regional directory
352420
allStyleFiles: list[str] = []
@@ -407,6 +475,11 @@ def getSpeechStyleFromDirectory(dir: str, lang: str) -> list[str]:
407475
self._choiceSpeechStyle.SetSelection(0)
408476

409477
def getBrailleCodes(self) -> None:
478+
"""Initializes and populates the braille code choice control with available braille codes.
479+
480+
Scans the braille codes folder for valid directories containing rules files, and adds them
481+
to the braille code dropdown in the dialog.
482+
"""
410483
# initialise the braille code list
411484
self._choiceBrailleMathCode.Clear()
412485
# populate the available braille codes in the dialog
@@ -419,7 +492,11 @@ def getBrailleCodes(self) -> None:
419492
self._choiceBrailleMathCode.Append(brailleCode)
420493

421494
def setUIValues(self) -> None:
422-
# set the UI elements to the ones read from the preference file(s)
495+
"""Sets the UI elements based on the values read from the user preferences.
496+
497+
Attempts to match preference values to UI controls; falls back to defaults if values are invalid
498+
or missing.
499+
"""
423500
try:
424501
self._choiceImpairment.SetSelection(
425502
Speech_Impairment.index(userPreferences["Speech"]["Impairment"]),
@@ -512,6 +589,7 @@ def setUIValues(self) -> None:
512589
print("Key not found", err)
513590

514591
def getUIValues(self) -> None:
592+
"""Reads the current values from the UI controls and updates the user preferences accordingly."""
515593
global userPreferences
516594
# read the values from the UI and update the user preferences dictionary
517595
userPreferences["Speech"]["Impairment"] = Speech_Impairment[self._choiceImpairment.GetSelection()]
@@ -556,20 +634,24 @@ def getUIValues(self) -> None:
556634

557635
@staticmethod
558636
def pathToDefaultPreferences() -> str:
637+
"""Returns the full path to the default preferences file."""
559638
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "prefs.yaml")
560639

561640
@staticmethod
562641
def pathToUserPreferencesFolder() -> str:
642+
"""Returns the path to the folder where user preferences are stored."""
563643
# the user preferences file is stored at: C:\Users\<user-name>AppData\Roaming\MathCAT\prefs.yaml
564644
return os.path.join(os.path.expandvars("%APPDATA%"), "MathCAT")
565645

566646
@staticmethod
567647
def pathToUserPreferences() -> str:
648+
"""Returns the full path to the user preferences file."""
568649
# the user preferences file is stored at: C:\Users\<user-name>AppData\Roaming\MathCAT\prefs.yaml
569650
return os.path.join(UserInterface.pathToUserPreferencesFolder(), "prefs.yaml")
570651

571652
@staticmethod
572653
def loadDefaultPreferences() -> None:
654+
"""Loads the default preferences, overwriting any existing user preferences."""
573655
global userPreferences
574656
# load default preferences into the user preferences data structure (overwrites existing)
575657
if os.path.exists(UserInterface.pathToDefaultPreferences()):
@@ -581,6 +663,10 @@ def loadDefaultPreferences() -> None:
581663

582664
@staticmethod
583665
def loadUserPreferences() -> None:
666+
"""Loads user preferences from a file and merges them into the current preferences.
667+
668+
If the user preferences file exists, its values overwrite the defaults.
669+
"""
584670
global userPreferences
585671
# merge user file values into the user preferences data structure
586672
if os.path.exists(UserInterface.pathToUserPreferences()):
@@ -595,6 +681,15 @@ def validate(
595681
validValues: list[str | bool],
596682
defaultValue: str | bool,
597683
) -> None:
684+
"""Validates that a preference value is in a list of valid options or non-empty if no list is given.
685+
686+
If the value is missing or invalid, sets it to the default.
687+
688+
:param key1: The first-level key in the preferences dictionary.
689+
:param key2: The second-level key in the preferences dictionary.
690+
:param validValues: A list of valid values; if empty, any non-empty value is valid.
691+
:param defaultValue: The default value to set if validation fails.
692+
"""
598693
global userPreferences
599694
try:
600695
if validValues == []:
@@ -621,6 +716,15 @@ def validateInt(
621716
validValues: list[int],
622717
defaultValue: int,
623718
) -> None:
719+
"""Validates that an integer preference is within a specified range.
720+
721+
If the value is missing or out of bounds, sets it to the default.
722+
723+
:param key1: The first-level key in the preferences dictionary.
724+
:param key2: The second-level key in the preferences dictionary.
725+
:param validValues: A list with two integers [min, max] representing valid bounds.
726+
:param defaultValue: The default value to set if validation fails.
727+
"""
624728
global userPreferences
625729
try:
626730
# any value between lower and upper bounds is valid
@@ -639,7 +743,11 @@ def validateInt(
639743

640744
@staticmethod
641745
def validateUserPreferences():
642-
# check each user preference value to ensure it is present and valid, set default value if not
746+
"""Validates all user preferences, ensuring each is present and valid.
747+
748+
If a preference is missing or invalid, it is reset to its default value.
749+
Validation covers speech, navigation, and braille settings.
750+
"""
643751
# Speech:
644752
# Impairment: Blindness # LearningDisability, LowVision, Blindness
645753
UserInterface.validate(
@@ -693,6 +801,11 @@ def validateUserPreferences():
693801

694802
@staticmethod
695803
def writeUserPreferences() -> None:
804+
"""Writes the current user preferences to a file and updates special settings.
805+
806+
Sets the language preference through the native library, ensures the preferences
807+
folder exists, and saves the preferences to disk.
808+
"""
696809
# Language is special because it is set elsewhere by SetPreference which overrides the user_prefs -- so set it here
697810
from . import libmathcat_py as libmathcat
698811

@@ -710,6 +823,13 @@ def writeUserPreferences() -> None:
710823
yaml.dump(userPreferences, stream=f, allow_unicode=True)
711824

712825
def onRelativeSpeedChanged(self, event: wx.ScrollEvent) -> None:
826+
"""Handles changes to the relative speed slider and updates speech output.
827+
828+
Adjusts the speech rate based on the slider value and speaks a test phrase
829+
with the updated rate.
830+
831+
:param event: The scroll event triggered by adjusting the relative speed slider.
832+
"""
713833
rate: int = self._sliderRelativeSpeed.GetValue()
714834
# Translators: this is a test string that is spoken. Only translate "the square root of x squared plus y squared"
715835
text: str = _("<prosody rate='XXX%'>the square root of x squared plus y squared</prosody>").replace(
@@ -720,6 +840,13 @@ def onRelativeSpeedChanged(self, event: wx.ScrollEvent) -> None:
720840
speak(convertSSMLTextForNVDA(text))
721841

722842
def onPauseFactorChanged(self, event: wx.ScrollEvent) -> None:
843+
"""Handles changes to the pause factor slider and updates speech output accordingly.
844+
845+
Calculates the pause durations based on the slider value, constructs an SSML string
846+
with adjusted prosody and breaks, and sends it for speech synthesis.
847+
848+
:param event: The scroll event triggered by adjusting the pause factor slider.
849+
"""
723850
rate: int = self._sliderRelativeSpeed.GetValue()
724851
pfSlider = self._sliderPauseFactor.GetValue()
725852
pauseFactor = (
@@ -747,35 +874,91 @@ def onPauseFactorChanged(self, event: wx.ScrollEvent) -> None:
747874
speak(convertSSMLTextForNVDA(text))
748875

749876
def onClickOK(self, event: wx.CommandEvent) -> None:
877+
"""Saves current preferences and closes the dialog.
878+
879+
Retrieves values from the UI, writes them to the preferences, and then closes the window.
880+
881+
:param event: The event triggered by clicking the OK button.
882+
"""
750883
UserInterface.getUIValues(self)
751884
UserInterface.writeUserPreferences()
752885
self.Destroy()
753886

754887
def onClickCancel(self, event: wx.CommandEvent) -> None:
888+
"""Closes the preferences dialog without saving changes.
889+
890+
:param event: The event triggered by clicking the Cancel button.
891+
"""
755892
self.Destroy()
756893

757894
def onClickApply(self, event: wx.CommandEvent) -> None:
895+
"""Applies the current UI settings to the user preferences.
896+
897+
Retrieves values from the UI and writes them to the preferences configuration.
898+
899+
:param event: The event triggered by clicking the Apply button.
900+
"""
758901
UserInterface.getUIValues(self)
759902
UserInterface.writeUserPreferences()
760903

761904
def onClickReset(self, event: wx.CommandEvent) -> None:
905+
"""Resets preferences to their default values.
906+
907+
Loads the default preferences, validates them, and updates the UI accordingly.
908+
909+
:param event: The event triggered by clicking the Reset button.
910+
"""
762911
UserInterface.loadDefaultPreferences()
763912
UserInterface.validateUserPreferences()
764913
UserInterface.setUIValues(self)
765914

766915
def onClickHelp(self, event: wx.CommandEvent) -> None:
916+
"""Opens the MathCAT user guide in the default web browser.
917+
918+
Triggered when the Help button is clicked.
919+
920+
:param event: The event triggered by clicking the Help button.
921+
"""
767922
webbrowser.open("https://nsoiffer.github.io/MathCAT/users.html")
768923

769924
def onListBoxCategories(self, event: wx.CommandEvent) -> None:
770-
# the category changed, now show the appropriate dialogue page
925+
"""Handles category selection changes in the preferences list box.
926+
927+
Updates the displayed panel in the dialog to match the newly selected category.
928+
929+
:param event: The event triggered by selecting a different category.
930+
"""
771931
self._simplebookPanelsCategories.SetSelection(self._listBoxPreferencesTopic.GetSelection())
772932

773933
def onLanguage(self, event: wx.CommandEvent) -> None:
774-
# the language changed, get the SpeechStyles for the new language
934+
"""Handles the event when the user changes the selected language.
935+
936+
Retrieves and updates the available speech styles for the newly selected language
937+
in the preferences dialog.
938+
939+
:param event: The event triggered by changing the language selection.
940+
"""
775941
UserInterface.getSpeechStyles(self, self._choiceSpeechStyle.GetStringSelection())
776942

777943
def mathCATPreferencesDialogOnCharHook(self, event: wx.KeyEvent) -> None:
778-
# designed choice is that Enter is the same as clicking OK, and Escape is the same as clicking Cancel
944+
"""Handles character key events within the MathCAT Preferences dialog.
945+
946+
This method interprets specific key presses to mimic button clicks or
947+
navigate within the preferences dialog:
948+
949+
- Escape: Triggers the Cancel button functionality.
950+
- Enter: Triggers the OK button functionality.
951+
- Ctrl+Tab: Cycles forward through the preference categories.
952+
- Ctrl+Shift+Tab: Cycles backward through the preference categories.
953+
- Tab: Moves focus to the first control in the currently selected category,
954+
if the category list has focus.
955+
- Shift+Tab: Moves focus to the second row of controls,
956+
if the OK button has focus.
957+
958+
If none of these keys are matched, the event is skipped to allow default processing.
959+
960+
:param event: The keyboard event to handle.
961+
"""
779962
keyCode: int = event.GetKeyCode()
780963
if keyCode == wx.WXK_ESCAPE:
781964
UserInterface.onClickCancel(self, event)

0 commit comments

Comments
 (0)