diff --git a/.github/workflows/testrunner.yml b/.github/workflows/testrunner.yml index 5ab95fc8a..9dc002a91 100644 --- a/.github/workflows/testrunner.yml +++ b/.github/workflows/testrunner.yml @@ -31,7 +31,7 @@ jobs: python-linux-3-10-x: name: python 3 linux - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: python-version: [3.10.x] diff --git a/castervoice/asynch/hmc/h_launch.py b/castervoice/asynch/hmc/h_launch.py index 60fbcf6ec..9a2f1c5b1 100644 --- a/castervoice/asynch/hmc/h_launch.py +++ b/castervoice/asynch/hmc/h_launch.py @@ -52,17 +52,17 @@ def _get_title(hmc_type): def main(): - import PySide2.QtWidgets + from castervoice.lib.qt import QtWidgets, qapp_exec from castervoice.asynch.hmc.homunculus import Homunculus from castervoice.lib.merge.communication import Communicator server_address = (Communicator.LOCALHOST, Communicator().com_registry["hmc"]) # Enabled by default logging causes RPC to malfunction when the GUI runs on # pythonw. Explicitly disable logging for the XML server. server = SimpleXMLRPCServer(server_address, logRequests=False, allow_none=True) - app = PySide2.QtWidgets.QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) window = Homunculus(server, sys.argv) window.show() - exit_code = app.exec_() + exit_code = qapp_exec(app) server.shutdown() sys.exit(exit_code) diff --git a/castervoice/asynch/hmc/homunculus.py b/castervoice/asynch/hmc/homunculus.py index 882558c8c..49484c9c6 100644 --- a/castervoice/asynch/hmc/homunculus.py +++ b/castervoice/asynch/hmc/homunculus.py @@ -4,23 +4,6 @@ import dragonfly -# TODO: Remove this try wrapper when CI server supports Qt -try: - import PySide2.QtCore - from PySide2.QtWidgets import QApplication - from PySide2.QtWidgets import QCheckBox - from PySide2.QtWidgets import QDialog - from PySide2.QtWidgets import QFileDialog - from PySide2.QtWidgets import QFormLayout - from PySide2.QtWidgets import QLabel - from PySide2.QtWidgets import QLineEdit - from PySide2.QtWidgets import QScrollArea - from PySide2.QtWidgets import QTextEdit - from PySide2.QtWidgets import QVBoxLayout - from PySide2.QtWidgets import QWidget -except ImportError: - sys.exit(0) - try: # Style C -- may be imported into Caster, or externally BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0] if BASE_PATH not in sys.path: @@ -28,7 +11,31 @@ finally: from castervoice.lib import settings -RPC_DIR_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) +try: + from castervoice.lib.qt import QtCore, QtWidgets, qt_attr +except ImportError: + sys.exit(0) + +QApplication = QtWidgets.QApplication +QCheckBox = QtWidgets.QCheckBox +QDialog = QtWidgets.QDialog +QFileDialog = QtWidgets.QFileDialog +QFormLayout = QtWidgets.QFormLayout +QLabel = QtWidgets.QLabel +QLineEdit = QtWidgets.QLineEdit +QScrollArea = QtWidgets.QScrollArea +QTextEdit = QtWidgets.QTextEdit +QVBoxLayout = QtWidgets.QVBoxLayout +QWidget = QtWidgets.QWidget + +ALIGN_CENTER = qt_attr(QtCore, ("Qt", "AlignCenter"), ("Qt", "AlignmentFlag", "AlignCenter")) +SHOW_DIRS_ONLY = qt_attr( + QtWidgets, + ("QFileDialog", "ShowDirsOnly"), + ("QFileDialog", "Option", "ShowDirsOnly"), +) + +RPC_DIR_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) class Homunculus(QDialog): @@ -64,7 +71,7 @@ def setup_base_window(self, data=None): self.setWindowTitle(settings.HOMUNCULUS_VERSION) self.data = data.split("|") if data else [0, 0] label = QLabel(" ".join(self.data[0].split(settings.HMC_SEPARATOR))) if data else QLabel("Enter response then say 'complete'") # pylint: disable=no-member - label.setAlignment(PySide2.QtCore.Qt.AlignCenter) + label.setAlignment(ALIGN_CENTER) self.ext_box = QTextEdit() self.mainLayout.addWidget(label) self.mainLayout.addWidget(self.ext_box) @@ -98,7 +105,7 @@ def setup_recording_window(self, history): self.setGeometry(x, y, 640, 480) self.setWindowTitle(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_RECORDING) label = QLabel("Macro Recording Options") - label.setAlignment(PySide2.QtCore.Qt.AlignCenter) + label.setAlignment(ALIGN_CENTER) self.mainLayout.addWidget(label) label = QLabel("Command Words:") self.word_box = QLineEdit() @@ -108,7 +115,7 @@ def setup_recording_window(self, history): self.repeatable = QCheckBox("Make Repeatable") self.mainLayout.addWidget(self.repeatable) label = QLabel("Dictation History") - label.setAlignment(PySide2.QtCore.Qt.AlignCenter) + label.setAlignment(ALIGN_CENTER) self.mainLayout.addWidget(label) self.word_state = [] cb_number = 1 @@ -144,7 +151,7 @@ def check_range_of_boxes(self, details): self.word_state[i].setChecked(True) def ask_directory(self): - result = QFileDialog.getExistingDirectory(self, "Please select directory", os.environ["HOME"], QFileDialog.ShowDirsOnly) + result = QFileDialog.getExistingDirectory(self, "Please select directory", os.environ["HOME"], SHOW_DIRS_ONLY) self.word_box.setText(result) def event(self, event): @@ -214,7 +221,7 @@ def xmlrpc_get_message_confirm(self): def xmlrpc_do_action_directory(self, action, details=None): if action == "dir": - PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(RPC_DIR_EVENT)) + QtCore.QCoreApplication.postEvent(self, QtCore.QEvent(RPC_DIR_EVENT)) def xmlrpc_get_message_directory(self): response = None diff --git a/castervoice/asynch/hud.py b/castervoice/asynch/hud.py index e82d0236f..1129be09d 100644 --- a/castervoice/asynch/hud.py +++ b/castervoice/asynch/hud.py @@ -9,16 +9,8 @@ import signal import sys import threading -import PySide2.QtCore -import PySide2.QtGui import dragonfly from xmlrpc.server import SimpleXMLRPCServer -from PySide2.QtWidgets import QApplication -from PySide2.QtWidgets import QMainWindow -from PySide2.QtWidgets import QTextEdit -from PySide2.QtWidgets import QTreeView -from PySide2.QtWidgets import QVBoxLayout -from PySide2.QtWidgets import QWidget try: # Style C -- may be imported into Caster, or externally BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0] if BASE_PATH not in sys.path: @@ -26,19 +18,38 @@ finally: from castervoice.lib.merge.communication import Communicator from castervoice.lib import settings - -CLEAR_HUD_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) -HIDE_HUD_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) -SHOW_HUD_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) -HIDE_RULES_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) -SHOW_RULES_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) -SEND_COMMAND_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) - - -class RPCEvent(PySide2.QtCore.QEvent): + from castervoice.lib.qt import QtCore, QtGui, QtWidgets, qt_attr, qapp_exec + +QApplication = QtWidgets.QApplication +QMainWindow = QtWidgets.QMainWindow +QTextEdit = QtWidgets.QTextEdit +QTreeView = QtWidgets.QTreeView +QVBoxLayout = QtWidgets.QVBoxLayout +QWidget = QtWidgets.QWidget + +WINDOW_STAYS_ON_TOP_HINT = qt_attr( + QtCore, + ("Qt", "WindowStaysOnTopHint"), + ("Qt", "WindowType", "WindowStaysOnTopHint"), +) +TEXT_CURSOR_END = qt_attr( + QtGui, + ("QTextCursor", "End"), + ("QTextCursor", "MoveOperation", "End"), +) + +CLEAR_HUD_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) +HIDE_HUD_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) +SHOW_HUD_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) +HIDE_RULES_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) +SHOW_RULES_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) +SEND_COMMAND_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) + + +class RPCEvent(QtCore.QEvent): def __init__(self, type, text): - PySide2.QtCore.QEvent.__init__(self, type) + QtCore.QEvent.__init__(self, type) self._text = text @property @@ -52,29 +63,29 @@ class RulesWindow(QWidget): _MARGIN = 30 def __init__(self, text): - QWidget.__init__(self, f=(PySide2.QtCore.Qt.WindowStaysOnTopHint)) + QWidget.__init__(self, f=WINDOW_STAYS_ON_TOP_HINT) x = dragonfly.monitors[0].rectangle.dx - (RulesWindow._WIDTH + RulesWindow._MARGIN) y = 300 dx = RulesWindow._WIDTH dy = dragonfly.monitors[0].rectangle.dy - (y + 2 * RulesWindow._MARGIN) self.setGeometry(x, y, dx, dy) self.setWindowTitle("Active Rules") - rules_tree = PySide2.QtGui.QStandardItemModel() + rules_tree = QtGui.QStandardItemModel() rules_tree.setColumnCount(2) rules_tree.setHorizontalHeaderLabels(['phrase', 'action']) rules_dict = json.loads(text) rules = rules_tree.invisibleRootItem() for g in rules_dict: - gram = PySide2.QtGui.QStandardItem(g["name"]) if len(g["rules"]) > 1 else None + gram = QtGui.QStandardItem(g["name"]) if len(g["rules"]) > 1 else None for r in g["rules"]: - rule = PySide2.QtGui.QStandardItem(r["name"]) + rule = QtGui.QStandardItem(r["name"]) rule.setRowCount(len(r["specs"])) rule.setColumnCount(2) row = 0 for s in r["specs"]: phrase, _, action = s.partition('::') - rule.setChild(row, 0, PySide2.QtGui.QStandardItem(phrase)) - rule.setChild(row, 1, PySide2.QtGui.QStandardItem(action)) + rule.setChild(row, 0, QtGui.QStandardItem(phrase)) + rule.setChild(row, 1, QtGui.QStandardItem(action)) row += 1 if gram is None: rules.appendRow(rule) @@ -84,7 +95,7 @@ def __init__(self, text): rules.appendRow(gram) tree_view = QTreeView(self) tree_view.setModel(rules_tree) - tree_view.setColumnWidth(0, RulesWindow._WIDTH / 2) + tree_view.setColumnWidth(0, RulesWindow._WIDTH // 2) layout = QVBoxLayout() layout.addWidget(tree_view) self.setLayout(layout) @@ -97,7 +108,7 @@ class HUDWindow(QMainWindow): _MARGIN = 30 def __init__(self, server): - QMainWindow.__init__(self, flags=(PySide2.QtCore.Qt.WindowStaysOnTopHint)) + QMainWindow.__init__(self, flags=WINDOW_STAYS_ON_TOP_HINT) x = dragonfly.monitors[0].rectangle.dx - (HUDWindow._WIDTH + HUDWindow._MARGIN) y = HUDWindow._MARGIN dx = HUDWindow._WIDTH @@ -137,7 +148,7 @@ def event(self, event): # self.output.append('
') self.output.append(formatted_text) cursor = self.output.textCursor() - cursor.movePosition(PySide2.QtGui.QTextCursor.End) + cursor.movePosition(TEXT_CURSOR_END) self.output.setTextCursor(cursor) self.output.ensureCursorVisible() self.commands_count += 1 @@ -176,33 +187,33 @@ def setup_xmlrpc_server(self): def xmlrpc_clear(self): - PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(CLEAR_HUD_EVENT)) + QtCore.QCoreApplication.postEvent(self, QtCore.QEvent(CLEAR_HUD_EVENT)) return 0 def xmlrpc_ping(self): return 0 def xmlrpc_hide_hud(self): - PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(HIDE_HUD_EVENT)) + QtCore.QCoreApplication.postEvent(self, QtCore.QEvent(HIDE_HUD_EVENT)) return 0 def xmlrpc_show_hud(self): - PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(SHOW_HUD_EVENT)) + QtCore.QCoreApplication.postEvent(self, QtCore.QEvent(SHOW_HUD_EVENT)) return 0 def xmlrpc_hide_rules(self): - PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(HIDE_RULES_EVENT)) + QtCore.QCoreApplication.postEvent(self, QtCore.QEvent(HIDE_RULES_EVENT)) return 0 def xmlrpc_kill(self): QApplication.quit() def xmlrpc_send(self, text): - PySide2.QtCore.QCoreApplication.postEvent(self, RPCEvent(SEND_COMMAND_EVENT, text)) + QtCore.QCoreApplication.postEvent(self, RPCEvent(SEND_COMMAND_EVENT, text)) return len(text) def xmlrpc_show_rules(self, text): - PySide2.QtCore.QCoreApplication.postEvent(self, RPCEvent(SHOW_RULES_EVENT, text)) + QtCore.QCoreApplication.postEvent(self, RPCEvent(SHOW_RULES_EVENT, text)) return len(text) @@ -225,6 +236,6 @@ def handler(signum, frame): app = QApplication(sys.argv) window = HUDWindow(server) window.show() - exit_code = app.exec_() + exit_code = qapp_exec(app) server.shutdown() sys.exit(exit_code) diff --git a/castervoice/asynch/settingswindow.py b/castervoice/asynch/settingswindow.py index 46cdb943a..0fc76db3f 100644 --- a/castervoice/asynch/settingswindow.py +++ b/castervoice/asynch/settingswindow.py @@ -14,20 +14,21 @@ from castervoice.lib import settings from castervoice.lib.merge.communication import Communicator -from PySide2 import QtCore -from PySide2.QtGui import QPalette -from PySide2.QtWidgets import QApplication -from PySide2.QtWidgets import QDialogButtonBox -from PySide2.QtWidgets import QCheckBox -from PySide2.QtWidgets import QDialog -from PySide2.QtWidgets import QFormLayout -from PySide2.QtWidgets import QGroupBox -from PySide2.QtWidgets import QLabel -from PySide2.QtWidgets import QLineEdit -from PySide2.QtWidgets import QScrollArea -from PySide2.QtWidgets import QTabWidget -from PySide2.QtWidgets import QVBoxLayout -from PySide2.QtWidgets import QWidget +from castervoice.lib.qt import QtCore, QtGui, QtWidgets, qt_attr, qapp_exec + +QPalette = QtGui.QPalette +QApplication = QtWidgets.QApplication +QDialogButtonBox = QtWidgets.QDialogButtonBox +QCheckBox = QtWidgets.QCheckBox +QDialog = QtWidgets.QDialog +QFormLayout = QtWidgets.QFormLayout +QGroupBox = QtWidgets.QGroupBox +QLabel = QtWidgets.QLabel +QLineEdit = QtWidgets.QLineEdit +QScrollArea = QtWidgets.QScrollArea +QTabWidget = QtWidgets.QTabWidget +QVBoxLayout = QtWidgets.QVBoxLayout +QWidget = QtWidgets.QWidget @@ -39,8 +40,21 @@ NUMBER_SETTING = 16 BOOLEAN_SETTING = 32 -CONTROL_KEY = QtCore.Qt.Key_Meta if sys.platform == "darwin" else QtCore.Qt.Key_Control -SHIFT_TAB_KEY = int(QtCore.Qt.Key_Tab) + 1 +KEY_META = qt_attr(QtCore, ("Qt", "Key_Meta"), ("Qt", "Key", "Key_Meta")) +KEY_CONTROL = qt_attr(QtCore, ("Qt", "Key_Control"), ("Qt", "Key", "Key_Control")) +KEY_TAB = qt_attr(QtCore, ("Qt", "Key_Tab"), ("Qt", "Key", "Key_Tab")) + +CONTROL_KEY = int(KEY_META if sys.platform == "darwin" else KEY_CONTROL) +TAB_KEY = int(KEY_TAB) +SHIFT_TAB_KEY = TAB_KEY + 1 + +KEY_RELEASE_EVENT = qt_attr(QtCore, ("QEvent", "KeyRelease"), ("QEvent", "Type", "KeyRelease")) +PALETTE_MID = qt_attr(QtGui, ("QPalette", "Mid"), ("QPalette", "ColorRole", "Mid")) +FORM_GROW_ALL_NON_FIXED = qt_attr( + QtWidgets, + ("QFormLayout", "AllNonFixedFieldsGrow"), + ("QFormLayout", "FieldGrowthPolicy", "AllNonFixedFieldsGrow"), +) RPC_COMPLETE_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) @@ -83,11 +97,11 @@ def __init__(self, server): self.expiration.start() def event(self, event): - if event.type() == QtCore.QEvent.KeyRelease: + if event.type() == KEY_RELEASE_EVENT: if self.modifier == 1: curr = self.tabs.currentIndex() tabs_count = self.tabs.count() - if event.key() == QtCore.Qt.Key_Tab: + if event.key() == TAB_KEY: next = curr + 1 next = 0 if next == tabs_count else next self.tabs.setCurrentIndex(next) @@ -116,7 +130,7 @@ def keyReleaseEvent(self, event): def make_tab(self, title): area = QScrollArea() field = Field(area, title) - area.setBackgroundRole(QPalette.Mid) + area.setBackgroundRole(PALETTE_MID) area.setWidgetResizable(True) area.setWidget(self.add_fields(self, title, field)) self.fields.append(field) @@ -125,7 +139,7 @@ def make_tab(self, title): def add_fields(self, parent, title, field): tab = QWidget(parent) form = QFormLayout() - form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) + form.setFieldGrowthPolicy(FORM_GROW_ALL_NON_FIXED) for label in sorted(settings.SETTINGS[title].keys()): value = settings.SETTINGS[title][label] subfield = Field(None, label) @@ -233,7 +247,7 @@ def main(): app = QApplication(sys.argv) window = SettingsDialog(server) window.show() - exit_code = app.exec_() + exit_code = qapp_exec(app) server.shutdown() sys.exit(exit_code) diff --git a/castervoice/lib/ctrl/mgr/engine_manager.py b/castervoice/lib/ctrl/mgr/engine_manager.py index c95c85631..305a468cb 100644 --- a/castervoice/lib/ctrl/mgr/engine_manager.py +++ b/castervoice/lib/ctrl/mgr/engine_manager.py @@ -1,8 +1,10 @@ -from dragonfly import get_engine, get_current_engine +from dragonfly import get_current_engine from castervoice.lib import printer -if get_engine().name == 'natlink': +try: import natlink +except ImportError: + natlink = None class EngineModesManager(object): @@ -43,7 +45,10 @@ def set_mic_mode(self, mode): if mode in self.mic_modes: self.mic_state = mode if self.engine == 'natlink': - natlink.setMicState(mode) + if natlink is not None: + natlink.setMicState(mode) + else: + printer.out("Caster: natlink is not available on this system") # Overrides DNS/DPI is built in sleep grammar self._exclusive_manager.set_mode(mode, modetype="mic_mode") else: @@ -85,6 +90,9 @@ def set_engine_mode(self, mode=None, state=True): if mode in self.engine_modes: if self.engine == 'natlink': + if natlink is None: + printer.out("Caster: natlink is not available on this system") + return try: natlink.execScript("SetRecognitionMode {}".format( self.engine_modes[mode])) # mode is an integer @@ -117,6 +125,8 @@ def _sync_mode(self): Synchronizes Caster exclusivity modes an with DNS/DPI GUI built-in modes state. """ # TODO: Implement set_engine_mode logic with modes not just mic_state. + if self.engine != 'natlink' or natlink is None: + return caster_mic = self.get_mic_mode() natlink_mic = natlink.getMicState() if caster_mic is None: diff --git a/castervoice/lib/qt.py b/castervoice/lib/qt.py new file mode 100644 index 000000000..f4e735647 --- /dev/null +++ b/castervoice/lib/qt.py @@ -0,0 +1,41 @@ +# pylint: disable=import-error,no-name-in-module + +""" +Minimal PySide2/PySide6 compatibility helpers. +""" + + +try: + from PySide2 import QtCore, QtGui, QtWidgets # type: ignore + QT_API = "PySide2" +except ImportError: # pragma: no cover + from PySide6 import QtCore, QtGui, QtWidgets # type: ignore + QT_API = "PySide6" + + +def qt_attr(root, *paths): + """ + Return the first attribute path that exists on `root`. + + `paths` should be tuples of attribute names, e.g. ("Qt", "Key", "Key_Tab"). + """ + last_error = None + for path in paths: + try: + obj = root + for name in path: + obj = getattr(obj, name) + return obj + except AttributeError as exc: + last_error = exc + if last_error is None: + raise AttributeError("qt_attr() requires at least one path") + raise last_error + + +def qapp_exec(app): + exec_fn = getattr(app, "exec", None) or getattr(app, "exec_", None) + if exec_fn is None: + raise AttributeError("QApplication has no exec/exec_ method") + return exec_fn() + diff --git a/castervoice/rules/core/text_manipulation_rules/text_manipulation_support.py b/castervoice/rules/core/text_manipulation_rules/text_manipulation_support.py index 14de25ff8..293407a71 100644 --- a/castervoice/rules/core/text_manipulation_rules/text_manipulation_support.py +++ b/castervoice/rules/core/text_manipulation_rules/text_manipulation_support.py @@ -25,21 +25,28 @@ def get_start_end_position(text, phrase, direction, occurrence_number, dictation # def get_start_end_position(text, phrase, direction): if dictation_versus_character == "character": pattern = re.escape(phrase) - if dictation_versus_character == "dictation": + elif dictation_versus_character == "dictation": # avoid e.g. matching 'and' in 'land' but allow e.g. matching 'and' in 'hello.and' # for matching purposes use lowercase # PROBLEM: this will not match words in class names like "Class" in "ClassName" # PROBLEM: it's not matching the right one when you have two occurrences of the same word in a row pattern = r'(?:[^A-Za-z]|\A)({})(?:[^A-Za-z]|\Z)'.format(phrase.lower()) # must get group 1 + else: + print( + "dictation_versus_character must be either 'character' or 'dictation' (got '{}')" + .format(dictation_versus_character) + ) + return - if not re.search(pattern, text.lower()): + lowered_text = text.lower() + if not re.search(pattern, lowered_text): # replaced phase not found print("'{}' not found".format(phrase)) return - match_iter = re.finditer(pattern, text.lower()) + match_iter = re.finditer(pattern, lowered_text) if dictation_versus_character == "character": match_index_list = [(m.start(), m.end()) for m in match_iter] - if dictation_versus_character == "dictation": + else: match_index_list = [(m.start(1), m.end(1)) for m in match_iter] # first group if direction == "left": @@ -48,12 +55,15 @@ def get_start_end_position(text, phrase, direction, occurrence_number, dictation except IndexError: print("There aren't that many occurrences of '{}'".format(phrase)) return - if direction == "right": + elif direction == "right": try: match = match_index_list[occurrence_number - 1] # count from the left except IndexError: print("There aren't that many occurrences of '{}'".format(phrase)) return + else: + print("Direction must be either 'left' or 'right' (got '{}')".format(direction)) + return left_index, right_index = match return (left_index, right_index) @@ -315,6 +325,9 @@ def move_until_phrase(direction, before_after, phrase, number_of_lines_to_search before_after = "after" if direction == "right": before_after = "before" + elif before_after not in ("before", "after"): + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) if not selected_text: @@ -329,7 +342,7 @@ def move_until_phrase(direction, before_after, phrase, number_of_lines_to_search return if application == "texstudio": - # Approach 1: Unselect text by pressing left and then right. A little slower but works in Texstudio + # Approach 1: Unselect text by pressing left and then right. A little slower but works in Texstudio Key("left, right").execute() # unselect text if direction == "left": # cursor is at the left side of the previously selected text @@ -337,24 +350,32 @@ def move_until_phrase(direction, before_after, phrase, number_of_lines_to_search selected_text_to_the_left_of_phrase = selected_text[:left_index] multiline_offset_correction = selected_text_to_the_left_of_phrase.count("\r\n") offset = left_index - multiline_offset_correction - if before_after == "after": + elif before_after == "after": selected_text_to_the_left_of_phrase = selected_text[:right_index] multiline_offset_correction = selected_text_to_the_left_of_phrase.count("\r\n") offset = right_index - multiline_offset_correction - Key("right:%d" %offset).execute() + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return + Key("right:%d" % offset).execute() - if direction == "right": + elif direction == "right": # cursor is at the left side of the previously selected text if before_after == "before": selected_text_to_the_right_of_phrase = selected_text[left_index :] - if before_after == "after": + offset_index = left_index + elif before_after == "after": selected_text_to_the_right_of_phrase = selected_text[right_index :] + offset_index = right_index + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") - if before_after == "before": - offset = len(selected_text) - left_index - multiline_offset_correction - if before_after == "after": - offset = len(selected_text) - right_index - multiline_offset_correction - Key("left:%d" %offset).execute() + offset = len(selected_text) - offset_index - multiline_offset_correction + Key("left:%d" % offset).execute() + else: + print("Direction must be either 'left' or 'right' (got '{}')".format(direction)) + return else: # Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio if right_index < round(len(selected_text))/2: @@ -363,20 +384,26 @@ def move_until_phrase(direction, before_after, phrase, number_of_lines_to_search if before_after == "before": offset_correction = selected_text[: left_index].count("\r\n") offset = left_index - offset_correction - if before_after == "after": + elif before_after == "after": offset_correction = selected_text[: right_index].count("\r\n") offset = right_index - offset_correction - Key("right:%d" %offset).execute() + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return + Key("right:%d" % offset).execute() else: # it's faster to approach phrase from the right Key("right").execute() # unselect text and place cursor on the right side of selection if before_after == "before": offset_correction = selected_text[left_index :].count("\r\n") offset = len(selected_text) - left_index - offset_correction - if before_after == "after": + elif before_after == "after": offset_correction = selected_text[right_index :].count("\r\n") offset = len(selected_text) - right_index - offset_correction - Key("left:%d" %offset).execute() + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return + Key("left:%d" % offset).execute() def select_phrase(phrase, direction, number_of_lines_to_search, occurrence_number, dictation_versus_character): @@ -443,6 +470,9 @@ def select_until_phrase(direction, phrase, before_after, number_of_lines_to_sear before_after = "before" if direction == "right": before_after = "after" + elif before_after not in ("before", "after"): + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) if not selected_text: @@ -461,32 +491,39 @@ def select_until_phrase(direction, phrase, before_after, number_of_lines_to_sear if application == "texstudio": text_manipulation_paste(selected_text, application) # yes, this is kind of redundant but it gets the proper pause time if direction == "left": - if before_after == "before": - selected_text_to_the_right_of_phrase = selected_text[left_index :] - multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") - offset = len(selected_text) - left_index - multiline_offset_correction - - if before_after == "after": - selected_text_to_the_right_of_phrase = selected_text[right_index :] - multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") - offset = len(selected_text) - right_index - multiline_offset_correction - - Key("s-left:%d" %offset).execute() - if direction == "right": + if before_after == "before": + offset_index = left_index + elif before_after == "after": + offset_index = right_index + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return + + selected_text_to_the_right_of_phrase = selected_text[offset_index :] + multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") + offset = len(selected_text) - offset_index - multiline_offset_correction + Key("s-left:%d" % offset).execute() + elif direction == "right": multiline_movement_correction = selected_text.count("\r\n") movement_offset = len(selected_text) - multiline_movement_correction if before_after == "before": - multiline_selection_correction = selected_text[: left_index].count("\r\n") - selection_offset = left_index - multiline_movement_correction - if before_after == "after": - multiline_selection_correction = selected_text[: right_index].count("\r\n") - selection_offset = right_index + selection_offset_correction = selected_text[: left_index].count("\r\n") + selection_offset = left_index - selection_offset_correction + elif before_after == "after": + selection_offset_correction = selected_text[: right_index].count("\r\n") + selection_offset = right_index - selection_offset_correction + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return # move cursor to original position - Key("left:%d" %movement_offset).execute() + Key("left:%d" % movement_offset).execute() # select text - Key("s-right:%d" %selection_offset).execute() + Key("s-right:%d" % selection_offset).execute() + else: + print("Direction must be either 'left' or 'right' (got '{}')".format(direction)) + return # Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio else: @@ -495,17 +532,25 @@ def select_until_phrase(direction, phrase, before_after, number_of_lines_to_sear if before_after == "before": multiline_correction = selected_text[left_index :].count("\r\n") offset = len(selected_text) - left_index - multiline_correction - if before_after == "after": + elif before_after == "after": multiline_correction = selected_text[right_index :].count("\r\n") offset = len(selected_text) - right_index - multiline_correction - Key("s-left:%d" %offset).execute() - if direction == "right": + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return + Key("s-left:%d" % offset).execute() + elif direction == "right": Key("left").execute() # unselect text and move to the right side of selection if before_after == "before": multiline_correction = selected_text[: left_index].count("\r\n") offset = left_index - multiline_correction - if before_after == "after": + elif before_after == "after": multiline_correction = selected_text[: right_index].count("\r\n") offset = right_index - multiline_correction - Key("s-right:%d" %offset).execute() - + else: + print("before_after must be either 'before' or 'after' (got '{}')".format(before_after)) + return + Key("s-right:%d" % offset).execute() + else: + print("Direction must be either 'left' or 'right' (got '{}')".format(direction)) + return diff --git a/requirements-mac-linux.txt b/requirements-mac-linux.txt index a58788fc2..ce5d99e20 100644 --- a/requirements-mac-linux.txt +++ b/requirements-mac-linux.txt @@ -1,4 +1,5 @@ -dragonfly2[kaldi]>=0.34.0 +dragonfly2>=0.34.0; platform_system == "Darwin" +dragonfly2[kaldi]>=0.34.0; platform_system == "Linux" pillow>=8.0.0 tomlkit>=0.11.8 future>=0.18.2 @@ -6,6 +7,7 @@ mock>=3.0.5 appdirs>=1.4.3 scandir>=1.10.0 pylint -PySide2>=5.14 +PySide6>=6.5; platform_system == "Darwin" +PySide2>=5.14; platform_system != "Darwin" six g2p_en