4242
4343
4444class 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