From 734d1dbd70c5c5ec8e2d4004a9e513ba4f06a06e Mon Sep 17 00:00:00 2001 From: Dmitri Makarov Date: Mon, 18 Oct 2021 17:19:10 -0700 Subject: [PATCH] Add HUD (#773) --- _caster.py | 40 ++- castervoice/asynch/hmc/h_launch.py | 58 ++-- castervoice/asynch/hmc/hmc_ask_directory.py | 2 +- castervoice/asynch/hmc/hmc_confirm.py | 2 +- castervoice/asynch/hmc/hmc_recording.py | 2 +- castervoice/asynch/hmc/homunculus.py | 297 ++++++++++++++---- castervoice/asynch/hud.py | 230 ++++++++++++++ castervoice/asynch/hud_support.py | 130 ++++++++ castervoice/lib/ctrl/configure_engine.py | 30 +- castervoice/lib/ctrl/dependencies.py | 9 +- castervoice/lib/ctrl/mgr/engine_manager.py | 4 +- castervoice/lib/ctrl/mgr/grammar_manager.py | 8 +- castervoice/lib/ctrl/mgr/rule_details.py | 7 +- .../ccrmerging2/hooks/events/event_types.py | 1 + .../hooks/events/rules_loaded_event.py | 8 + .../hooks/standard_hooks/rules_loaded_hook.py | 18 ++ castervoice/lib/merge/communication.py | 7 +- castervoice/lib/merge/state/actions.py | 12 + castervoice/lib/merge/state/contextoptions.py | 17 + castervoice/lib/rules_collection.py | 33 ++ castervoice/lib/settings.py | 9 +- castervoice/lib/utilities.py | 18 +- .../rules/ccr/recording_rules/history.py | 1 - .../rules/core/utility_rules/caster_rule.py | 19 +- 24 files changed, 814 insertions(+), 148 deletions(-) create mode 100644 castervoice/asynch/hud.py create mode 100644 castervoice/asynch/hud_support.py create mode 100644 castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py create mode 100644 castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py create mode 100644 castervoice/lib/rules_collection.py diff --git a/_caster.py b/_caster.py index bda4b7a0b..2c0ffb18c 100644 --- a/_caster.py +++ b/_caster.py @@ -2,26 +2,26 @@ main Caster module Created on Jun 29, 2014 ''' - +import logging import importlib -from castervoice.lib.ctrl.dependencies import DependencyMan # requires nothing -DependencyMan().initialize() - -from castervoice.lib import settings # requires DependencyMan to be initialized -settings.initialize() +from dragonfly import get_engine, get_current_engine +from castervoice.lib import control +from castervoice.lib import settings +from castervoice.lib import printer +from castervoice.lib.ctrl.configure_engine import EngineConfigEarly, EngineConfigLate +from castervoice.lib.ctrl.dependencies import DependencyMan +from castervoice.lib.ctrl.updatecheck import UpdateChecker +from castervoice.asynch import hud_support -from castervoice.lib.ctrl.updatecheck import UpdateChecker # requires settings/dependencies -UpdateChecker().initialize() +printer.out("@ - Starting {} with `{}` Engine -\n".format(settings.SOFTWARE_NAME, get_engine().name)) -from castervoice.lib.ctrl.configure_engine import EngineConfigEarly, EngineConfigLate +DependencyMan().initialize() # requires nothing +settings.initialize() +UpdateChecker().initialize() # requires settings/dependencies EngineConfigEarly() # requires settings/dependencies -_NEXUS = None - -from castervoice.lib import printer -from castervoice.lib import control -if control.nexus() is None: # Initialize Caster State +if control.nexus() is None: from castervoice.lib.ctrl.mgr.loading.load.content_loader import ContentLoader from castervoice.lib.ctrl.mgr.loading.load.content_request_generator import ContentRequestGenerator from castervoice.lib.ctrl.mgr.loading.load.reload_fn_provider import ReloadFunctionProvider @@ -32,9 +32,17 @@ _content_loader = ContentLoader(_crg, importlib.import_module, _rp.get_reload_fn(), _sma) control.init_nexus(_content_loader) EngineConfigLate() # Requires grammars to be loaded and nexus - + if settings.SETTINGS["sikuli"]["enabled"]: from castervoice.asynch.sikuli import sikuli_controller sikuli_controller.get_instance().bootstrap_start_server_proxy() -printer.out("\n*- Starting " + settings.SOFTWARE_NAME + " -*") +try: + if get_current_engine().name != "text": + hud_support.start_hud() +except ImportError: + pass # HUD is not available + +dh = printer.get_delegating_handler() +dh.register_handler(hud_support.HudPrintMessageHandler()) # After hud starts +printer.out("\n") # Force update to display text diff --git a/castervoice/asynch/hmc/h_launch.py b/castervoice/asynch/hmc/h_launch.py index ecbe6dbbe..e25aa4ff9 100644 --- a/castervoice/asynch/hmc/h_launch.py +++ b/castervoice/asynch/hmc/h_launch.py @@ -1,36 +1,26 @@ -from subprocess import Popen -import sys, os +import os +import subprocess +import sys + +from xmlrpc.server import SimpleXMLRPCServer # pylint: disable=no-name-in-module 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: sys.path.append(BASE_PATH) finally: - from castervoice.asynch.hmc.hmc_ask_directory import HomunculusDirectory - from castervoice.asynch.hmc.hmc_recording import HomunculusRecording - from castervoice.asynch.hmc.hmc_confirm import HomunculusConfirm - from castervoice.asynch.hmc.homunculus import Homunculus from castervoice.lib import settings -''' -To add a new homunculus (pop-up ui window) type: - (1) create the module - (2) and its type and title constants to settings.py - (3) add it to _get_title(), and "if __name__ == '__main__':" in this module - (4) call launch() from this module with its type and any data it needs (data as a single string with no spaces) -''' def launch(hmc_type, data=None): from dragonfly import (WaitWindow, FocusWindow, Key) instructions = _get_instructions(hmc_type) - if data is not None: # and callback!=None: + if data is not None: instructions.append(data) - Popen(instructions) - + subprocess.Popen(instructions) hmc_title = _get_title(hmc_type) WaitWindow(title=hmc_title, timeout=5).execute() FocusWindow(title=hmc_title).execute() - Key("tab").execute() def _get_instructions(hmc_type): @@ -61,17 +51,25 @@ def _get_title(hmc_type): return default +def main(): + # TODO: Remove this try wrapper when CI server supports Qt + try: + import PySide2.QtWidgets + from castervoice.asynch.hmc.homunculus import Homunculus + from castervoice.lib.merge.communication import Communicator + except ImportError: + sys.exit(0) + 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) + window = Homunculus(server, sys.argv) + window.show() + exit_code = app.exec_() + server.shutdown() + sys.exit(exit_code) + + if __name__ == '__main__': - found_word = None - if len(sys.argv) > 2: - found_word = sys.argv[2] - if sys.argv[1] == settings.QTYPE_DEFAULT: - app = Homunculus(sys.argv[1]) - elif sys.argv[1] == settings.QTYPE_RECORDING: - app = HomunculusRecording([settings.QTYPE_RECORDING, found_word]) - elif sys.argv[1] == settings.QTYPE_INSTRUCTIONS: - app = Homunculus(sys.argv[1], sys.argv[2]) - elif sys.argv[1] == settings.QTYPE_DIRECTORY: - app = HomunculusDirectory(sys.argv[1]) - elif sys.argv[1] == settings.QTYPE_CONFIRM: - app = HomunculusConfirm([sys.argv[1], sys.argv[2]]) + main() diff --git a/castervoice/asynch/hmc/hmc_ask_directory.py b/castervoice/asynch/hmc/hmc_ask_directory.py index 29c979a58..8fad0cce5 100644 --- a/castervoice/asynch/hmc/hmc_ask_directory.py +++ b/castervoice/asynch/hmc/hmc_ask_directory.py @@ -15,7 +15,7 @@ class HomunculusDirectory(Homunculus): def __init__(self, params): - Homunculus.__init__(self, params[0]) + Homunculus.__init__(self, params[0], args=None) self.title(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_DIRECTORY) self.geometry("640x50+" + str(int(self.winfo_screenwidth()/2 - 320)) + "+" + diff --git a/castervoice/asynch/hmc/hmc_confirm.py b/castervoice/asynch/hmc/hmc_confirm.py index 7c25da031..18e38ea32 100644 --- a/castervoice/asynch/hmc/hmc_confirm.py +++ b/castervoice/asynch/hmc/hmc_confirm.py @@ -14,7 +14,7 @@ class HomunculusConfirm(Homunculus): def __init__(self, params): - Homunculus.__init__(self, params[0]) + Homunculus.__init__(self, params[0], args=None) self.title(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_CONFIRM) self.geometry("320x50+" + str(int(self.winfo_screenwidth()/2 - 160)) + "+" + diff --git a/castervoice/asynch/hmc/hmc_recording.py b/castervoice/asynch/hmc/hmc_recording.py index 64881947d..5d6dd45ad 100644 --- a/castervoice/asynch/hmc/hmc_recording.py +++ b/castervoice/asynch/hmc/hmc_recording.py @@ -21,7 +21,7 @@ def get_row(self, cut_off=0): def __init__(self, params): self.grid_row = 0 - Homunculus.__init__(self, params[0]) + Homunculus.__init__(self, params[0], args=None) self.title(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_RECORDING) self.geometry("640x480+" + str(int(self.winfo_screenwidth()/2 - 320)) + "+" + diff --git a/castervoice/asynch/hmc/homunculus.py b/castervoice/asynch/hmc/homunculus.py index ba4a1c272..882558c8c 100644 --- a/castervoice/asynch/hmc/homunculus.py +++ b/castervoice/asynch/hmc/homunculus.py @@ -1,11 +1,25 @@ +import os import sys +import threading -from xmlrpc.server import SimpleXMLRPCServer -from tkinter import Label, Text -import tkinter as tk +import dragonfly -import signal, os -from threading import Timer +# 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] @@ -13,80 +27,235 @@ sys.path.append(BASE_PATH) finally: from castervoice.lib import settings - from castervoice.lib.merge.communication import Communicator +RPC_DIR_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) -class Homunculus(tk.Tk): - def __init__(self, htype, data=None): - tk.Tk.__init__(self, baseName="") - self.setup_xmlrpc_server() - self.htype = htype - self.completed = False - self.max_after_completed = 10 - self.title(settings.HOMUNCULUS_VERSION) - self.geometry("300x200+" + str(int(self.winfo_screenwidth()/2 - 150)) + "+" + - str(int(self.winfo_screenheight()/2 - 100))) - self.wm_attributes("-topmost", 1) - self.protocol("WM_DELETE_WINDOW", self.xmlrpc_kill) +class Homunculus(QDialog): - # + def __init__(self, server, args): + QDialog.__init__(self, None) + self.htype = args[1] + self.completed = False + self.server = server + self.setup_xmlrpc_server() + self.mainLayout = QVBoxLayout() + found_word = None + if len(args) > 2: + found_word = args[2] if self.htype == settings.QTYPE_DEFAULT: - Label( - self, text="Enter response then say 'complete'", name="pathlabel").pack() - self.ext_box = Text(self, name="ext_box") - self.ext_box.pack(side=tk.LEFT) - self.data = [0, 0] + self.setup_base_window() elif self.htype == settings.QTYPE_INSTRUCTIONS: - self.data = data.split("|") - Label( - self, - text=" ".join(self.data[0].split(settings.HMC_SEPARATOR)), # pylint: disable=no-member - name="pathlabel").pack() - self.ext_box = Text(self, name="ext_box") - self.ext_box.pack(side=tk.LEFT) - - # start server, tk main loop - def start_server(): - while not self.server_quit: - self.server.handle_request() - - Timer(1, start_server).start() - Timer(0.05, self.start_tk).start() - # backup plan in case for whatever reason Dragon doesn't shut it down: - Timer(300, self.xmlrpc_kill).start() - - def start_tk(self): - self.mainloop() + self.setup_base_window(found_word) + elif self.htype == settings.QTYPE_CONFIRM: + self.setup_confirm_window(found_word) + elif self.htype == settings.QTYPE_DIRECTORY: + self.setup_directory_window() + elif self.htype == settings.QTYPE_RECORDING: + self.setup_recording_window(found_word) + self.setLayout(self.mainLayout) + self.expiration = threading.Timer(300, self.xmlrpc_kill) + self.expiration.start() + + def setup_base_window(self, data=None): + x = dragonfly.monitors[0].rectangle.dx / 2 - 150 + y = dragonfly.monitors[0].rectangle.dy / 2 - 100 + self.setGeometry(x, y, 300, 200) + 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) + self.ext_box = QTextEdit() + self.mainLayout.addWidget(label) + self.mainLayout.addWidget(self.ext_box) + self.setWindowTitle(settings.HOMUNCULUS_VERSION) + + def setup_confirm_window(self, params): + x = dragonfly.monitors[0].rectangle.dx / 2 - 160 + y = dragonfly.monitors[0].rectangle.dy / 2 - 25 + self.setGeometry(x, y, 320, 50) + self.setWindowTitle(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_CONFIRM) + label1 = QLabel("Please confirm: " + " ".join(params.split(settings.HMC_SEPARATOR))) + label2 = QLabel("(say \"confirm\" or \"disconfirm\")") + self.mainLayout.addWidget(label1) + self.mainLayout.addWidget(label2) + + def setup_directory_window(self): + x = dragonfly.monitors[0].rectangle.dx / 2 - 320 + y = dragonfly.monitors[0].rectangle.dy / 2 - 25 + self.setGeometry(x, y, 640, 50) + self.setWindowTitle(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_DIRECTORY) + label = QLabel("Enter directory or say 'browse'") + self.word_box = QLineEdit() + label.setBuddy(self.word_box) + self.mainLayout.addWidget(label) + self.mainLayout.addWidget(self.word_box) + + def setup_recording_window(self, history): + self.grid_row = 0 + x = dragonfly.monitors[0].rectangle.dx / 2 - 320 + y = dragonfly.monitors[0].rectangle.dy / 2 - 240 + 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) + self.mainLayout.addWidget(label) + label = QLabel("Command Words:") + self.word_box = QLineEdit() + label.setBuddy(self.word_box) + self.mainLayout.addWidget(label) + self.mainLayout.addWidget(self.word_box) + self.repeatable = QCheckBox("Make Repeatable") + self.mainLayout.addWidget(self.repeatable) + label = QLabel("Dictation History") + label.setAlignment(PySide2.QtCore.Qt.AlignCenter) + self.mainLayout.addWidget(label) + self.word_state = [] + cb_number = 1 + sentences = history.split("[s]") + sentences.pop() + form = QFormLayout() + for sentence in sentences: + sentence_words = sentence.split("[w]") + sentence_words.pop() + display_sentence = " ".join(sentence_words) + cb = QCheckBox("(" + str(cb_number) + ")") + form.addRow(QLabel(display_sentence), cb) + self.word_state.append(cb) + cb_number += 1 + self.word_state[0].setChecked(True) + self.cb_max = cb_number + area = QScrollArea(self) + area.setWidgetResizable(True) + group = QWidget(area) + group.setLayout(form) + area.setWidget(group) + self.mainLayout.addWidget(area) + + def check_boxes(self, details): + for box_index in details: + if 0 < box_index and box_index < self.cb_max: + self.word_state[box_index - 1].setChecked(True) + + def check_range_of_boxes(self, details): + box_index_from = details[0] - 1 + box_index_to = details[1] + for i in range(max(0, box_index_from), min(box_index_to, self.cb_max - 1)): + self.word_state[i].setChecked(True) + + def ask_directory(self): + result = QFileDialog.getExistingDirectory(self, "Please select directory", os.environ["HOME"], QFileDialog.ShowDirsOnly) + self.word_box.setText(result) + + def event(self, event): + if event.type() == RPC_DIR_EVENT: + self.ask_directory() + return True + return QDialog.event(self, event) + + def reject(self): + self.expiration.cancel() + QApplication.quit() + + ''' + XMLRPC methods + ''' def setup_xmlrpc_server(self): - self.server_quit = 0 - comm = Communicator() - self.server = SimpleXMLRPCServer( - (Communicator.LOCALHOST, comm.com_registry["hmc"]), - logRequests=False, allow_none=True) - self.server.register_function(self.xmlrpc_do_action, "do_action") - self.server.register_function(self.xmlrpc_complete, "complete") - self.server.register_function(self.xmlrpc_get_message, "get_message") self.server.register_function(self.xmlrpc_kill, "kill") + self.server.register_function(self.xmlrpc_complete, "complete") + if self.htype == settings.QTYPE_DEFAULT or self.htype == settings.QTYPE_INSTRUCTIONS: + self.server.register_function(self.xmlrpc_do_action, "do_action") + self.server.register_function(self.xmlrpc_get_message, "get_message") + elif self.htype == settings.QTYPE_CONFIRM: + self.server.register_function(self.xmlrpc_do_action_confirm, "do_action") + self.server.register_function(self.xmlrpc_get_message_confirm, "get_message") + elif self.htype == settings.QTYPE_DIRECTORY: + self.server.register_function(self.xmlrpc_do_action_directory, "do_action") + self.server.register_function(self.xmlrpc_get_message_directory, "get_message") + elif self.htype == settings.QTYPE_RECORDING: + self.server.register_function(self.xmlrpc_do_action_recording, "do_action") + self.server.register_function(self.xmlrpc_get_message_recording, "get_message") + server_thread = threading.Thread(target=self.server.serve_forever) + server_thread.daemon = True + server_thread.start() def xmlrpc_kill(self): - self.server_quit = 1 - self.destroy() - os.kill(os.getpid(), signal.SIGTERM) + self.expiration.cancel() + QApplication.quit() def xmlrpc_complete(self): self.completed = True - self.after(10, self.withdraw) - Timer(self.max_after_completed, self.xmlrpc_kill).start() + threading.Timer(10, self.xmlrpc_kill).start() + + def xmlrpc_do_action(self, action, details=None): + pass def xmlrpc_get_message(self): - '''override this for every new child class''' + response = None if self.completed: - Timer(1, self.xmlrpc_kill).start() - return [self.ext_box.get("1.0", tk.END), self.data] - else: - return None + response = [self.ext_box.toPlainText(), self.data] + threading.Timer(1, self.xmlrpc_kill).start() + return response - def xmlrpc_do_action(self, action, details=None): - '''override''' + def xmlrpc_do_action_confirm(self, action, details=None): + if isinstance(action, bool): + self.completed = True + '''1 is True, 2 is False''' + self.value = 1 if action else 2 + + def xmlrpc_get_message_confirm(self): + response = None + if self.completed: + response = {"mode": "confirm"} + response["confirm"] = self.value + threading.Timer(1, self.xmlrpc_kill).start() + return response + + def xmlrpc_do_action_directory(self, action, details=None): + if action == "dir": + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(RPC_DIR_EVENT)) + + def xmlrpc_get_message_directory(self): + response = None + if self.completed: + response = {"mode": "ask_dir"} + response["path"] = self.word_box.text() + threading.Timer(1, self.xmlrpc_kill).start() + return response + + def xmlrpc_do_action_recording(self, action, details=None): + '''acceptable keys are numbers and w and p''' + if action == "check": + self.check_boxes(details) + elif action == "focus": + if details == "word": + self.word_box.setFocus() + elif action == "check_range": + self.check_range_of_boxes(details) + elif action == "exclude": + box_index = details + if 0 < box_index and box_index < self.cb_max: + self.word_state[box_index - 1].setChecked(False) + elif action == "repeatable": + self.repeatable.setChecked(not self.repeatable.isChecked()) + + def xmlrpc_get_message_recording(self): + response = None + if self.completed: + word = self.word_box.text() + if len(word) > 0: + response = {"mode": "recording"} + response["word"] = word + response["repeatable"] = self.repeatable.isChecked() + selected_indices = [] + index = 0 + for ws in self.word_state: + if ws.isChecked(): + selected_indices.append(index) + index += 1 + response["selected_indices"] = selected_indices + if len(selected_indices) == 0: + response = None + threading.Timer(1, self.xmlrpc_kill).start() + return response diff --git a/castervoice/asynch/hud.py b/castervoice/asynch/hud.py new file mode 100644 index 000000000..e82d0236f --- /dev/null +++ b/castervoice/asynch/hud.py @@ -0,0 +1,230 @@ +#! python +''' +Caster HUD Window module +''' +# pylint: disable=import-error,no-name-in-module +import html +import json +import os +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: + sys.path.append(BASE_PATH) +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): + + def __init__(self, type, text): + PySide2.QtCore.QEvent.__init__(self, type) + self._text = text + + @property + def text(self): + return self._text + + +class RulesWindow(QWidget): + + _WIDTH = 600 + _MARGIN = 30 + + def __init__(self, text): + QWidget.__init__(self, f=(PySide2.QtCore.Qt.WindowStaysOnTopHint)) + 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.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 + for r in g["rules"]: + rule = PySide2.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)) + row += 1 + if gram is None: + rules.appendRow(rule) + else: + gram.appendRow(rule) + if gram: + rules.appendRow(gram) + tree_view = QTreeView(self) + tree_view.setModel(rules_tree) + tree_view.setColumnWidth(0, RulesWindow._WIDTH / 2) + layout = QVBoxLayout() + layout.addWidget(tree_view) + self.setLayout(layout) + + +class HUDWindow(QMainWindow): + + _WIDTH = 300 + _HEIGHT = 200 + _MARGIN = 30 + + def __init__(self, server): + QMainWindow.__init__(self, flags=(PySide2.QtCore.Qt.WindowStaysOnTopHint)) + x = dragonfly.monitors[0].rectangle.dx - (HUDWindow._WIDTH + HUDWindow._MARGIN) + y = HUDWindow._MARGIN + dx = HUDWindow._WIDTH + dy = HUDWindow._HEIGHT + self.server = server + self.setup_xmlrpc_server() + self.setGeometry(x, y, dx, dy) + self.setWindowTitle(settings.HUD_TITLE) + self.output = QTextEdit() + self.output.setReadOnly(True) + self.setCentralWidget(self.output) + self.rules_window = None + self.commands_count = 0 + + def event(self, event): + if event.type() == SHOW_HUD_EVENT: + self.show() + return True + if event.type() == HIDE_HUD_EVENT: + self.hide() + return True + if event.type() == SHOW_RULES_EVENT: + self.rules_window = RulesWindow(event.text) + self.rules_window.show() + return True + if event.type() == HIDE_RULES_EVENT and self.rules_window: + self.rules_window.close() + self.rules_window = None + return True + if event.type() == SEND_COMMAND_EVENT: + escaped_text = html.escape(event.text) + if escaped_text.startswith('$'): + formatted_text = '<{}'.format(escaped_text[1:]) + if self.commands_count == 0: + self.output.setHtml(formatted_text) + else: + # self.output.append('
') + self.output.append(formatted_text) + cursor = self.output.textCursor() + cursor.movePosition(PySide2.QtGui.QTextCursor.End) + self.output.setTextCursor(cursor) + self.output.ensureCursorVisible() + self.commands_count += 1 + if self.commands_count == 50: + self.commands_count = 0 + return True + if escaped_text.startswith('@'): + formatted_text = '>{}'.format(escaped_text[1:]) + elif escaped_text.startswith(''): + formatted_text = '>{}'.format(escaped_text) + else: + formatted_text = escaped_text + self.output.append(formatted_text) + self.output.ensureCursorVisible() + return True + if event.type() == CLEAR_HUD_EVENT: + self.commands_count = 0 + return True + return QMainWindow.event(self, event) + + def closeEvent(self, event): + event.accept() + + def setup_xmlrpc_server(self): + self.server.register_function(self.xmlrpc_clear, "clear_hud") + self.server.register_function(self.xmlrpc_ping, "ping") + self.server.register_function(self.xmlrpc_hide_hud, "hide_hud") + self.server.register_function(self.xmlrpc_hide_rules, "hide_rules") + self.server.register_function(self.xmlrpc_kill, "kill") + self.server.register_function(self.xmlrpc_send, "send") + self.server.register_function(self.xmlrpc_show_hud, "show_hud") + self.server.register_function(self.xmlrpc_show_rules, "show_rules") + server_thread = threading.Thread(target=self.server.serve_forever) + server_thread.daemon = True + server_thread.start() + + + def xmlrpc_clear(self): + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.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)) + return 0 + + def xmlrpc_show_hud(self): + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(SHOW_HUD_EVENT)) + return 0 + + def xmlrpc_hide_rules(self): + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.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)) + return len(text) + + def xmlrpc_show_rules(self, text): + PySide2.QtCore.QCoreApplication.postEvent(self, RPCEvent(SHOW_RULES_EVENT, text)) + return len(text) + + +def handler(signum, frame): + """ + This handler doesn't stop the application when ^C is pressed, + but it prevents exceptions being thrown when later + the application is terminated from GUI. Normally, HUD is started + by the recognition process and can't be killed from shell prompt, + in which case this handler is not needed. + """ + pass + + +if __name__ == "__main__": + signal.signal(signal.SIGINT, handler) + server_address = (Communicator.LOCALHOST, Communicator().com_registry["hud"]) + # allow_none=True means Python constant None will be translated into XML + server = SimpleXMLRPCServer(server_address, logRequests=False, allow_none=True) + app = QApplication(sys.argv) + window = HUDWindow(server) + window.show() + exit_code = app.exec_() + server.shutdown() + sys.exit(exit_code) diff --git a/castervoice/asynch/hud_support.py b/castervoice/asynch/hud_support.py new file mode 100644 index 000000000..167b334e3 --- /dev/null +++ b/castervoice/asynch/hud_support.py @@ -0,0 +1,130 @@ +import sys, subprocess, json + +from dragonfly import CompoundRule, MappingRule, get_current_engine + +from pathlib import Path + +try: # Style C -- may be imported into Caster, or externally + BASE_PATH = str(Path(__file__).resolve().parent.parent) + if BASE_PATH not in sys.path: + sys.path.append(BASE_PATH) +finally: + from castervoice.lib import settings + +from castervoice.lib import printer +from castervoice.lib import control +from castervoice.lib.rules_collection import get_instance + +def start_hud(): + hud = control.nexus().comm.get_com("hud") + try: + hud.ping() + except Exception: + subprocess.Popen([settings.SETTINGS["paths"]["PYTHONW"], + settings.SETTINGS["paths"]["HUD_PATH"]]) + + +def show_hud(): + hud = control.nexus().comm.get_com("hud") + try: + hud.show_hud() + except Exception as e: + printer.out("Unable to show hud. Hud not available. \n{}".format(e)) + + +def hide_hud(): + hud = control.nexus().comm.get_com("hud") + try: + hud.hide_hud() + except Exception as e: + printer.out("Unable to hide hud. Hud not available. \n{}".format(e)) + + +def clear_hud(): + hud = control.nexus().comm.get_com("hud") + try: + hud.clear_hud() + except Exception as e: + printer.out("Unable to clear hud. Hud not available. \n{}".format(e)) + + +def show_rules(): + """ + Get a list of active grammars loaded into the current engine, + including active rules and their attributes. Send the list + to HUD GUI for display. + """ + grammars = [] + engine = get_current_engine() + for grammar in engine.grammars: + if any([r.active for r in grammar.rules]): + rules = [] + for rule in grammar.rules: + if rule.active and not rule.name.startswith('_'): + if isinstance(rule, CompoundRule): + specs = [rule.spec] + elif isinstance(rule, MappingRule): + specs = sorted(["{}::{}".format(x, rule._mapping[x]) for x in rule._mapping]) + else: + specs = [rule.element.gstring()] + rules.append({ + "name": rule.name, + "exported": rule.exported, + "specs": specs + }) + grammars.append({"name": grammar.name, "rules": rules}) + grammars.extend(get_instance().serialize()) + hud = control.nexus().comm.get_com("hud") + try: + hud.show_rules(json.dumps(grammars)) + except Exception as e: + printer.out("Unable to show hud. Hud not available. \n{}".format(e)) + +def hide_rules(): + """ + Instruct HUD to hide the frame with the list of rules. + """ + hud = control.nexus().comm.get_com("hud") + try: + hud.hide_rules() + except Exception as e: + printer.out("Unable to show hud. Hud not available. \n{}".format(e)) + + +class HudPrintMessageHandler(printer.BaseMessageHandler): + """ + Hud message handler which prints formatted messages to the gui Hud. + Add symbols as the 1st character in strings utilizing printer.out + + @ Purple arrow - Bold Text - Important Info + # Red arrow - Plain text - Caster Info + $ Blue arrow - Plain text - Commands/Dictation + """ + + def __init__(self): + super(HudPrintMessageHandler, self).__init__() + self.hud = control.nexus().comm.get_com("hud") + self.exception = False + try: + if get_current_engine().name != "text": + self.hud.ping() # HUD running? + except Exception as e: + self.exception = True + printer.out("Hud not available. \n{}".format(e)) + + def handle_message(self, items): + if self.exception is False: + # The timeout with the hud can interfere with the dragonfly speech recognition loop. + # This appears as a stutter in recognition. + # Exceptions are tracked so this stutter only happens to end user once. + # Make exception if the hud is not available/python 2/text engine + # TODO: handle raising exception gracefully + try: + self.hud.send("\n".join([str(m) for m in items])) + except Exception as e: + # If an exception, print is managed by SimplePrintMessageHandler + self.exception = True + printer.out("Hud not available. \n{}".format(e)) + raise("") # pylint: disable=raising-bad-type + else: + raise("") # pylint: disable=raising-bad-type \ No newline at end of file diff --git a/castervoice/lib/ctrl/configure_engine.py b/castervoice/lib/ctrl/configure_engine.py index 4a3598212..98bd442d0 100644 --- a/castervoice/lib/ctrl/configure_engine.py +++ b/castervoice/lib/ctrl/configure_engine.py @@ -1,17 +1,37 @@ import time -from dragonfly import get_engine, get_current_engine, register_recognition_callback +from dragonfly import get_current_engine, register_recognition_callback, RecognitionObserver +from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager from castervoice.lib import settings +from castervoice.lib import printer + + +class Observer(RecognitionObserver): + def __init__(self): + from castervoice.lib import control + self.mic_mode = None + self._engine_modes_manager = control.nexus().engine_modes_manager + + def on_begin(self): + self.mic_mode = self._engine_modes_manager.get_mic_mode() + + def on_recognition(self, words): + if not self.mic_mode == "sleeping": + printer.out("$ {}".format(" ".join(words))) + + def on_failure(self): + if not self.mic_mode == "sleeping": + printer.out("?!") + class EngineConfigEarly: """ - Initializes engine specific customizations before Nexus initializes. + Initializes engine customizations before Nexus initializes. Grammars are not loaded """ # get_engine used as a workaround for running Natlink inprocess - engine = get_engine().name - def __init__(self): + self.engine = get_current_engine().name self._set_cancel_word() def _set_cancel_word(self): @@ -35,6 +55,8 @@ def __init__(self): self.engine = get_current_engine().name self.sync_timer = None self.sleep_timer = None + Observer().register() + if self.engine != 'natlink': # Other engines besides natlink needs a default mic state for sleep_timer diff --git a/castervoice/lib/ctrl/dependencies.py b/castervoice/lib/ctrl/dependencies.py index 57bcf3b92..37fc06f34 100644 --- a/castervoice/lib/ctrl/dependencies.py +++ b/castervoice/lib/ctrl/dependencies.py @@ -5,6 +5,7 @@ ''' import os, sys, time, pkg_resources from pkg_resources import VersionConflict, DistributionNotFound +from castervoice.lib import printer DARWIN = sys.platform == "darwin" LINUX = sys.platform == "linux" @@ -48,7 +49,7 @@ def dep_missing(): missing_list.append('{0}'.format(dep)) if missing_list: pippackages = (' '.join(map(str, missing_list))) - print("\nCaster: dependencys are missing. Use 'python -m pip install {0}'".format(pippackages)) + printer.out("\nCaster: dependencys are missing. Use 'python -m pip install {0}'".format(pippackages)) time.sleep(10) @@ -69,10 +70,10 @@ def dep_min_version(): except VersionConflict as e: if operator == ">=": if issue_url is not None: - print("\nCaster: Requires {0} v{1} or greater.\nIssue reference: {2}".format(package, version, issue_url)) - print("Update with: 'python -m pip install {} --upgrade' \n".format(package)) + printer.out("\nCaster: Requires {0} v{1} or greater.\nIssue reference: {2}".format(package, version, issue_url)) + printer.out("Update with: 'python -m pip install {} --upgrade' \n".format(package)) if operator == "==": - print("\nCaster: Requires an exact version of {0}.\nIssue reference: {1}".format(package, issue_url)) + printer.out("\nCaster: Requires an exact version of {0}.\nIssue reference: {1}".format(package, issue_url)) print("Install with: 'python -m pip install {}' \n".format(e.req)) diff --git a/castervoice/lib/ctrl/mgr/engine_manager.py b/castervoice/lib/ctrl/mgr/engine_manager.py index 75ebd3600..c95c85631 100644 --- a/castervoice/lib/ctrl/mgr/engine_manager.py +++ b/castervoice/lib/ctrl/mgr/engine_manager.py @@ -1,7 +1,7 @@ -from dragonfly import get_current_engine +from dragonfly import get_engine, get_current_engine from castervoice.lib import printer -if get_current_engine().name == 'natlink': +if get_engine().name == 'natlink': import natlink diff --git a/castervoice/lib/ctrl/mgr/grammar_manager.py b/castervoice/lib/ctrl/mgr/grammar_manager.py index cc38e3d9d..9bc3a3b76 100644 --- a/castervoice/lib/ctrl/mgr/grammar_manager.py +++ b/castervoice/lib/ctrl/mgr/grammar_manager.py @@ -1,4 +1,5 @@ -import os, traceback +import os +import traceback from dragonfly import Grammar @@ -12,6 +13,7 @@ from castervoice.lib.ctrl.mgr.rules_enabled_diff import RulesEnabledDiff from castervoice.lib.merge.ccrmerging2.hooks.events.activation_event import RuleActivationEvent from castervoice.lib.merge.ccrmerging2.hooks.events.on_error_event import OnErrorEvent +from castervoice.lib.merge.ccrmerging2.hooks.events.rules_loaded_event import RulesLoadedEvent from castervoice.lib.merge.ccrmerging2.sorting.config_ruleset_sorter import ConfigBasedRuleSetSorter from castervoice.lib.util.ordered_set import OrderedSet @@ -240,7 +242,7 @@ def _remerge_ccr_rules(self, enabled_rcns): active_rule_class_names = [rcn for rcn in enabled_rcns if rcn in loaded_enabled_rcns] active_mrs = [self._managed_rules[rcn] for rcn in active_rule_class_names] active_ccr_mrs = [mr for mr in active_mrs if mr.get_details().declared_ccrtype is not None] - + self._hooks_runner.execute(RulesLoadedEvent(rules=active_ccr_mrs)) ''' The merge may result in 1 to n+1 rules where n is the number of ccr app rules which are in the active rules list. @@ -302,7 +304,7 @@ def receive(self, file_path_changed): if class_name in self._config.get_enabled_rcns_ordered(): self._delegate_enable_rule(class_name, True) except Exception as error: - printer.out('Grammar Manager: {} - See error message above'.format(error)) + printer.out('Grammar Manager: {} - See error message above'.format(error)) self._hooks_runner.execute(OnErrorEvent()) def _get_invalidation(self, rule_class, details): diff --git a/castervoice/lib/ctrl/mgr/rule_details.py b/castervoice/lib/ctrl/mgr/rule_details.py index 022e2f15c..b6a4e3832 100644 --- a/castervoice/lib/ctrl/mgr/rule_details.py +++ b/castervoice/lib/ctrl/mgr/rule_details.py @@ -37,6 +37,9 @@ def __init__(self, name=None, function_context=None, executable=None, title=None stack = inspect.stack(0) self._filepath = RuleDetails._calculate_filepath_from_frame(stack, 1) + def __str__(self): + return 'ccrtype {}'.format(self.declared_ccrtype if self.declared_ccrtype else '_') + @staticmethod def _calculate_filepath_from_frame(stack, index): try: @@ -46,11 +49,11 @@ def _calculate_filepath_from_frame(stack, index): if filepath.endswith("pyc"): filepath = filepath[:-1] return filepath - except AttributeError as e: + except AttributeError: if not os.path.isfile(frame[1]): pyc = frame[1] + "c" if os.path.isfile(pyc): - printer.out("\n {} \n Caster Detected a stale .pyc file. The stale file has been removed please restart Caster. \n".format(pyc)) + printer.out('\n {}\n Caster removed a stale .pyc file. Please, restart Caster. \n'.format(pyc)) os.remove(pyc) else: traceback.print_exc() diff --git a/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py b/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py index f5b2eef89..b00acb8ba 100644 --- a/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py +++ b/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py @@ -2,3 +2,4 @@ class EventType(object): ACTIVATION = "activation" NODE_CHANGE = "node change" ON_ERROR = "on error" + RULES_LOADED = "rules loaded" diff --git a/castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py b/castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py new file mode 100644 index 000000000..3379accde --- /dev/null +++ b/castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py @@ -0,0 +1,8 @@ +from castervoice.lib.merge.ccrmerging2.hooks.events.base_event import BaseHookEvent +from castervoice.lib.merge.ccrmerging2.hooks.events.event_types import EventType + + +class RulesLoadedEvent(BaseHookEvent): + def __init__(self, rules=None): + super(RulesLoadedEvent, self).__init__(EventType.RULES_LOADED) + self.rules = rules diff --git a/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py b/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py new file mode 100644 index 000000000..35096f12d --- /dev/null +++ b/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py @@ -0,0 +1,18 @@ +import castervoice.lib.rules_collection +from castervoice.lib.merge.ccrmerging2.hooks.base_hook import BaseHook +from castervoice.lib.merge.ccrmerging2.hooks.events.event_types import EventType + + +class RulesLoadedHook(BaseHook): + def __init__(self): + super(RulesLoadedHook, self).__init__(EventType.RULES_LOADED) + + def get_pronunciation(self): + return "rules loaded" + + def run(self, event_data): + castervoice.lib.rules_collection.get_instance().update(event_data.rules) + + +def get_hook(): + return RulesLoadedHook diff --git a/castervoice/lib/merge/communication.py b/castervoice/lib/merge/communication.py index 39664ec13..74f8cf2b3 100644 --- a/castervoice/lib/merge/communication.py +++ b/castervoice/lib/merge/communication.py @@ -6,9 +6,10 @@ class Communicator: def __init__(self): self.coms = {} self.com_registry = { - "hmc": 1338, - "grids": 1339, - "sikuli": 8000 + "hmc": 8337, + "hud": 8338, + "grids": 8339, + "sikuli": 8340 } def get_com(self, name): diff --git a/castervoice/lib/merge/state/actions.py b/castervoice/lib/merge/state/actions.py index 619a60660..a0cb929d6 100644 --- a/castervoice/lib/merge/state/actions.py +++ b/castervoice/lib/merge/state/actions.py @@ -1,3 +1,4 @@ +from functools import reduce from dragonfly import ActionBase from castervoice.lib import control @@ -34,6 +35,9 @@ def set_nexus(self, nexus): def nexus(self): return self._nexus or control.nexus() + def __str__(self): + return '{}'.format(self.base) + class ContextSeeker(RegisteredAction): def __init__(self, @@ -53,6 +57,10 @@ def __init__(self, def _execute(self, data=None): self.nexus().state.add(StackItemSeeker(self, data)) + def __str__(self): + tail = reduce((lambda x, y: "{}_{}".format(x, y)), self.forward) if isinstance(self.forward, list) else self.forward + return '{}!{}'.format(self.back, tail) if self.back else '!{}'.format(tail) + class AsynchronousAction(ContextSeeker): ''' @@ -91,6 +99,10 @@ def _execute(self, data=None): self.nexus().state.add(StackItemAsynchronous(self, data)) + def __str__(self): + action = reduce((lambda x, y: "{}${}".format(x, y)), self.forward) if isinstance(self.forward, list) else self.forward + return '#{}&{}*{}'.format(self.time_in_seconds, action, self.repetitions) + @staticmethod def hmc_complete(data_function): ''' returns a function which applies the passed in function to diff --git a/castervoice/lib/merge/state/contextoptions.py b/castervoice/lib/merge/state/contextoptions.py index 43b4970c4..365a913a2 100644 --- a/castervoice/lib/merge/state/contextoptions.py +++ b/castervoice/lib/merge/state/contextoptions.py @@ -4,6 +4,9 @@ @author: dave ''' +from functools import reduce +from types import FunctionType + class ContextSet: # ContextSet ''' @@ -27,6 +30,13 @@ def __init__(self, self.use_spoken = use_spoken self.use_rspec = use_rspec + def __str__(self): + prefix = reduce((lambda x, y: '{}`{}'.format(x, y)), + self.specTriggers) if len(self.specTriggers) > 1 else self.specTriggers[0].__str__() + params = reduce((lambda x, y: '{}, {}'.format(x, y)), self.parameters) if self.parameters else '' + action = self.f.__name__ if type(self.f) is FunctionType else self.f.__str__() + return '{}^{}({})'.format(prefix, action, params) + class ContextLevel: # ContextLevel ''' @@ -48,3 +58,10 @@ def copy(self): def number(self, index): # used for assigning indices self.index = index + + def __str__(self): + if len(self.sets) > 1: + return reduce((lambda x, y: '{}, {}'.format(x, y)), self.sets) + elif len(self.sets) == 1: + return '{}'.format(self.sets[0]) + return '' diff --git a/castervoice/lib/rules_collection.py b/castervoice/lib/rules_collection.py new file mode 100644 index 000000000..f1ba607aa --- /dev/null +++ b/castervoice/lib/rules_collection.py @@ -0,0 +1,33 @@ +''' +Collection of rules that are merged into a CCR grammar. +''' +_RULES = None + + +class RulesCollection: + + def __init__(self): + self._rules = [] + + def update(self, rules=None): + self._rules = rules + + def serialize(self): + rules = [] + for rule in self._rules: + klass = rule.get_rule_class() + instance = rule.get_rule_instance() + mapping = instance._smr_mapping if '_smr_mapping' in instance.__dict__ else klass.mapping + specs = sorted(["{}::{}".format(x, mapping[x]) for x in mapping]) + rules.append({ + 'name': rule.get_rule_class_name(), + 'specs': specs + }) + return [{'name': 'ccr', 'rules': rules}] + + +def get_instance(): + global _RULES + if _RULES is None: + _RULES = RulesCollection() + return _RULES diff --git a/castervoice/lib/settings.py b/castervoice/lib/settings.py index 9c36acec8..695026b0d 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -22,6 +22,7 @@ HMC_TITLE_RECORDING = " :: Recording Manager" HMC_TITLE_DIRECTORY = " :: Directory Selector" HMC_TITLE_CONFIRM = " :: Confirm" +HUD_TITLE = "Caster HUD v " + SOFTWARE_VERSION_NUMBER LEGION_TITLE = "legiongrid" RAINBOW_TITLE = "rainbowgrid" DOUGLAS_TITLE = "douglasgrid" @@ -281,6 +282,8 @@ def _get_defaults(): _validate_engine_path(), "HOMUNCULUS_PATH": str(Path(_BASE_PATH).joinpath("asynch/hmc/h_launch.py")), + "HUD_PATH": + str(Path(_BASE_PATH).joinpath("asynch/hud.py")), "LEGION_PATH": str(Path(_BASE_PATH).joinpath("asynch/mouse/legion.py")), "MEDIA_PATH": @@ -356,7 +359,7 @@ def _get_defaults(): # Default enabled hooks: Use hook class name "hooks": { - "default_hooks": ['PrinterHook'], + "default_hooks": ['PrinterHook', 'RulesLoadedHook'], }, # miscellaneous section @@ -480,8 +483,4 @@ def initialize(): if _debugger_path not in sys.path and os.path.isdir(_debugger_path): sys.path.append(_debugger_path) - # set up printer -- it doesn't matter where you do this; messages will start printing to the console after this - dh = printer.get_delegating_handler() - dh.register_handler(printer.SimplePrintMessageHandler()) - # begin using printer printer.out("Caster User Directory: {}".format(_USER_DIR)) diff --git a/castervoice/lib/utilities.py b/castervoice/lib/utilities.py index 2c73cc665..fe96b326c 100644 --- a/castervoice/lib/utilities.py +++ b/castervoice/lib/utilities.py @@ -1,9 +1,11 @@ import io import json import os +import json import re import subprocess import sys +import six import time import traceback import webbrowser @@ -30,19 +32,16 @@ # TODO: Move functions that manipulate or retrieve information from Windows to `window_mgmt_support` in navigation_rules. # TODO: Implement Optional exact title matching for `get_matching_windows` in Dragonfly def window_exists(windowname=None, executable=None): - if Window.get_matching_windows(title=windowname, executable=executable): - return True - else: - return False + return Window.get_matching_windows(title=windowname, executable=executable) and True -def get_window_by_title(title=None): +def get_window_by_title(title=None): # returns 0 if nothing found Matches = Window.get_matching_windows(title=title) if Matches: return Matches[0].handle else: - return 0 + return 0 def get_active_window_title(): @@ -159,6 +158,7 @@ def remote_debug(who_called_it=None): printer.out("ERROR: " + who_called_it + " called utilities.remote_debug() but the debug server wasn't running.") + def reboot(): # TODO: Save engine arguments elsewhere and retrieves for reboot. Allows for user-defined arguments. popen_parameters = [] @@ -186,7 +186,7 @@ def reboot(): printer.out(popen_parameters) subprocess.Popen(popen_parameters) else: - # Natlink out-of-process + # Natlink out-of-process engine.disconnect() subprocess.Popen([sys.executable, '-m', 'dragonfly', 'load', '--engine', 'natlink', '_*.py', '--no-recobs-messages']) @@ -228,9 +228,9 @@ def clear_log(): # TODO: window_exists utilized when engine launched through Dragonfly CLI via bat in future try: if WIN32: - clearcmd = "cls" # Windows OS + clearcmd = "cls" # Windows OS else: - clearcmd = "clear" # Linux + clearcmd = "clear" # Linux if get_current_engine().name == 'natlink': from natlinkcore import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() diff --git a/castervoice/rules/ccr/recording_rules/history.py b/castervoice/rules/ccr/recording_rules/history.py index 6dfc24540..26e0c94f4 100644 --- a/castervoice/rules/ccr/recording_rules/history.py +++ b/castervoice/rules/ccr/recording_rules/history.py @@ -1,4 +1,3 @@ -from dragonfly import RecognitionHistory from dragonfly.actions.action_base import Repeat from dragonfly.actions.action_function import Function from dragonfly.actions.action_playback import Playback diff --git a/castervoice/rules/core/utility_rules/caster_rule.py b/castervoice/rules/core/utility_rules/caster_rule.py index ef8c53b52..b001fc4fd 100644 --- a/castervoice/rules/core/utility_rules/caster_rule.py +++ b/castervoice/rules/core/utility_rules/caster_rule.py @@ -1,10 +1,15 @@ -from dragonfly import MappingRule, Function, RunCommand, Playback +from dragonfly import MappingRule, Function, RunCommand from castervoice.lib import control, utilities from castervoice.lib.ctrl.dependencies import find_pip # pylint: disable=no-name-in-module from castervoice.lib.ctrl.updatecheck import update from castervoice.lib.ctrl.mgr.rule_details import RuleDetails from castervoice.lib.merge.state.short import R +from castervoice.asynch.hud_support import show_hud +from castervoice.asynch.hud_support import hide_hud +from castervoice.asynch.hud_support import show_rules +from castervoice.asynch.hud_support import hide_rules +from castervoice.asynch.hud_support import clear_hud _PIP = find_pip() @@ -31,7 +36,7 @@ class CasterRule(MappingRule): "update dragonfly": R(_DependencyUpdate([_PIP, "install", "--upgrade", "dragonfly2"])), # update management ToDo: Fully implement castervoice PIP install - #"update caster": + #"update caster": # R(_DependencyUpdate([_PIP, "install", "--upgrade", "castervoice"])), # ccr de/activation @@ -39,6 +44,16 @@ class CasterRule(MappingRule): R(Function(lambda: control.nexus().set_ccr_active(True))), "disable (c c r|ccr)": R(Function(lambda: control.nexus().set_ccr_active(False))), + "show hud": + R(Function(show_hud), rdescript="Show the HUD window"), + "hide hud": + R(Function(hide_hud), rdescript="Hide the HUD window"), + "show rules": + R(Function(show_rules), rdescript="Open HUD frame with the list of active rules"), + "hide rules": + R(Function(hide_rules), rdescript="Hide the list of active rules"), + "clear hud": + R(Function(clear_hud), rdescript="Clear output the HUD window"), }