Skip to content

Commit

Permalink
Add HUD
Browse files Browse the repository at this point in the history
  • Loading branch information
dmakarov committed Apr 5, 2020
1 parent 936c244 commit 68d2db9
Show file tree
Hide file tree
Showing 15 changed files with 481 additions and 54 deletions.
65 changes: 52 additions & 13 deletions _caster.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,61 @@
main Caster module
Created on Jun 29, 2014
'''
import imp
import logging
import six
from dragonfly import get_engine
from dragonfly import RecognitionObserver
from castervoice.lib import control
from castervoice.lib import settings
from castervoice.lib.ctrl.dependencies import DependencyMan
from castervoice.lib.ctrl.updatecheck import UpdateChecker
from castervoice.lib.utilities import start_hud

if six.PY2:
import logging
logging.basicConfig()

from castervoice.lib.ctrl.dependencies import DependencyMan # requires nothing
DependencyMan().initialize()
class LoggingHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
self.hud = control.nexus().comm.get_com("hud")

from castervoice.lib import settings
settings.initialize()
def emit(self, record):
try:
self.hud.send("# {}".format(record.msg))
except ConnectionRefusedError: # pylint: disable=undefined-variable
print("# {}".format(record.msg))

from castervoice.lib.ctrl.updatecheck import UpdateChecker # requires settings/dependencies
UpdateChecker().initialize()

from dragonfly import get_engine
class Observer(RecognitionObserver):
def __init__(self):
self.hud = control.nexus().comm.get_com("hud")

def on_begin(self):
pass

def on_recognition(self, words):
try:
self.hud.send("$ {}".format(" ".join(words)))
except ConnectionRefusedError: # pylint: disable=undefined-variable
print("$ {}".format(" ".join(words)))

_NEXUS = None
def on_failure(self):
try:
self.hud.send("?!")
except ConnectionRefusedError: # pylint: disable=undefined-variable
print("?!")


if six.PY2:
logging.basicConfig()
DependencyMan().initialize() # requires nothing
settings.initialize()
UpdateChecker().initialize() # requires settings/dependencies

if get_engine()._name in ["sapi5shared", "sapi5", "sapi5inproc"]:
settings.WSR = True
from castervoice.rules.ccr.standard import SymbolSpecs
SymbolSpecs.set_cancel_word("escape")

from castervoice.lib import control

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
Expand All @@ -41,3 +70,13 @@
sikuli_controller.get_instance().bootstrap_start_server_proxy()

print("\n*- Starting " + settings.SOFTWARE_NAME + " -*")

try:
imp.find_module('PySide2')
start_hud()
_logger = logging.getLogger('caster')
_logger.addHandler(LoggingHandler()) # must be after nexus initialization
_logger.setLevel(logging.DEBUG)
Observer().register() # must be after HUD process has started
except ImportError:
pass # HUD is not available
217 changes: 217 additions & 0 deletions castervoice/asynch/hud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#! 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

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("Caster HUD")
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 = '<font color="blue">&lt;</font><b>{}</b>'.format(escaped_text[1:])
if self.commands_count == 0:
self.output.setHtml(formatted_text)
else:
# self.output.append('<br>')
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 == 20:
self.commands_count = 0
return True
elif escaped_text.startswith('#'):
formatted_text = '<font color="red">&gt;</font>{}'.format(escaped_text[1:])
else:
formatted_text = escaped_text
self.output.append(formatted_text)
self.output.ensureCursorVisible()
return True
return QMainWindow.event(self, event)

def closeEvent(self, event):
event.accept()

def setup_xmlrpc_server(self):
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_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)
8 changes: 5 additions & 3 deletions castervoice/lib/ctrl/mgr/grammar_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os, traceback
import os
import traceback

from dragonfly import Grammar

Expand All @@ -11,6 +12,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

Expand Down Expand Up @@ -239,7 +241,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.
Expand Down Expand Up @@ -298,7 +300,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):
Expand Down
12 changes: 8 additions & 4 deletions castervoice/lib/ctrl/mgr/rule_details.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os, traceback
import inspect
import os
import traceback
from castervoice.lib import printer


Expand Down Expand Up @@ -34,20 +35,23 @@ 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:
frame = stack[index]
module = inspect.getmodule(frame[0])
filepath = module.__file__.replace("\\", "/")
filepath = module.__file__.replace("\\", "/")
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ class EventType(object):
ACTIVATION = "activation"
NODE_CHANGE = "node change"
ON_ERROR = "on error"
RULES_LOADED = "rules loaded"
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 68d2db9

Please sign in to comment.