diff --git a/python/box.py b/python/box.py index 0c55a08ebfdfd5338b659d628f76be52dfd64093..0d924d18359e9a4ae6a803e3f4c0a3ae47ed731d 100644 --- a/python/box.py +++ b/python/box.py @@ -2,7 +2,6 @@ from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import QPainterPath from PySide6.QtGui import QFont from PySide6.QtGui import QCursor -from PySide6.QtGui import QAction from PySide6.QtWidgets import QGraphicsItem from PySide6.QtWidgets import QGraphicsSceneContextMenuEvent from PySide6.QtWidgets import QMenu diff --git a/python/code_editor_menu.py b/python/code_editor_settings_menu.py similarity index 58% rename from python/code_editor_menu.py rename to python/code_editor_settings_menu.py index 4859d28d213728b99a94e0bd48e739b75c4688b7..1105ed9d59b3786a3acd0790dce9ceef42cfedb5 100644 --- a/python/code_editor_menu.py +++ b/python/code_editor_settings_menu.py @@ -2,25 +2,27 @@ from PySide6.QtCore import Slot # for signal/slot support from PySide6.QtWidgets import QMenu """ -Provides a code editor menu. +Provides a settings menu for code editor settings. """ -def make_code_editor_menu(preferences_manager, spellcheck_manager): +def make_code_editor_settings_menu(gui_manager): + preferences_manager = gui_manager.preferences_manager + mp_code_column = gui_manager.mp_code_column + spellcheck_dialog = gui_manager.spellcheck_whitelist_dialog_wrapper + @Slot() def _about_to_show(): # split screen - is_using_code_editor_split_screen = preferences_manager \ - .gui_manager.mp_code_column.using_code_editor_split_screen() preferences_manager.action_use_code_editor_split_screen.setChecked( - is_using_code_editor_split_screen) + mp_code_column.is_using_code_editor_split_screen()) - # spellchecker whitelist - spellcheck_manager.action_spellchecker_whitelist.setDisabled( - spellcheck_manager.dialog.isVisible()) + # spellchecker whitelist dialog + spellcheck_dialog.action_open_spellcheck_whitelist_dialog.setDisabled( + spellcheck_dialog.isVisible()) menu = QMenu("&Code editor") menu.addAction(preferences_manager.action_use_spellchecker) - menu.addAction(spellcheck_manager.action_spellchecker_whitelist) + menu.addAction(spellcheck_dialog.action_open_spellcheck_whitelist_dialog) menu.addSeparator() line_mode_menu = QMenu("Line mode", menu) menu.addMenu(line_mode_menu) diff --git a/python/mp_code_edit_menu.py b/python/edit_menu.py similarity index 86% rename from python/mp_code_edit_menu.py rename to python/edit_menu.py index e0a51b8de785a5371b69c9c94b05fad58ae52fa5..8c746659fff115f2809ed895af743ec0bc563032 100644 --- a/python/mp_code_edit_menu.py +++ b/python/edit_menu.py @@ -1,5 +1,4 @@ from PySide6.QtWidgets import QMenu -from PySide6.QtGui import QAction from PySide6.QtCore import Slot """ @@ -9,7 +8,7 @@ Menus for MP code view 1 are used unless view 2 has focus and split-screen is active. """ -class MPCodeEditMenu(QMenu): +class EditMenu(QMenu): def __init__(self, gui_manager): super().__init__("&Edit") self.mp_code_column = gui_manager.mp_code_column @@ -22,7 +21,7 @@ class MPCodeEditMenu(QMenu): def _set_menu_visibility(self): self.clear() if self.mp_code_view_2.hasFocus() \ - and self.mp_code_column.using_code_editor_split_screen(): + and self.mp_code_column.is_using_code_editor_split_screen(): menu = self.mp_code_view_2.mp_code_view_menu() else: menu = self.mp_code_view_1.mp_code_view_menu() diff --git a/python/graph_list_selection_model.py b/python/graph_list_selection_model.py index 292da31321172b10b799cc9f697183d35f3c8249..d3abf15dd863edb5ad2e5c1d57b1128fc2c44b32 100644 --- a/python/graph_list_selection_model.py +++ b/python/graph_list_selection_model.py @@ -1,6 +1,4 @@ from PySide6.QtCore import QObject # for signal/slot support -from PySide6.QtCore import Signal -from PySide6.QtCore import Slot from PySide6.QtCore import Qt from PySide6.QtCore import QModelIndex from PySide6.QtCore import QItemSelectionModel diff --git a/python/graph_list_table_model.py b/python/graph_list_table_model.py index a78fb0a394902356dc56bcf91eba7575dbab0516..6d1c40b98804a50c0ff23ad9e6fb5784410cd710 100644 --- a/python/graph_list_table_model.py +++ b/python/graph_list_table_model.py @@ -1,5 +1,3 @@ -from PySide6.QtCore import Signal # for signal/slot support -from PySide6.QtCore import Slot # for signal/slot support from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex from graph_item import GraphItem from settings import settings diff --git a/python/graph_list_view.py b/python/graph_list_view.py index 2be81077a6150fc868149df057179c04f43dda0c..91432ac8484b20db45e5462c8b47c3d9c0b3cd97 100644 --- a/python/graph_list_view.py +++ b/python/graph_list_view.py @@ -1,5 +1,4 @@ from PySide6.QtCore import QObject # for signal/slot support -from PySide6.QtCore import Signal from PySide6.QtCore import Slot from PySide6.QtCore import Qt from PySide6.QtCore import QModelIndex diff --git a/python/gui_manager.py b/python/gui_manager.py index 83b85e3c400a3a9366dc25bd03ebd6498df3c3f9..7b5bcd6e66b726b389567f202d05c372cc96d29b 100644 --- a/python/gui_manager.py +++ b/python/gui_manager.py @@ -35,8 +35,8 @@ from graph_list_selection_model import GraphListSelectionModel from mp_code_manager import MPCodeManager from mp_code_view import MPCodeView from mp_code_change_tracker import MPCodeChangeTracker -from mp_code_edit_menu import MPCodeEditMenu -from code_editor_menu import make_code_editor_menu +from edit_menu import EditMenu +from code_editor_settings_menu import make_code_editor_settings_menu from gry_manager import GryManager from settings_themes_menu import SettingsThemesMenu from socket_client_endpoint_menu import SocketClientEndpointMenu @@ -52,7 +52,8 @@ from preferences import preferences from preferences_manager import PreferencesManager from settings import settings from settings_manager import SettingsManager -from spellcheck_manager import SpellcheckManager +from spellcheck_whitelist_manager import SpellcheckWhitelistManager +from spellcheck_whitelist_dialog_wrapper import SpellcheckWhitelistDialogWrapper from session_persistence import SessionPersistence from session_snapshots import SessionSnapshots from session_snapshots_dialog_wrapper import snapshot_files @@ -112,7 +113,12 @@ class GUIManager(QObject): self.session_snapshots = SessionSnapshots(self) # spellcheck manager - self.spellcheck_manager = SpellcheckManager(self.w) + self.spellcheck_whitelist_manager = SpellcheckWhitelistManager() + + # spellcheck whitelist dialog wrapper + self.spellcheck_whitelist_dialog_wrapper = \ + SpellcheckWhitelistDialogWrapper(self.w, + self.spellcheck_whitelist_manager) # the graph list table model self.graph_list_table_model = GraphListTableModel() @@ -187,21 +193,21 @@ class GUIManager(QObject): self.response_compile_mp_code) # the MP Code manager self.mp_code_manager = MPCodeManager( - self.preferences_manager.signal_preferences_changed, - self.settings_manager.signal_settings_changed, - self.spellcheck_manager.signal_spellcheck_changed, - self.statusbar) + self.preferences_manager.signal_preferences_changed, + self.settings_manager.signal_settings_changed, + self.spellcheck_whitelist_manager.signal_spellcheck_changed, + self.statusbar) # the MP Code change tracker self.mp_code_change_tracker = MPCodeChangeTracker(self) # persistent dialogs self.search_mp_files_dialog_wrapper = SearchMPFilesDialogWrapper( - self.w, - self.preferences_manager.signal_preferences_changed, - self.settings_manager.signal_settings_changed, - self.spellcheck_manager.signal_spellcheck_changed, - self.open_mp_code) + self.w, + self.preferences_manager.signal_preferences_changed, + self.settings_manager.signal_settings_changed, + self.spellcheck_whitelist_manager.signal_spellcheck_changed, + self.open_mp_code) self.model_statistics_dialog = ModelStatisticsDialog( self.w, self.graphs_manager, self.mp_code_manager.signal_mp_code_loaded, @@ -210,13 +216,15 @@ class GUIManager(QObject): # the two MP Code editor view windows self.mp_code_view_1 = MPCodeView(self.mp_code_manager, self.preferences_manager, - self.spellcheck_manager, + self.spellcheck_whitelist_manager, + self.spellcheck_whitelist_dialog_wrapper, self.search_mp_files_dialog_wrapper.action_search_mp_files, self.application, 1) self.mp_code_view_2 = MPCodeView(self.mp_code_manager, self.preferences_manager, - self.spellcheck_manager, + self.spellcheck_whitelist_manager, + self.spellcheck_whitelist_dialog_wrapper, self.search_mp_files_dialog_wrapper.action_search_mp_files, self.application, 2) @@ -519,7 +527,7 @@ class GUIManager(QObject): self.file_menu.addAction(self.action_exit) # menu | edit - self.mp_code_edit_menu = menubar.addMenu(MPCodeEditMenu(self)) + self.edit_menu = menubar.addMenu(EditMenu(self)) # menu | actions actions_menu = menubar.addMenu('&Actions') @@ -554,9 +562,8 @@ class GUIManager(QObject): self.settings_menu.addMenu(self.settings_themes_menu) # menu | settings | code editor preferences - self._code_editor_menu = make_code_editor_menu( - self.preferences_manager, self.spellcheck_manager) - self.settings_menu.addMenu(self._code_editor_menu) + self._code_editor_settings_menu = make_code_editor_settings_menu(self) + self.settings_menu.addMenu(self._code_editor_settings_menu) # menu | settings | graph pane preferences self.settings_menu.addMenu(self.graph_pane_menu) diff --git a/python/main_graph_scene.py b/python/main_graph_scene.py index 7710e0a453969be52b25cbf2dd57f2d963cc437d..dac43b0fd45328977931d190cb21ac54fc4fb297 100644 --- a/python/main_graph_scene.py +++ b/python/main_graph_scene.py @@ -2,7 +2,6 @@ import math from PySide6.QtCore import QRectF, Qt -from PySide6.QtCore import Signal from PySide6.QtCore import Slot from PySide6.QtCore import QObject from PySide6.QtCore import QModelIndex diff --git a/python/mp_code_column.py b/python/mp_code_column.py index e8c4b2417990f6af87b38a8b389743e97369cbd3..f64100684170335330504749aa0ba3022bd923ff 100644 --- a/python/mp_code_column.py +++ b/python/mp_code_column.py @@ -44,6 +44,6 @@ class MPCodeColumn(QWidget): b = 0 self.splitter.setSizes((a, b, sizes[2])) - def using_code_editor_split_screen(self): + def is_using_code_editor_split_screen(self): return bool(self.splitter.sizes()[1]) diff --git a/python/mp_code_syntax_highlighter.py b/python/mp_code_syntax_highlighter.py index 29afb74152e7faa224194ff01a5b30198a2dba38..8617cb3e190c37dc4fa2e580fe2ee7427ea70cf5 100644 --- a/python/mp_code_syntax_highlighter.py +++ b/python/mp_code_syntax_highlighter.py @@ -10,7 +10,7 @@ from mp_code_expressions import ATOMIC_NAME_EXPRESSION, \ OPERATOR_EXPRESSION, NUMBER_EXPRESSION, \ VARIABLE_EXPRESSION, QUOTED_TEXT_EXPRESSION from mp_code_event_dict import mp_code_event_dict -from spellcheck import unknown_words +from spellcheck import unknown_words_regex """ Optimization: @@ -87,11 +87,7 @@ class MPCodeSyntaxHighlighter(QSyntaxHighlighter): # create the regex for misspelled words if preferences["use_spellchecker"]: - unknown_word_set = unknown_words(mp_code_text) - misspell_text = '|'.join(unknown_word_set) - self.misspell_expression = QRegularExpression( - r"(?<=[^a-zA-Z0-9']|^)(%s)(?=[^a-zA-Z0-9']|$)"%misspell_text, - options=QRegularExpression.CaseInsensitiveOption) + self.misspell_expression = unknown_words_regex(mp_code_text) # perform the rehighlighting super().rehighlight() diff --git a/python/mp_code_view.py b/python/mp_code_view.py index 3d87dc6be406ec4dfc2fb3d509c64d62732cd395..bb6452d3b6f30b1d26e40926b3e8f305bbe9a525 100644 --- a/python/mp_code_view.py +++ b/python/mp_code_view.py @@ -3,6 +3,7 @@ # http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html from os.path import join, split +import re from PySide6.QtWidgets import QWidget from PySide6.QtWidgets import QPlainTextEdit from PySide6.QtWidgets import QCompleter @@ -29,6 +30,7 @@ from find_and_replace_dialog_wrapper import FindAndReplaceDialogWrapper from mp_json_io_manager import save_mp_code_file from preferences import preferences from mp_file_dialog import get_save_filename +from spellcheck import is_unknown_word, candidate_words from message_popup import message_popup from mp_logger import log @@ -53,8 +55,10 @@ class MPCodeView(QPlainTextEdit): def _move_cursor_to_top(self): self.moveCursor(QTextCursor.MoveOperation.Start) - def __init__(self, mp_code_manager, preferences_manager, spellcheck_manager, - action_search_mp_files, application, instance_number): + def __init__(self, mp_code_manager, preferences_manager, + spellcheck_whitelist_manager, + spellcheck_whitelist_dialog_wrapper, action_search_mp_files, + application, instance_number): super().__init__() # the application instance to submit remapped Ctrl+Shift+Z redo to @@ -66,7 +70,11 @@ class MPCodeView(QPlainTextEdit): self.setDocument(self.mp_code_manager.document) # the spellcheck manager - self.spellcheck_manager = spellcheck_manager + self.spellcheck_whitelist_manager = spellcheck_whitelist_manager + + # the spellcheck whitelist dialog wrapper + self.spellcheck_whitelist_dialog_wrapper \ + = spellcheck_whitelist_dialog_wrapper # the find and replace dialog wrapper self.find_and_replace_dialog_wrapper \ @@ -166,6 +174,31 @@ class MPCodeView(QPlainTextEdit): # great, exported. log("Selection saved as %s"%mp_code_filename) + # return word to spellcheck else "" if not on a spellcheckable word + def _spellcheckable_word(self): + # the selected word + selected_word = self.textCursor().selectedText() + + # require an alphabetical selection at least 3 characters long + if not re.fullmatch(r'[A-Za-z]{3,}', selected_word): + return "" + + # require alphabetical boundary around selection + anchor = self.textCursor().anchor() + if anchor > 0: + + char_left = self.document().characterAt(anchor - 1) + if re.fullmatch(r'[A-Za-z]', char_left): + return "" + position = self.textCursor().position() + if position < self.document().characterCount() - 1: + char_right = self.document().characterAt(position) + if re.fullmatch(r'[A-Za-z]', char_right): + return "" + + # return the valid spellcheckable word + return selected_word + # standard menu plus custom items # Note: we do not keep this menu because we do not have hooks for # built-in items such as "Copy" to keep their selection state fresh. @@ -181,9 +214,45 @@ class MPCodeView(QPlainTextEdit): self.action_save_selection.setDisabled( not self.textCursor().hasSelection()) menu.addSeparator() - menu.addAction(self.spellcheck_manager.action_spellchecker_whitelist) - self.spellcheck_manager.action_spellchecker_whitelist.setDisabled( - self.spellcheck_manager.dialog.isVisible()) + menu.addAction(self.spellcheck_whitelist_dialog_wrapper + .action_open_spellcheck_whitelist_dialog) + self.spellcheck_whitelist_dialog_wrapper \ + .action_open_spellcheck_whitelist_dialog.setDisabled( + self.spellcheck_whitelist_dialog_wrapper.isVisible()) + + # maybe append candidate words and ability to whitelist to the menu + cursor_word = self._spellcheckable_word() + if cursor_word and is_unknown_word(cursor_word): + unknown_word = cursor_word + candidates = candidate_words(cursor_word) + else: + unknown_word, candidates = None, None + + if unknown_word: + menu.addSeparator() + + # add each candidate + def insert_candidate_function_function(candidate): + @Slot() + def insert_candidate_function(): + self.textCursor().insertText(candidate) + return insert_candidate_function + + for candidate in candidates[:10]: # not too many + action = QAction(candidate, menu) + action.triggered.connect(insert_candidate_function_function( + candidate)) + menu.addAction(action) + + # offer to add to whitelist + def add_to_whitelist(): + self.spellcheck_whitelist_manager.add_user_whitelist_word( + unknown_word) + action = QAction('Add "%s" to whitelist'%unknown_word, menu) + action.setToolTip("Add the selected word to the whitelist") + action.triggered.connect(add_to_whitelist) + menu.addAction(action) + return menu # custom menu at the cursor on the editor screen diff --git a/python/paths_gryphon.py b/python/paths_gryphon.py index f4b609a0ae4f4aba96d84a0a86f3c6950799305c..ed90b5858bb6ee96493da887ce4912f65febd4c6 100644 --- a/python/paths_gryphon.py +++ b/python/paths_gryphon.py @@ -3,13 +3,13 @@ from os.path import expanduser, join # Gryphon paths user_mp_path = join(expanduser("~"), ".gryphon") -USER_PREFERENCES_FILENAME = join(user_mp_path, "preference_settings") -SPELLCHECK_WHITELIST_FILENAME = join(user_mp_path, "spellcheck_whitelist") +USER_PREFERENCES_FILENAME = join(user_mp_path, "preference_settings.json") +SPELLCHECK_WHITELIST_FILENAME = join(user_mp_path, "spellcheck_whitelist.txt") PREVIOUS_SESSION_FILENAME = join(user_mp_path, "previous_session.mp") DEFAULT_SETTINGS_FILENAME = join(user_mp_path, "default_theme_settings.stg") SNAPSHOTS_PATH = join(user_mp_path, "snapshots") -TRACE_GENERATOR_PATH_FILENAME = join(user_mp_path, "trace_generator_path") -EXAMPLES_PATH_FILENAME = join(user_mp_path, "preloaded_examples_path") +TRACE_GENERATOR_PATH_FILENAME = join(user_mp_path, "trace_generator_path.json") +EXAMPLES_PATH_FILENAME = join(user_mp_path, "preloaded_examples_path.json") RIGAL_SCRATCH = join(user_mp_path, "mp_gryphon_rigal_scratch") makedirs(RIGAL_SCRATCH, exist_ok=True) # make requisite .gryphon paths makedirs(SNAPSHOTS_PATH, exist_ok=True) # make requisite .gryphon paths diff --git a/python/preferences_manager.py b/python/preferences_manager.py index 0046a51a03658627ed130a0e041433cfea3aaf12..54e762fd8b6f0fb72080d75dc1fa65fbe9e0f3bc 100644 --- a/python/preferences_manager.py +++ b/python/preferences_manager.py @@ -210,7 +210,7 @@ class PreferencesManager(QObject): # split screen: no, see code_editor_menu self.action_use_code_editor_split_screen.setChecked( self.gui_manager.mp_code_column \ - .using_code_editor_split_screen()) + .is_using_code_editor_split_screen()) # scroll scroll_mode = preferences["scroll_mode"] diff --git a/python/search_mp_files_dialog_wrapper.py b/python/search_mp_files_dialog_wrapper.py index 188de24837fa5e01120be5b837cd56dc46fa51c0..e6ffd1cb9b7624848cf2d4d154ab57b6ecd1b15f 100644 --- a/python/search_mp_files_dialog_wrapper.py +++ b/python/search_mp_files_dialog_wrapper.py @@ -5,7 +5,7 @@ from PySide6.QtCore import Qt from PySide6.QtCore import Slot # for signal/slot support from PySide6.QtCore import QAbstractTableModel, QModelIndex, QSortFilterProxyModel from PySide6.QtGui import QAction -from PySide6.QtGui import QTextDocument, QTextCursor, QIcon +from PySide6.QtGui import QIcon from PySide6.QtGui import QClipboard from PySide6.QtWidgets import QDialog, QComboBox, QAbstractItemView from PySide6.QtWidgets import QTableView diff --git a/python/spellcheck.py b/python/spellcheck.py index e4f8a18c91ea5d99669ae6a4094e3d8e1d1b3dff..7afbe8fb34c9eb52eeeee0cd47452e234b2ac8dc 100644 --- a/python/spellcheck.py +++ b/python/spellcheck.py @@ -1,40 +1,59 @@ from os.path import isfile import re from spellchecker import SpellChecker -from PySide6.QtCore import QFile, QTextStream, QIODevice +from PySide6.QtCore import QFile, QTextStream, QIODevice, QRegularExpression from paths_gryphon import SPELLCHECK_WHITELIST_FILENAME from mp_code_expressions import MP_KEYWORDS, MP_META_SYMBOLS -""" -Note: this code uses Python module pyspellchecker which uses a Levenshtein -Distance algorithm. It is not complete but does fairly well. We remove -all non-ASCII unicode, so words with character accents are not managed. -Whitelist words come from pyspellchecker's dictionary, MP keywords and -metasymbols, Gryphon's whitelist, and the user-defined whitelist. -This code requires the pyspellchecker module. -To install use: pip install pyspellchecker. +""" +spellcheck provides singletone global interfaces. Interfaces: - * unknown_words(mp_code_text) - Returns set of unknown words. - * user_whitelist() - Returns the user's active whitelist text. - * change_user_whitelist(text) - Saves the user's whitelist text and - replaces the user whitelist with the new one. + * set_checker(whitelist_text) - set the spellchecker given the user's text + * user_whitelist() - returns _user_whitelist + * unknown_words(mp_code_text) - returns set of unknown words + * unknown_words_regex(mp_code_text) + * is_unknown_word(word) + * candidate_words(word) - returns list or [] + * deduplicated_user_whitelist() + +Internal data: + * _SPELLCHECK_DICT set by calling _set_checker """ -# strings are immutable so we use dict to preserve this global object -_WHITELIST_DICT = dict() +_SPELLCHECK_DICT = {"whitelist_text": None, "checker": None} def _read_user_whitelist(): if isfile(SPELLCHECK_WHITELIST_FILENAME): + # use established whitelist with open (SPELLCHECK_WHITELIST_FILENAME) as f: return f.read() + + # user does not have a whitelist file yet return "" -def _write_user_whitelist(text): +def _write_user_whitelist(whitelist_text): with open(SPELLCHECK_WHITELIST_FILENAME, "w", encoding='utf-8') as f: - f.write(text) + f.write(whitelist_text) + +def _clean_text(text): + # replace everything that is not a letter, number, or apostrophe + # with a space + text = re.sub(r"[^a-zA-Z0-9']+", " ", text) + # replace text containing numbers with space + text = re.sub(r"[a-zA-Z0-9']*\d{1,}[a-zA-Z0-9']*", " ", text) + # replace short 1 or 2 character long text with space + text = re.sub(r"\b[a-zA-Z]{1,2}\b", " ", text) + return text + +# set the whitelist text and the checker +def _set_checker(whitelist_text): + whitelist_text = _clean_text(whitelist_text) -def _set_spellchecker(whitelist_text): + # set whitelist text + _SPELLCHECK_DICT["whitelist_text"] = whitelist_text + + # set the checker checker = SpellChecker() # add MP keywords and metasymbols @@ -47,38 +66,62 @@ def _set_spellchecker(whitelist_text): checker.word_frequency.load_text(QTextStream(f).readAll()) # add user's whitelist - checker.word_frequency.load_text(whitelist_text) + checker.word_frequency.load_text(_SPELLCHECK_DICT["whitelist_text"]) - _WHITELIST_DICT["spellchecker"] = checker - _WHITELIST_DICT["user_whitelist"] = whitelist_text + _SPELLCHECK_DICT["checker"] = checker -_set_spellchecker(_read_user_whitelist()) +# initialize _SPELLCHECK_DICT +_set_checker(_read_user_whitelist()) -# space-separated words -def user_whitelist(): - return _WHITELIST_DICT["user_whitelist"] +# set the whitelist text and the checker +def set_checker(whitelist_text): -# save user whitelist and replace the spell checker with an updated one -def change_user_whitelist(whitelist_text): - # prepare cleaned sorted text separated by spaces - words = list(set(re.sub(r"[^a-zA-Z ]+", " ", whitelist_text).split())) - words.sort(key=str.lower) - - # save - whitelist_text = ' '.join(words) + # save to file _write_user_whitelist(whitelist_text) - _set_spellchecker(whitelist_text) + + # set the checker + _set_checker(whitelist_text) + +def user_whitelist(): + return _SPELLCHECK_DICT["whitelist_text"] def unknown_words(mp_code_text): + text = _clean_text(mp_code_text) + return _SPELLCHECK_DICT["checker"].unknown(text.split()) + +def unknown_words_regex(mp_code_text): + unknown_word_set = unknown_words(mp_code_text) + unknown_word_text = '|'.join(unknown_word_set) + regex = QRegularExpression( + r"(?<=[^a-zA-Z0-9']|^)(%s)(?=[^a-zA-Z0-9']|$)"%unknown_word_text, + options=QRegularExpression.CaseInsensitiveOption) + return regex + +def is_unknown_word(word): + return _SPELLCHECK_DICT["checker"].unknown([word]) + +def candidate_words(unknown_word): + candidates = _SPELLCHECK_DICT["checker"].candidates(unknown_word) + if not candidates: + candidates = set() + return sorted(candidates) + +# returns the user whitelist sorted and deduplicated +def deduplicated_user_whitelist(): + # prepare a checker + checker = SpellChecker() - # replace everything that is not a letter, number, or apostrophe with space - text = re.sub(r"[^a-zA-Z0-9']+", " ", mp_code_text) + # add MP keywords and metasymbols + checker.word_frequency.load_words("|".split(MP_KEYWORDS)) + checker.word_frequency.load_words("|".split(MP_META_SYMBOLS)) - # replace text containing numbers with space - text = re.sub(r"[a-zA-Z0-9']*\d{1,}[a-zA-Z0-9']*", " ", text) + # add resource whitelist + f = QFile(":/spellcheck_whitelist") + f.open(QIODevice.ReadOnly | QIODevice.Text) + checker.word_frequency.load_text(QTextStream(f).readAll()) - # replace short 1 or 2 character long text with space - text = re.sub(r"\b[a-zA-Z]{1,2}\b", " ", text) + deduplicated_words = list(checker.unknown(user_whitelist().split())) + deduplicated_words.sort(key=str.lower) + return ' '.join(deduplicated_words) - return _WHITELIST_DICT["spellchecker"].unknown(text.split()) diff --git a/python/spellcheck_manager.py b/python/spellcheck_manager.py deleted file mode 100644 index 38536f01f19cb5b46a368722c130e93652a61e05..0000000000000000000000000000000000000000 --- a/python/spellcheck_manager.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Use this for managing the spellchecker and user whitelist. - -Signals: - * signal_spellcheck_changed -""" - -from PySide6.QtCore import QObject, Signal, Slot # for signal/slot support -from PySide6.QtGui import QAction -from spellcheck import change_user_whitelist -from spellcheck_whitelist_dialog_wrapper import SpellcheckWhitelistDialogWrapper - -class SpellcheckManager(QObject): - """ - Signal whitelist change during runtime. Specifically, highlighting - wants this. - """ - - # signal - signal_spellcheck_changed = Signal(name='spellcheckChanged') - - def __init__(self, parent_window): - super().__init__() - - self.parent_window = parent_window - - # action - self.action_spellchecker_whitelist = QAction( - "Spellchecker whitelist...") - self.action_spellchecker_whitelist.setToolTip( - "Set a word whitelist for the code editor spellchecker") - self.action_spellchecker_whitelist.triggered.connect(self._open_dialog) - - # singleton non-modal dialog - self.dialog = SpellcheckWhitelistDialogWrapper(self.parent_window, self) - - def set_user_whitelist(self, text): - change_user_whitelist(text) - self.signal_spellcheck_changed.emit() - - @Slot() - def _open_dialog(self): - self.dialog.show() - diff --git a/python/spellcheck_whitelist_dialog.py b/python/spellcheck_whitelist_dialog.py index 0527006b8879d59ca5b8882b19cbe6de5a04cbae..ef441e07760b165ec4a1b17aa9a5a832b107067f 100644 --- a/python/spellcheck_whitelist_dialog.py +++ b/python/spellcheck_whitelist_dialog.py @@ -25,13 +25,16 @@ class Ui_SpellcheckWhitelistDialog(object): SpellcheckWhitelistDialog.resize(572, 320) self.close_pb = QPushButton(SpellcheckWhitelistDialog) self.close_pb.setObjectName(u"close_pb") - self.close_pb.setGeometry(QRect(260, 290, 83, 25)) + self.close_pb.setGeometry(QRect(320, 290, 83, 25)) self.verticalLayoutWidget = QWidget(SpellcheckWhitelistDialog) self.verticalLayoutWidget.setObjectName(u"verticalLayoutWidget") self.verticalLayoutWidget.setGeometry(QRect(0, 0, 571, 281)) self.layout = QVBoxLayout(self.verticalLayoutWidget) self.layout.setObjectName(u"layout") self.layout.setContentsMargins(0, 0, 0, 0) + self.deduplicate_pb = QPushButton(SpellcheckWhitelistDialog) + self.deduplicate_pb.setObjectName(u"deduplicate_pb") + self.deduplicate_pb.setGeometry(QRect(172, 290, 101, 25)) self.retranslateUi(SpellcheckWhitelistDialog) @@ -41,5 +44,6 @@ class Ui_SpellcheckWhitelistDialog(object): def retranslateUi(self, SpellcheckWhitelistDialog): SpellcheckWhitelistDialog.setWindowTitle(QCoreApplication.translate("SpellcheckWhitelistDialog", u"MP Code Whitelist", None)) self.close_pb.setText(QCoreApplication.translate("SpellcheckWhitelistDialog", u"Close", None)) + self.deduplicate_pb.setText(QCoreApplication.translate("SpellcheckWhitelistDialog", u"Deduplicate", None)) # retranslateUi diff --git a/python/spellcheck_whitelist_dialog.ui b/python/spellcheck_whitelist_dialog.ui index 203b580d6f76edae7ac12e31b95f4261c657d3b7..41c710fd0ecf9ff1dbe857ae095dec9d973113b9 100644 --- a/python/spellcheck_whitelist_dialog.ui +++ b/python/spellcheck_whitelist_dialog.ui @@ -16,7 +16,7 @@ <widget class="QPushButton" name="close_pb"> <property name="geometry"> <rect> - <x>260</x> + <x>320</x> <y>290</y> <width>83</width> <height>25</height> @@ -37,6 +37,19 @@ </property> <layout class="QVBoxLayout" name="layout"/> </widget> + <widget class="QPushButton" name="deduplicate_pb"> + <property name="geometry"> + <rect> + <x>172</x> + <y>290</y> + <width>101</width> + <height>25</height> + </rect> + </property> + <property name="text"> + <string>Deduplicate</string> + </property> + </widget> </widget> <resources/> <connections/> diff --git a/python/spellcheck_whitelist_dialog_wrapper.py b/python/spellcheck_whitelist_dialog_wrapper.py index f6c4c85ceceba22404bc8d9c9ab9f65b7dd14fd3..9043e43768f8b55309c01e8999dfdad4081802d8 100644 --- a/python/spellcheck_whitelist_dialog_wrapper.py +++ b/python/spellcheck_whitelist_dialog_wrapper.py @@ -1,13 +1,17 @@ -# wrapper from https://stackoverflow.com/questions/2398800/linking-a-qtdesigner-ui-file-to-python-pyqt +import re from PySide6.QtCore import Qt -from PySide6.QtCore import Slot # for signal/slot support +from PySide6.QtCore import Signal, Slot # for signal/slot support from PySide6.QtWidgets import QDialog, QPlainTextEdit -from PySide6.QtGui import QKeyEvent +from PySide6.QtGui import QKeyEvent, QFocusEvent, QAction from spellcheck_whitelist_dialog import Ui_SpellcheckWhitelistDialog -from spellcheck import user_whitelist +from spellcheck import user_whitelist, set_checker, user_whitelist, \ + deduplicated_user_whitelist from verbose import verbose -class SpellcheckWhitelistView(QPlainTextEdit): +class SpellcheckWhitelistTextEdit(QPlainTextEdit): + + signal_refresh_whitelist = Signal(name='signalRefreshWhitelist') + def __init__(self): super().__init__() @@ -28,11 +32,21 @@ class SpellcheckWhitelistView(QPlainTextEdit): # keep the text super().keyPressEvent(e) + # refresh on CR + if e.key() in (Qt.Key_Return, Qt.Key_Enter): + self.signal_refresh_whitelist.emit() + + @Slot(QFocusEvent) + def focusOutEvent(self, e): + # refresh on lose keyboard focus + self.signal_refresh_whitelist.emit() + super().focusOutEvent(e) + class SpellcheckWhitelistDialogWrapper(QDialog): - def __init__(self, parent_window, spellcheck_manager): + def __init__(self, parent_window, spellcheck_whitelist_manager): super().__init__(parent_window) - self.spellcheck_manager = spellcheck_manager + self.spellcheck_whitelist_manager = spellcheck_whitelist_manager self.ui = Ui_SpellcheckWhitelistDialog() self.ui.setupUi(self) # we refine this further below self.setFixedSize(self.width(), self.height()) @@ -40,18 +54,40 @@ class SpellcheckWhitelistDialogWrapper(QDialog): self.setAttribute(Qt.WA_MacAlwaysShowToolWindow) self.setWindowTitle("MP Code Spellcheck Whitelist") - # the QPlainTextEdit view - self.view = SpellcheckWhitelistView() - self.ui.layout.addWidget(self.view) - self.document = self.view.document() - self.document.setPlainText(user_whitelist()) + # the QPlainTextEdit editor view + self.whitelist_text_edit = SpellcheckWhitelistTextEdit() + self.ui.layout.addWidget(self.whitelist_text_edit) + self.whitelist_text_edit.setPlainText(user_whitelist()) + + # action + self.action_open_spellcheck_whitelist_dialog = QAction( + "Spellchecker whitelist...") + self.action_open_spellcheck_whitelist_dialog.setToolTip( + "Set a word whitelist for the code editor spellchecker") + self.action_open_spellcheck_whitelist_dialog.triggered.connect( + self.show) - # connect buttons - self.view.textChanged.connect(self._text_changed) + # connect + self.ui.deduplicate_pb.clicked.connect(self._deduplicate_text) self.ui.close_pb.clicked.connect(self.close) # superclass close + self.whitelist_text_edit.signal_refresh_whitelist.connect( + self._accept_text_changed) + spellcheck_whitelist_manager.signal_spellcheck_changed.connect( + self._replace_editor_text) + + @Slot() + def _accept_text_changed(self): + text = self.whitelist_text_edit.toPlainText() + self.spellcheck_whitelist_manager.set_user_whitelist(text) + + @Slot() + def _replace_editor_text(self): + if self.whitelist_text_edit.toPlainText() != user_whitelist(): + # replace text in whitelist text edit view + self.whitelist_text_edit.setPlainText(user_whitelist()) @Slot() - def _text_changed(self): - text = self.document.toPlainText() - self.spellcheck_manager.set_user_whitelist(text) + def _deduplicate_text(self): + text = deduplicated_user_whitelist() + self.spellcheck_whitelist_manager.set_user_whitelist(text) diff --git a/python/spellcheck_whitelist_manager.py b/python/spellcheck_whitelist_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..f2e036c5af499e501e4f92f34fd4b26c8178c13e --- /dev/null +++ b/python/spellcheck_whitelist_manager.py @@ -0,0 +1,47 @@ +from os.path import isfile +from spellchecker import SpellChecker +from PySide6.QtCore import QObject, Signal, Slot # for signal/slot support +from PySide6.QtCore import QFile, QTextStream, QIODevice +from PySide6.QtGui import QTextDocument, QTextCursor +from paths_gryphon import SPELLCHECK_WHITELIST_FILENAME +from mp_code_expressions import MP_KEYWORDS, MP_META_SYMBOLS +#from spellcheck import change_user_whitelist +#from spellcheck_whitelist_dialog_wrapper import SpellcheckWhitelistDialogWrapper + +from spellcheck import set_checker, user_whitelist + + +class SpellcheckWhitelistManager(QObject): + """ + Use this for updating the user whitelist. + + Use spellcheck for singleton access to the checker. + + The spellcheck whitelist dialog should call set_user_whitelist on CR + or on lose focus in order to reset the checker with new whitelist_text. + It should not call set_user_whitelist every time text in the dialog + changes. + + Interfaces: + * set_user_whitelist(whitelist_text) - words separated by space + * add_user_whitelist_word(word) - add word + + Signals: + * signal_spellcheck_changed(whitelist_text) - The spellcheck + whitelist dialog should refresh with new text on this signal. + """ + + # signal + signal_spellcheck_changed = Signal(name='signalSpellcheckChanged') + + def __init__(self): + super().__init__() + + def set_user_whitelist(self, whitelist_text): + set_checker(whitelist_text) + self.signal_spellcheck_changed.emit() + + def add_user_whitelist_word(self, word): + set_checker("%s %s"%(user_whitelist(), word)) + self.signal_spellcheck_changed.emit() +