diff --git a/g13gui/g13gui/input.py b/g13gui/g13gui/input.py new file mode 100644 index 0000000..570602a --- /dev/null +++ b/g13gui/g13gui/input.py @@ -0,0 +1,63 @@ +import threading +import gi +import glob +import asyncio + +gi.require_version('Gtk', '3.0') +from gi.repository import GObject + +import evdev +from evdev import InputDevice +from evdev import ecodes +from select import select + + +class InputReader(threading.Thread, GObject.GObject): + INPUT_PATH = '/dev/input' + + def __init__(self): + threading.Thread.__init__(self, daemon=True) + GObject.GObject.__init__(self) + self.reset() + + def reset(self): + self._isRunning = False + self._keyStates = {} + + @GObject.Signal(name='evdev-key-released') + def keyReleased(self, keyCode, time): + print('key released: %d') + + @GObject.Signal(name='evdev-key-pressed') + def keyPressed(self, keyCode, time): + print('key pressed: %d') + + def run(self): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + devices = [InputDevice(path) for path in evdev.list_devices()] + keyboards = [dev for dev in devices if ecodes.EV_KEY in dev.capabilities()] + print('Have keyboards: %s' % (keyboards)) + + while self._isRunning: + r, w, x = select(keyboards, [], []) + print('keyboards listening!') + for fd in r: + for event in keyboards[fd].read(): + if event.type != ecodes.EV_KEY: + continue + + keyCode = event.keycode + keyDown = event.keystate == event.key_down + lastKeyState = self._keystates.get(keyCode, False) + + if keyDown and not lastKeyState: + self.emit('evdev-key-pressed', + keyCode, event.event.timestamp()) + elif not keyDown and lastKeyState: + self.emit('evdev-key-released', + keyCode, event.event.timestamp()) + + def shutdown(self): + self._isRunning = False diff --git a/g13gui/g13gui/observer/subject.py b/g13gui/g13gui/observer/subject.py new file mode 100644 index 0000000..794d21f --- /dev/null +++ b/g13gui/g13gui/observer/subject.py @@ -0,0 +1,76 @@ +import enum + + +class ChangeType(enum.Enum): + ADD = 0 + REMOVE = 1 + MODIFY = 2 + + +class Subject(object): + """Simple class to handle the subject-side of the Observer pattern.""" + + AllKeys = '' + + def registerObserver(self, observer, subscribedKeys=AllKeys): + """Registers an Observer class as an observer of this object""" + if subscribedKeys != Subject.AllKeys: + subscribedKeys = frozenset(subscribedKeys) + if '_observers' not in self.__dict__: + self._observers = {observer: subscribedKeys} + else: + self._observers[observer] = subscribedKeys + + def removeObserver(self, observer): + """Removes an observer from this object""" + if '_observers' in self.__dict__: + if observer in self._observers: + del self._observers[observer] + + def addChange(self, type, key, data=None): + """Schedules a change notification for transmitting later. + + type[ChangeType]: the type of change that occurred. + key[string]: a required name for what field changed. + data[object]: an optional context-dependent object, dict, or + None, specifying what changed. In the case of an ADD or MODIFY, + whatChanged should be the new data. In the case of a DELETE, it should + be the old data (or None). + """ + if '_changes' not in self.__dict__: + self._changes = [(type, key, data)] + else: + self._changes.append((type, key, data)) + + def clearChanges(self): + """Removes all scheduled changes from the change buffer.""" + self._changes = [] + + def notifyChange(self): + raise NotImplementedError('Use Subject.notifyChanged instead') + + def notifyChanged(self): + """Notifies all observers of scheduled changes in the change buffer. + + This method actually does the work of iterating through all observers + and all changes and delivering them to the Observer's onSubjectChanged + method. + + It is safe to call this if there are no changes to send in the buffer, + or there are no observers to send changes to. Note that calling this + when no observers are registered will still flush the change buffer. + """ + if '_observers' in self.__dict__ and '_changes' in self.__dict__: + for observer, subscribedKeys in self._observers.items(): + for type, key, data in self._changes: + if subscribedKeys == Subject.AllKeys or key in subscribedKeys: + observer.onSubjectChanged(self, type, key, data) + + self._changes = [] + + def setProperty(self, propertyName, value, notify=True): + propertyName = '_' + propertyName + self.__dict__[propertyName] = value + self.addChange(ChangeType.MODIFY, propertyName, value) + if notify: + self.notifyChanged() diff --git a/g13gui/g13gui/tests.py b/g13gui/g13gui/tests.py index d8943e7..5f03fa2 100644 --- a/g13gui/g13gui/tests.py +++ b/g13gui/g13gui/tests.py @@ -8,6 +8,8 @@ from g13gui.bitwidgets.fonts_tests import * from g13gui.bitwidgets.label_tests import * from g13gui.bitwidgets.display_tests import * from g13gui.bitwidgets.screen_tests import * +from g13gui.g13.displaydevice_tests import * +from g13gui.g13.manager_tests import * if __name__ == '__main__': diff --git a/g13gui/g13gui/ui/g13button.py b/g13gui/g13gui/ui/g13button.py index 7ff11da..a3147b5 100644 --- a/g13gui/g13gui/ui/g13button.py +++ b/g13gui/g13gui/ui/g13button.py @@ -44,7 +44,7 @@ class G13Button(Gtk.MenuButton, GtkObserver): bindings = self._prefs.selectedProfile().keyBinding(self._keyName) if len(bindings) > 0: - keybinds = G13ToGDK(bindings) + keybinds = BindsToKeynames(bindings) accelerator = '+'.join(keybinds) shortcut = Gtk.ShortcutsShortcut( shortcut_type=Gtk.ShortcutType.ACCELERATOR, diff --git a/g13gui/g13gui/ui/g13buttonpopover.py b/g13gui/g13gui/ui/g13buttonpopover.py index cd1c235..74fc8a8 100644 --- a/g13gui/g13gui/ui/g13buttonpopover.py +++ b/g13gui/g13gui/ui/g13buttonpopover.py @@ -1,17 +1,16 @@ import gi -from g13gui.observer import GtkObserver - -from g13gui.model.bindings import G13ToGDK -from g13gui.model.bindings import GDKToG13 -from g13gui.model.bindings import G13DKeyIsModifier +from g13gui.observer.gtkobserver import GtkObserver +from g13gui.model.bindings import BindsToKeynames +from g13gui.model.bindings import KeycodeIsModifier +from g13gui.input import InputReader gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') from gi.repository import Gtk, GObject, Gdk -MAX_DELAY_BETWEEN_PRESSES_MILLIS = 250 +MAX_DELAY_BETWEEN_PRESSES_SECONDS = 0.250 class G13ButtonPopover(Gtk.Popover, GtkObserver): @@ -21,6 +20,11 @@ class G13ButtonPopover(Gtk.Popover, GtkObserver): self._prefs = prefs self._prefs.registerObserver(self, {'selectedProfile'}) + + self._inputReader = InputReader() + self._inputReader.connect('evdev-key-pressed', self.keypress) + self._inputReader.connect('evdev-key-released', self.keyrelease) + self._keyName = keyName self._modifiers = set() @@ -52,8 +56,6 @@ class G13ButtonPopover(Gtk.Popover, GtkObserver): self._box.pack_start(button, True, True, 6) self._box.show_all() - self.connect("key-press-event", self.keypress) - self.connect("key-release-event", self.keyrelease) self.connect("show", self.shown) self.connect("closed", self.closed) button.connect("pressed", self.clear) @@ -61,7 +63,7 @@ class G13ButtonPopover(Gtk.Popover, GtkObserver): self.buildBindingDisplay() def shown(self, widget): - self.grab_add() + self._inputReader.start() def rebuildBindingDisplay(self): if self._bindingBox: @@ -76,13 +78,11 @@ class G13ButtonPopover(Gtk.Popover, GtkObserver): self._box.reorder_child(self._bindingBox, 1) if len(self._currentBindings) > 0: - keybinds = G13ToGDK(self._currentBindings) + keybinds = BindsToKeynames(self._currentBindings) accelerator = '+'.join(keybinds) - shortcut = Gtk.ShortcutsShortcut( - shortcut_type=Gtk.ShortcutType.ACCELERATOR, - accelerator=accelerator) - shortcut.set_halign(Gtk.Align.CENTER) - self._bindingBox.pack_start(shortcut, True, True, 6) + label = Gtk.Label(accelerator) + label.set_halign(Gtk.Align.CENTER) + self._bindingBox.pack_start(label, True, True, 6) else: label = Gtk.Label() label.set_markup("No binding. Press a key to bind.") @@ -90,31 +90,26 @@ class G13ButtonPopover(Gtk.Popover, GtkObserver): self._bindingBox.show_all() - def keypress(self, buttonMenu, eventKey): - pressDelta = eventKey.time - self._lastPressTime - if pressDelta > MAX_DELAY_BETWEEN_PRESSES_MILLIS: + def keypress(self, keyCode, timestamp): + print('keypress: %s' % repr(keyCode)) + + pressDelta = timestamp - self._lastPressTime + if pressDelta > MAX_DELAY_BETWEEN_PRESSES_SECONDS: self._modifiers = set() self._consonantKey = None - binding = Gdk.keyval_name(eventKey.keyval) - if len(binding) == 1: - binding = binding.upper() - if binding == 'Meta_L': - binding = 'Alt_L' - if binding == 'Meta_R': - binding = 'Alt_R' + key = keyCode + print('Hardware keycode: %s' % (key)) - binding = GDKToG13(binding) - - if G13DKeyIsModifier(binding): - self._modifiers.add(binding) + if KeycodeIsModifier(key): + self._modifiers.add(key) else: - self._consonantKey = binding + self._consonantKey = key - self._lastPressTime = eventKey.time + self._lastPressTime = timestamp return True - def keyrelease(self, buttonMenu, eventKey): + def keyrelease(self, keyCode): self._currentBindings = sorted(list(self._modifiers)) if self._consonantKey: self._currentBindings += [self._consonantKey] @@ -128,7 +123,7 @@ class G13ButtonPopover(Gtk.Popover, GtkObserver): self.hide() def closed(self, buttonMenu): - self.grab_remove() + self._inputReader.shutdown() self._prefs.selectedProfile().bindKey(self._keyName, self._currentBindings) self.hide()