From 476b43ed5f02c9d8e0e7b9fa68d30634d0078a24 Mon Sep 17 00:00:00 2001 From: June Tate-Gans Date: Wed, 28 Apr 2021 18:25:47 -0500 Subject: [PATCH] g13gui: Got a bit carried away, totally reworked things So at this point, g13gui has quite a lot of functionality built into it now. We can bind keys, we can unbind keys, we can upload whole profiles to g13d, we can create and edit profiles, and we can even set LCD colors per-profile. We're not loading the configuration from disk, sadly, since I had to rework quite a lot of the UI infrastructure to get observer notifications to work with GTK. Lots of simplifications in here, though, which reduces the complexity of the code considerably. - Migrated all model related classes into the model module. - Migrated UI classes into the ui module. - Migrated observer-related stuff into observer. - Created the GtkObserver adaptor for GTK UI threading. This makes heavy use of GObject signals and queuing to dispatch to the UI thread. - Migrated all of the G13 buttons into their own class. - Renamed ButtonMenu into G13ButtonPopover. - Setup the profile combo box as its own class. - Created the profile popover so we can add/remove/edit profiles. --- g13gui/g13gui/__init__.py | 3 + g13gui/g13gui/g13d.py | 23 +- g13gui/g13gui/main.py | 11 +- g13gui/g13gui/mainwindow.py | 234 ------------------ g13gui/g13gui/model/__init__.py | 2 + g13gui/g13gui/{ => model}/bindingprofile.py | 41 ++- .../{ => model}/bindingprofile_tests.py | 31 ++- g13gui/g13gui/{ => model}/bindings.py | 94 ++++--- g13gui/g13gui/{ => model}/prefs.py | 14 +- g13gui/g13gui/{ => model}/prefs_tests.py | 8 +- g13gui/g13gui/observer/__init__.py | 5 + g13gui/g13gui/observer/gtkobserver.py | 53 ++++ g13gui/g13gui/{ => observer}/observer.py | 0 .../g13gui/{ => observer}/observer_tests.py | 0 g13gui/g13gui/ui/__init__.py | 6 + g13gui/g13gui/ui/g13button.py | 71 ++++++ .../{buttonmenu.py => ui/g13buttonpopover.py} | 89 ++++--- g13gui/g13gui/ui/mainwindow.py | 186 ++++++++++++++ g13gui/g13gui/ui/profilecombobox.py | 59 +++++ g13gui/g13gui/ui/profilepopover.py | 129 ++++++++++ 20 files changed, 702 insertions(+), 357 deletions(-) delete mode 100644 g13gui/g13gui/mainwindow.py create mode 100644 g13gui/g13gui/model/__init__.py rename g13gui/g13gui/{ => model}/bindingprofile.py (70%) rename g13gui/g13gui/{ => model}/bindingprofile_tests.py (68%) rename g13gui/g13gui/{ => model}/bindings.py (74%) rename g13gui/g13gui/{ => model}/prefs.py (89%) rename g13gui/g13gui/{ => model}/prefs_tests.py (95%) create mode 100644 g13gui/g13gui/observer/__init__.py create mode 100644 g13gui/g13gui/observer/gtkobserver.py rename g13gui/g13gui/{ => observer}/observer.py (100%) rename g13gui/g13gui/{ => observer}/observer_tests.py (100%) create mode 100644 g13gui/g13gui/ui/__init__.py create mode 100644 g13gui/g13gui/ui/g13button.py rename g13gui/g13gui/{buttonmenu.py => ui/g13buttonpopover.py} (55%) create mode 100644 g13gui/g13gui/ui/mainwindow.py create mode 100644 g13gui/g13gui/ui/profilecombobox.py create mode 100644 g13gui/g13gui/ui/profilepopover.py diff --git a/g13gui/g13gui/__init__.py b/g13gui/g13gui/__init__.py index e69de29..a3962c5 100644 --- a/g13gui/g13gui/__init__.py +++ b/g13gui/g13gui/__init__.py @@ -0,0 +1,3 @@ +import g13gui.model as model +import g13gui.ui as ui +import g13gui.observer as observer diff --git a/g13gui/g13gui/g13d.py b/g13gui/g13gui/g13d.py index 6b1c938..073100f 100644 --- a/g13gui/g13gui/g13d.py +++ b/g13gui/g13gui/g13d.py @@ -9,11 +9,10 @@ import traceback import xdg.BaseDirectory as basedir import json -from common import PROFILES_CONFIG_PATH -from common import VERSION +from g13gui.common import PROFILES_CONFIG_PATH +from g13gui.common import VERSION gi.require_version('Gtk', '3.0') - from gi.repository import GObject @@ -32,24 +31,12 @@ class UploadTask(): class SaveTask(): - def __init__(self, profiles, defaultProfileName): - self._profiles = profiles - self._defaultProfileName = defaultProfileName + def __init__(self, prefsDict): + self._prefsDict = prefsDict def run(self, outfp, infp, callback): - profiles = {} - for key, profile in self._profiles.items(): - profiles[key] = profile.toDict() - - config = { - 'version': VERSION, - 'defaultProfileName': self._defaultProfileName, - 'profiles': profiles - } - - encoder = json.JSONEncoder() with open(PROFILES_CONFIG_PATH, 'w') as f: - f.write(encoder.encode(config)) + f.write(json.dumps(self._prefsDict, default=str)) f.flush() diff --git a/g13gui/g13gui/main.py b/g13gui/g13gui/main.py index fd53f97..483e26c 100644 --- a/g13gui/g13gui/main.py +++ b/g13gui/g13gui/main.py @@ -3,20 +3,23 @@ import queue import gi -gi.require_version('Gtk', '3.0') +import g13gui.model as model +import g13gui.ui as ui +from g13gui.g13d import G13DWorker + +gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject -from mainwindow import MainWindow -from g13d import G13DWorker VERSION = '0.1.0' if __name__ == '__main__': + prefs = model.Preferences() queue = queue.Queue() - win = MainWindow(queue) + win = ui.MainWindow(queue, prefs) win.connect("destroy", Gtk.main_quit) win.show_all() diff --git a/g13gui/g13gui/mainwindow.py b/g13gui/g13gui/mainwindow.py deleted file mode 100644 index ecef2cc..0000000 --- a/g13gui/g13gui/mainwindow.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/python - -import gi -import json -import traceback - -from common import VERSION -from common import PROFILES_CONFIG_PATH - -from g13d import UploadTask -from g13d import SaveTask -from bindings import G13D_TO_GDK_KEYBINDS -from bindings import G13_KEYS -from bindingprofile import BindingProfile -from buttonmenu import ButtonMenu - -gi.require_version('Gtk', '3.0') - -from gi.repository import Gtk, GObject - - -class MainWindow(Gtk.Window): - def __init__(self, workerQueue): - Gtk.Window.__init__(self) - - self._workerQueue = workerQueue - - GObject.signal_new("uploading", self, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_FLOAT,)) - self.connect("uploading", self.uploadStatusChanged) - GObject.signal_new("daemon-connection-changed", self, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_BOOLEAN,)) - self.connect("daemon-connection-changed", self.daemonConnectionChanged) - - default_profile = BindingProfile() - default_profile.registerObserver(self) - self._profiles = {'Default Profile': default_profile} - self._currentProfile = self._profiles['Default Profile'] - - self.loadProfiles() - - self.headerBar = Gtk.HeaderBar() - self.headerBar.set_title("G13 Configurator") - self.headerBar.set_show_close_button(True) - - self.profileComboBox = Gtk.ComboBoxText() - self.profileComboBox.connect("changed", self.profileChanged) - self.headerBar.add(self.profileComboBox) - - addProfileButton = Gtk.Button.new_from_icon_name("add", 1) - addProfileButton.connect("clicked", self.addProfileClicked) - self.headerBar.add(addProfileButton) - - self._uploadButton = Gtk.Button.new_from_icon_name("up", 1) - self._uploadButton.connect("clicked", self.uploadClicked) - self.headerBar.add(self._uploadButton) - - Gtk.Window.set_default_size(self, 640, 480) - Gtk.Window.set_titlebar(self, self.headerBar) - - self.box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) - self.add(self.box) - - self.setupG13ButtonGrid() - self.updateProfileBox() - - def loadProfiles(self): - result = {} - currentProfile = None - - try: - with open(PROFILES_CONFIG_PATH, 'r') as f: - serializedConfig = json.load(f) - - if serializedConfig['version'] != VERSION: - print('WARNING: This profile config is from a different version (wanted %s got %s)!' % - (VERSION, result['version'])) - print('This configuration may not load properly!') - - print("Loaded dict: %s" % (serializedConfig)) - - for name, dict in serializedConfig['profiles'].items(): - result[name] = BindingProfile(dict=dict) - - for name, dict in result.items(): - if name == serializedConfig['defaultProfileName']: - currentProfile = result[name] - - except (OSError, json.JSONDecodeError, KeyError, ValueError) as e: - print("Failed to read profiles from disk: %s" % (e)) - traceback.print_exc() - else: - self._profiles = result - self._currentProfile = currentProfile - - def daemonConnectionChanged(self, widget, connected): - self._connected = connected - if connected: - self._uploadButton.set_state_flags(Gtk.StateFlags.NORMAL, True) - else: - self._uploadButton.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) - - def uploadStatusChanged(self, widget, percentage): - print("Upload in progress: %f" % (percentage * 100)) - - def profileChanged(self, widget): - pass - - def addProfileClicked(self, widget): - pass - - def uploadClicked(self, widget): - config = self._currentProfile.generateConfigString() - task = UploadTask(config) - self._workerQueue.put(task) - - currentProfileName = None - for name, profile in self._profiles.items(): - if self._currentProfile == profile: - currentProfileName = name - break - - task = SaveTask(self._profiles, currentProfileName) - self._workerQueue.put(task) - - def updateProfileBox(self): - self.profileComboBox.remove_all() - row = 0 - for profileName in self._profiles.keys(): - self.profileComboBox.append_text(profileName) - - if self._profiles[profileName] == self._currentProfile: - print("Set active profile to %d (%s)" % (row, profileName)) - self.profileComboBox.set_active(row) - - row = row + 1 - - def setupG13ButtonGrid(self): - self.lcdButtons = Gtk.Box(spacing=3, orientation=Gtk.Orientation.HORIZONTAL) - self.box.pack_start(self.lcdButtons, True, True, 6) - - self.mButtons = Gtk.Box(spacing=3, orientation=Gtk.Orientation.HORIZONTAL) - self.box.pack_start(self.mButtons, True, True, 6) - - self.keyGrid = Gtk.Grid() - self.keyGrid.set_row_spacing(3) - self.keyGrid.set_column_spacing(3) - self.box.pack_start(self.keyGrid, True, True, 6) - - self.stickGrid = Gtk.Grid() - self.stickGrid.set_row_spacing(3) - self.stickGrid.set_column_spacing(3) - self.box.pack_start(self.stickGrid, False, False, 6) - - self.g13Buttons = {} - - self.lcdButtons.pack_start(self.newG13Button('BD'), True, True, 6) - self.lcdButtons.pack_start(self.newG13Button('L1'), True, True, 6) - self.lcdButtons.pack_start(self.newG13Button('L2'), True, True, 6) - self.lcdButtons.pack_start(self.newG13Button('L3'), True, True, 6) - self.lcdButtons.pack_start(self.newG13Button('L4'), True, True, 6) - self.lcdButtons.pack_start(self.newG13Button('LIGHT'), True, True, 6) - - self.mButtons.pack_start(self.newG13Button('M1'), True, True, 6) - self.mButtons.pack_start(self.newG13Button('M2'), True, True, 6) - self.mButtons.pack_start(self.newG13Button('M3'), True, True, 6) - self.mButtons.pack_start(self.newG13Button('MR'), True, True, 6) - - # G1 to G14 - self._buttonNum = 1 - for row in range(0, 2): - for col in range(0, 7): - self.keyGrid.attach(self.newG13NumberedButton(), - col, row, 1, 1) - - # G15 to G19 - self.keyGrid.attach(self.newG13NumberedButton(), 1, 3, 1, 1) - self.keyGrid.attach(self.newG13NumberedButton(), 2, 3, 1, 1) - self.keyGrid.attach(self.newG13NumberedButton(), 3, 3, 1, 1) - self.keyGrid.attach(self.newG13NumberedButton(), 4, 3, 1, 1) - self.keyGrid.attach(self.newG13NumberedButton(), 5, 3, 1, 1) - - # G20 to G22 - self.keyGrid.attach(self.newG13NumberedButton(), 2, 4, 1, 1) - self.keyGrid.attach(self.newG13NumberedButton(), 3, 4, 1, 1) - self.keyGrid.attach(self.newG13NumberedButton(), 4, 4, 1, 1) - - self.stickGrid.attach(self.newG13Button("STICK_UP"), 4, 0, 1, 1) - self.stickGrid.attach(self.newG13Button("LEFT"), 2, 1, 1, 1) - self.stickGrid.attach(self.newG13Button("STICK_LEFT"), 3, 1, 1, 1) - self.stickGrid.attach(self.newG13Button("TOP"), 4, 1, 1, 1) - self.stickGrid.attach(self.newG13Button("STICK_RIGHT"), 5, 1, 1, 1) - self.stickGrid.attach(self.newG13Button("STICK_DOWN"), 4, 2, 1, 1) - self.stickGrid.attach(self.newG13Button("DOWN"), 4, 3, 1, 1) - - def newG13NumberedButton(self): - button = self.newG13Button('G' + str(self._buttonNum)) - self._buttonNum = self._buttonNum + 1 - return button - - def newG13Button(self, name): - popover = ButtonMenu(self._currentProfile, name) - button = Gtk.MenuButton(popover=popover) - self.g13Buttons[name] = button - self.updateG13Button(name) - - return button - - def updateG13Button(self, name): - button = self.g13Buttons[name] - children = button.get_children() - - if len(children) > 0: - button.remove(children[0]) - - bindings = self._currentProfile.getBoundKey(name) - - if len(bindings) > 0: - keybinds = [G13D_TO_GDK_KEYBINDS[binding] for binding in bindings] - accelerator = '+'.join(keybinds) - shortcut = Gtk.ShortcutsShortcut( - shortcut_type=Gtk.ShortcutType.ACCELERATOR, - accelerator=accelerator) - shortcut.set_halign(Gtk.Align.CENTER) - button.add(shortcut) - else: - label = Gtk.Label(name) - button.add(label) - - button.show_all() - - def on_changed(self, profile): - for key in G13_KEYS: - self.updateG13Button(key) - - self.uploadClicked(self) diff --git a/g13gui/g13gui/model/__init__.py b/g13gui/g13gui/model/__init__.py new file mode 100644 index 0000000..ae7d54e --- /dev/null +++ b/g13gui/g13gui/model/__init__.py @@ -0,0 +1,2 @@ +from g13gui.model.bindingprofile import BindingProfile +from g13gui.model.prefs import Preferences diff --git a/g13gui/g13gui/bindingprofile.py b/g13gui/g13gui/model/bindingprofile.py similarity index 70% rename from g13gui/g13gui/bindingprofile.py rename to g13gui/g13gui/model/bindingprofile.py index 6ad31bf..d241d5f 100644 --- a/g13gui/g13gui/bindingprofile.py +++ b/g13gui/g13gui/model/bindingprofile.py @@ -1,9 +1,9 @@ #!/usr/bin/python -import bindings +import g13gui.model.bindings as bindings -from observer import Subject -from observer import ChangeType +from g13gui.observer import Subject +from g13gui.observer import ChangeType class BindingProfile(Subject): @@ -17,6 +17,10 @@ class BindingProfile(Subject): self._stickRegions = bindings.DEFAULT_STICK_REGIONS self._stickRegionBindings = bindings.DEFAULT_STICK_REGION_BINDINGS self._keyBindings = bindings.DEFAULT_KEY_BINDINGS + self._lcdColor = bindings.DEFAULT_LCD_COLOR + + def lcdColor(self): + return self._lcdColor def stickMode(self): return self._stickMode @@ -36,6 +40,14 @@ class BindingProfile(Subject): return [] + def _setLCDColor(self, red, green, blue): + self._lcdColor = (red, green, blue) + self.addChange(ChangeType.MODIFY, 'lcdcolor', self._lcdColor) + + def setLCDColor(self, red, green, blue): + self._setLCDColor(red, green, blue) + self.notifyChanged() + def _bindKey(self, gkey, keybinding): if gkey in self._stickRegions.keys(): self._stickRegionBindings[gkey] = keybinding @@ -59,15 +71,24 @@ class BindingProfile(Subject): self._setStickMode(stickmode) self.notifyChanged() + def _lcdColorToCommandString(self): + return 'rgb %d %d %d' % tuple([int(x * 255) for x in self._lcdColor]) + + def _keyBindingToCommandString(self, gkey): + kbdkey = self._keyBindings[gkey] + if len(kbdkey) > 0: + keys = '+'.join(['KEY_' + key for key in kbdkey]) + return "bind %s %s" % (gkey, keys) + else: + return "unbind %s" % (gkey) + def toCommandString(self): commands = [] - for gkey, kbdkey in self._keyBindings.items(): - if len(kbdkey) > 0: - keys = '+'.join(['KEY_' + key for key in kbdkey]) - commands.append("bind %s %s" % (gkey, keys)) - else: - commands.append("unbind %s" % (gkey)) + commands.append(self._lcdColorToCommandString()) + + for gkey in self._keyBindings.keys(): + commands.append(self._keyBindingToCommandString(gkey)) if self._stickMode == bindings.StickMode.KEYS: for region, bounds in self._stickRegions.items(): @@ -80,6 +101,7 @@ class BindingProfile(Subject): return '\n'.join(commands) def loadFromDict(self, dict): + self._lcdColor = dict['lcdcolor'] self._stickMode = dict['stickMode'] self._stickRegions = dict['stickRegions'] self._stickRegionBindings = dict['stickRegionBindings'] @@ -87,6 +109,7 @@ class BindingProfile(Subject): def saveToDict(self): return { + 'lcdcolor': self._lcdColor, 'stickMode': self._stickMode, 'stickRegions': self._stickRegions, 'stickRegionBindings': self._stickRegionBindings, diff --git a/g13gui/g13gui/bindingprofile_tests.py b/g13gui/g13gui/model/bindingprofile_tests.py similarity index 68% rename from g13gui/g13gui/bindingprofile_tests.py rename to g13gui/g13gui/model/bindingprofile_tests.py index 5684115..d263f41 100644 --- a/g13gui/g13gui/bindingprofile_tests.py +++ b/g13gui/g13gui/model/bindingprofile_tests.py @@ -2,10 +2,10 @@ import unittest -import bindings -from bindingprofile import BindingProfile -from observer import ChangeType -from observer import ObserverTestCase +import g13gui.model.bindings as bindings +from g13gui.model.bindingprofile import BindingProfile +from g13gui.observer import ChangeType +from g13gui.observer import ObserverTestCase class PrefsTestCase(ObserverTestCase): @@ -16,14 +16,18 @@ class PrefsTestCase(ObserverTestCase): bp = BindingProfile() self.assertEqual(bp.stickMode(), bindings.StickMode.KEYS) self.assertEqual(bp.stickRegions(), bindings.DEFAULT_STICK_REGIONS) - self.assertEqual(bp._stickRegionBindings, bindings.DEFAULT_STICK_REGION_BINDINGS) + self.assertEqual(bp.lcdColor(), bindings.DEFAULT_LCD_COLOR) + self.assertEqual(bp._stickRegionBindings, + bindings.DEFAULT_STICK_REGION_BINDINGS) self.assertEqual(bp._keyBindings, bindings.DEFAULT_KEY_BINDINGS) + self.assertEqual(bp._lcdColor, bindings.DEFAULT_LCD_COLOR) def testInvalidDict(self): bp = BindingProfile({}) self.assertEqual(bp.stickMode(), bindings.StickMode.KEYS) self.assertEqual(bp.stickRegions(), bindings.DEFAULT_STICK_REGIONS) - self.assertEqual(bp._stickRegionBindings, bindings.DEFAULT_STICK_REGION_BINDINGS) + self.assertEqual(bp._stickRegionBindings, + bindings.DEFAULT_STICK_REGION_BINDINGS) self.assertEqual(bp._keyBindings, bindings.DEFAULT_KEY_BINDINGS) def testDictLoadSave(self): @@ -72,6 +76,21 @@ class PrefsTestCase(ObserverTestCase): else: self.fail('Expected ValueError from setStickMode') + def testLCDColor(self): + bp = BindingProfile() + bp.registerObserver(self) + bp.setLCDColor(1.0, 0.5, 0.1) + self.assertEqual(bp._lcdColor, (1.0, 0.5, 0.1)) + self.assertEqual(bp.lcdColor(), (1.0, 0.5, 0.1)) + self.assertChangeCount(1) + self.assertChangeNotified(bp, ChangeType.MODIFY, 'lcdcolor') + self.assertChangeDataEquals((1.0, 0.5, 0.1)) + + def testToCommandString(self): + bp = BindingProfile() + result = bp.toCommandString() + self.assertIsNotNone(result) + if __name__ == '__main__': unittest.main() diff --git a/g13gui/g13gui/bindings.py b/g13gui/g13gui/model/bindings.py similarity index 74% rename from g13gui/g13gui/bindings.py rename to g13gui/g13gui/model/bindings.py index 48b538f..3636ea7 100644 --- a/g13gui/g13gui/bindings.py +++ b/g13gui/g13gui/model/bindings.py @@ -1,7 +1,9 @@ #!/usr/bin/python3 -import enum - +""" +Defines a whole bunch of constants relating to mapping G13D key names to GDK key +names, as well as the symbols that g13d natively supports. +""" G13D_TO_GDK_KEYBINDS = { '0': '0', @@ -147,42 +149,35 @@ G13_KEYS = [ 'LEFT', 'DOWN', 'TOP', ] -DEFAULT_STICK_REGIONS = { - 'STICK_UP': [0.0, 0.0, 1.0, 0.2], - 'STICK_DOWN': [0.0, 0.8, 1.0, 1.0], - 'STICK_LEFT': [0.0, 0.0, 0.2, 1.0], - 'STICK_RIGHT': [0.8, 0.0, 1.0, 1.0] -} - DEFAULT_KEY_BINDINGS = { - 'G1': ['ESC'], + 'G1': ['GRAVE'], 'G2': ['1'], 'G3': ['2'], 'G4': ['3'], 'G5': ['4'], 'G6': ['5'], - 'G7': ['Y'], - 'G8': ['Q'], - 'G9': ['Z'], - 'G10': ['V'], - 'G11': ['SPACE'], - 'G12': ['E'], - 'G13': ['R'], - 'G14': ['U'], - 'G15': ['LEFTSHIFT'], - 'G16': ['F'], - 'G17': ['X'], - 'G18': ['C'], - 'G19': ['H'], - 'G20': ['LEFTCTRL'], - 'G21': ['B'], - 'G22': ['T'], - 'LEFT': ['TAB'], - 'DOWN': ['M'], + 'G7': ['6'], + 'G8': ['TAB'], + 'G9': ['Q'], + 'G10': ['W'], + 'G11': ['E'], + 'G12': ['R'], + 'G13': ['T'], + 'G14': ['Y'], + 'G15': ['A'], + 'G16': ['S'], + 'G17': ['D'], + 'G18': ['F'], + 'G19': ['G'], + 'G20': ['X'], + 'G21': ['C'], + 'G22': ['V'], + 'LEFT': ['B'], + 'DOWN': ['N'], } -class StickRegion(enum.Enum): +class StickRegion(): UP = 'STICK_UP' DOWN = 'STICK_DOWN' LEFT = 'STICK_LEFT' @@ -190,35 +185,54 @@ class StickRegion(enum.Enum): ALL_STICK_REGIONS = frozenset({ - StickRegion.UP, - StickRegion.DOWN, - StickRegion.LEFT, - StickRegion.RIGHT + StickRegion.UP, StickRegion.DOWN, + StickRegion.LEFT, StickRegion.RIGHT }) +DEFAULT_STICK_REGIONS = { + StickRegion.UP: [0.0, 0.0, 1.0, 0.2], + StickRegion.DOWN: [0.0, 0.8, 1.0, 1.0], + StickRegion.LEFT: [0.0, 0.0, 0.2, 1.0], + StickRegion.RIGHT: [0.8, 0.0, 1.0, 1.0] +} + DEFAULT_STICK_REGION_BINDINGS = { - StickRegion.UP: ['W'], - StickRegion.DOWN: ['S'], - StickRegion.LEFT: ['A'], + StickRegion.UP: ['W'], + StickRegion.DOWN: ['S'], + StickRegion.LEFT: ['A'], StickRegion.RIGHT: ['D'] } -class StickMode(enum.Enum): +class StickMode(): ABSOLUTE = 'ABSOLUTE' RELATIVE = 'RELATIVE' KEYS = 'KEYS' ALL_STICK_MODES = frozenset({ - StickMode.ABSOLUTE, - StickMode.RELATIVE, - StickMode.KEYS + StickMode.ABSOLUTE, StickMode.RELATIVE, StickMode.KEYS }) + def G13DKeyIsModifier(key): key = key.upper() return (key == 'LEFTSHIFT' or key == 'RIGHTSHIFT' or key == 'LEFTALT' or key == 'RIGHTALT' or key == 'LEFTCTRL' or key == 'RIGHTCTRL') + + +def G13ToGDK(keybinds): + if type(keybinds) == list: + return [G13D_TO_GDK_KEYBINDS[binding] for binding in keybinds] + return G13D_TO_GDK_KEYBINDS[keybinds] + + +def GDKToG13(keybinds): + if type(keybinds) == list: + return [GDK_TO_G13D_KEYBINDS[binding] for binding in keybinds] + return GDK_TO_G13D_KEYBINDS[keybinds] + + +DEFAULT_LCD_COLOR = (1.0, 0.0, 0.0) diff --git a/g13gui/g13gui/prefs.py b/g13gui/g13gui/model/prefs.py similarity index 89% rename from g13gui/g13gui/prefs.py rename to g13gui/g13gui/model/prefs.py index 03ae1d3..b67d66d 100644 --- a/g13gui/g13gui/prefs.py +++ b/g13gui/g13gui/model/prefs.py @@ -2,10 +2,10 @@ import traceback -from common import VERSION -from bindingprofile import BindingProfile -from observer import Subject -from observer import ChangeType +from g13gui.common import VERSION +from g13gui.model.bindingprofile import BindingProfile +from g13gui.observer import Subject +from g13gui.observer import ChangeType DEFAULT_PROFILE_NAME = 'Default Profile' @@ -21,7 +21,9 @@ class Preferences(Subject): else: self.initDefaultProfile() - def profiles(self): + def profiles(self, profileName=None): + if profileName: + return self._profiles[profileName] return self._profiles def profileNames(self): @@ -83,7 +85,7 @@ class Preferences(Subject): def saveToDict(self): return { 'version': VERSION, - 'profiles': dict([(name, profile.toDict()) for name, profile in self._profiles.items()]), + 'profiles': dict([(name, profile.saveToDict()) for name, profile in self._profiles.items()]), 'selectedProfile': self._selectedProfile } diff --git a/g13gui/g13gui/prefs_tests.py b/g13gui/g13gui/model/prefs_tests.py similarity index 95% rename from g13gui/g13gui/prefs_tests.py rename to g13gui/g13gui/model/prefs_tests.py index 99b35f9..03d546f 100644 --- a/g13gui/g13gui/prefs_tests.py +++ b/g13gui/g13gui/model/prefs_tests.py @@ -1,11 +1,11 @@ #!/usr/bin/python import unittest -import prefs +import g13gui.model.prefs as prefs -from common import VERSION -from observer import ChangeType -from observer import ObserverTestCase +from g13gui.common import VERSION +from g13gui.observer import ChangeType +from g13gui.observer import ObserverTestCase class PrefsTestCase(ObserverTestCase): diff --git a/g13gui/g13gui/observer/__init__.py b/g13gui/g13gui/observer/__init__.py new file mode 100644 index 0000000..bc2e7fc --- /dev/null +++ b/g13gui/g13gui/observer/__init__.py @@ -0,0 +1,5 @@ +from g13gui.observer.observer import Observer +from g13gui.observer.observer import Subject +from g13gui.observer.observer import ObserverTestCase +from g13gui.observer.observer import ChangeType +from g13gui.observer.gtkobserver import GtkObserver diff --git a/g13gui/g13gui/observer/gtkobserver.py b/g13gui/g13gui/observer/gtkobserver.py new file mode 100644 index 0000000..d66c1df --- /dev/null +++ b/g13gui/g13gui/observer/gtkobserver.py @@ -0,0 +1,53 @@ +import gi +import queue + +from g13gui.observer import Observer + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GObject + + +class GObjectObserverProxy(GObject.Object): + def __init__(self, owner): + GObject.Object.__init__(self) + self._owner = owner + + @GObject.Signal(name='subject-changed') + def subjectChanged(self): + self._owner._gtkSubjectChanged(self) + + +class GtkObserver(Observer): + def __init__(self): + """Constructor. Must be called by a subclass' constructor.""" + self._observerQueue = queue.Queue() + self._gobjectProxy = GObjectObserverProxy(self) + + def onSubjectChanged(self, subject, changeType, key, data=None): + """Original Observer signal handler. + + Runs on a (possibly) background thread to put the notification into a + queue, then signal to trampoline to the UI thread before handling the + notification. + """ + self._observerQueue.put((subject, changeType, key, data)) + self._gobjectProxy.emit("subject-changed") + + def _gtkSubjectChanged(self, widget): + """GObject 'subject-changed' signal handler. + + Runs on the UI thread, and pops the change notification off the queue + and processes it by way of the gtkSubjectChanged method that must be + overridden. + """ + (subject, changeType, key, data) = self._observerQueue.get() + self.gtkSubjectChanged(subject, changeType, key, data) + self._observerQueue.task_done() + + def gtkSubjectChanged(self, subject, changeType, key, data=None): + """Subject notification handler. + + Runs on the UI thread, and must be overridden by subclasses. + """ + raise NotImplementedError( + "%s did not override Observer#gtkSubjectChanged" % (type(self))) diff --git a/g13gui/g13gui/observer.py b/g13gui/g13gui/observer/observer.py similarity index 100% rename from g13gui/g13gui/observer.py rename to g13gui/g13gui/observer/observer.py diff --git a/g13gui/g13gui/observer_tests.py b/g13gui/g13gui/observer/observer_tests.py similarity index 100% rename from g13gui/g13gui/observer_tests.py rename to g13gui/g13gui/observer/observer_tests.py diff --git a/g13gui/g13gui/ui/__init__.py b/g13gui/g13gui/ui/__init__.py new file mode 100644 index 0000000..6e98c7a --- /dev/null +++ b/g13gui/g13gui/ui/__init__.py @@ -0,0 +1,6 @@ +from g13gui.ui.profilecombobox import ProfileComboBox +from g13gui.ui.g13button import G13Button +from g13gui.ui.g13buttonpopover import G13ButtonPopover +from g13gui.ui.mainwindow import MainWindow +from g13gui.ui.profilepopover import ProfilePopover +from g13gui.ui.profilepopover import ProfilePopoverMode diff --git a/g13gui/g13gui/ui/g13button.py b/g13gui/g13gui/ui/g13button.py new file mode 100644 index 0000000..b50f8ec --- /dev/null +++ b/g13gui/g13gui/ui/g13button.py @@ -0,0 +1,71 @@ +import gi + +import g13gui.ui as ui +from g13gui.observer import GtkObserver +from g13gui.model.bindings import G13ToGDK + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, GObject, Gdk + + +class G13Button(Gtk.MenuButton, GtkObserver): + def __init__(self, prefs, g13KeyName): + Gtk.MenuButton.__init__(self) + GtkObserver.__init__(self) + + self._prefs = prefs + self._prefs.registerObserver(self, {'selectedProfile'}) + self._keyName = g13KeyName + self._lastProfileName = None + + self._popover = ui.G13ButtonPopover(self, self._prefs, self._keyName) + self.set_popover(self._popover) + + _image = Gtk.Image.new_from_file(g13KeyName + '.png') + self.get_style_context().add_class('flat') + + self.set_can_default(False) + self.updateProfileRegistration() + self.updateBindingDisplay() + + def updateProfileRegistration(self): + if self._lastProfileName: + if self._lastProfileName in self._prefs.profileNames(): + lastProfile = self._prefs.profiles(self._lastProfileName) + lastProfile.removeObserver(self) + + self._prefs.selectedProfile().registerObserver(self, {self._keyName}) + + def _removeChild(self): + child = self.get_child() + if child: + self.remove(child) + + def updateBindingDisplay(self): + self._removeChild() + bindings = self._prefs.selectedProfile().keyBinding(self._keyName) + print('[%s %s] %s: %s' % (self._prefs.selectedProfileName(), + self._prefs.selectedProfile(), + self._keyName, bindings)) + + if len(bindings) > 0: + keybinds = G13ToGDK(bindings) + accelerator = '+'.join(keybinds) + shortcut = Gtk.ShortcutsShortcut( + shortcut_type=Gtk.ShortcutType.ACCELERATOR, + accelerator=accelerator) + shortcut.set_halign(Gtk.Align.CENTER) + self.add(shortcut) + else: + label = Gtk.Label(self._keyName) + self.add(label) + + self.show_all() + + def gtkSubjectChanged(self, subject, changeType, key, data=None): + if key == 'selectedProfile': + self.updateProfileRegistration() + self.updateBindingDisplay() + elif key == self._keyName: + self.updateBindingDisplay() diff --git a/g13gui/g13gui/buttonmenu.py b/g13gui/g13gui/ui/g13buttonpopover.py similarity index 55% rename from g13gui/g13gui/buttonmenu.py rename to g13gui/g13gui/ui/g13buttonpopover.py index e6abe5d..cd1c235 100644 --- a/g13gui/g13gui/buttonmenu.py +++ b/g13gui/g13gui/ui/g13buttonpopover.py @@ -1,43 +1,55 @@ -#!/usr/bin/python3 - 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 + gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') - -from gi.repository import Gtk -from gi.repository import Gdk - -from bindings import GDK_TO_G13D_KEYBINDS -from bindings import G13D_TO_GDK_KEYBINDS -from bindings import G13DKeyIsModifier +from gi.repository import Gtk, GObject, Gdk MAX_DELAY_BETWEEN_PRESSES_MILLIS = 250 -class ButtonMenu(Gtk.Popover): - def __init__(self, profile, buttonName): +class G13ButtonPopover(Gtk.Popover, GtkObserver): + def __init__(self, buttonOwner, prefs, keyName): Gtk.Popover.__init__(self) + GtkObserver.__init__(self) - self._profile = profile - self._buttonName = buttonName - self._currentBindings = self._profile.getBoundKey(buttonName) - self._bindingBox = None - self._modifiers = {} + self._prefs = prefs + self._prefs.registerObserver(self, {'selectedProfile'}) + self._keyName = keyName + + self._modifiers = set() self._consonantKey = None self._lastPressTime = 0 + self.set_relative_to(buttonOwner) + self.updateBinding() + self.build() + + def updateBinding(self): + selectedProfile = self._prefs.selectedProfile() + self._currentBindings = selectedProfile.keyBinding(self._keyName) + + def gtkSubjectChanged(self, subject, changeType, key, data=None): + self.updateBinding() + + def build(self): self._box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + self._box.set_border_width(6) self.add(self._box) label = Gtk.Label() - label.set_markup("" + buttonName + "") + label.set_markup("" + self._keyName + "") self._box.pack_start(label, True, True, 6) button = Gtk.Button(label="Clear Binding") + button.set_can_focus(False) self._box.pack_start(button, True, True, 6) - self._box.show_all() self.connect("key-press-event", self.keypress) @@ -46,21 +58,25 @@ class ButtonMenu(Gtk.Popover): self.connect("closed", self.closed) button.connect("pressed", self.clear) - self.rebuildBindingDisplay() + self.buildBindingDisplay() def shown(self, widget): - Gdk.keyboard_grab(self.get_window(), False, Gdk.CURRENT_TIME) + self.grab_add() def rebuildBindingDisplay(self): if self._bindingBox: self._box.remove(self._bindingBox) - self._bindingBox = Gtk.Box(spacing=0, orientation=Gtk.Orientation.VERTICAL) + self.buildBindingDisplay() + + def buildBindingDisplay(self): + self._bindingBox = Gtk.Box(spacing=0, + orientation=Gtk.Orientation.VERTICAL) self._box.pack_start(self._bindingBox, True, True, 6) self._box.reorder_child(self._bindingBox, 1) if len(self._currentBindings) > 0: - keybinds = [G13D_TO_GDK_KEYBINDS[binding] for binding in self._currentBindings] + keybinds = G13ToGDK(self._currentBindings) accelerator = '+'.join(keybinds) shortcut = Gtk.ShortcutsShortcut( shortcut_type=Gtk.ShortcutType.ACCELERATOR, @@ -75,43 +91,44 @@ class ButtonMenu(Gtk.Popover): self._bindingBox.show_all() def keypress(self, buttonMenu, eventKey): - print("Keypressed! %s, %s" % (eventKey.keyval, Gdk.keyval_name(eventKey.keyval))) - - if eventKey.time - self._lastPressTime > MAX_DELAY_BETWEEN_PRESSES_MILLIS: - self._modifiers = {} + pressDelta = eventKey.time - self._lastPressTime + if pressDelta > MAX_DELAY_BETWEEN_PRESSES_MILLIS: + 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' - binding = GDK_TO_G13D_KEYBINDS[binding] - print('Binding is %s' % (binding)) + + binding = GDKToG13(binding) if G13DKeyIsModifier(binding): - self._modifiers[binding] = True - print("Modifiers are now %s" % (repr(self._modifiers.keys()))) + self._modifiers.add(binding) else: self._consonantKey = binding self._lastPressTime = eventKey.time + return True def keyrelease(self, buttonMenu, eventKey): - self._currentBindings = [modifier for modifier in self._modifiers.keys()] + self._currentBindings = sorted(list(self._modifiers)) if self._consonantKey: - self._currentBindings = self._currentBindings + [self._consonantKey] - + self._currentBindings += [self._consonantKey] self.rebuildBindingDisplay() - print("Bindings are now %s" % (self._currentBindings)) + self.hide() + return True def clear(self, button): self._currentBindings = [] self.rebuildBindingDisplay() + self.hide() def closed(self, buttonMenu): - self._profile.bindKey(self._buttonName, self._currentBindings) + self.grab_remove() + self._prefs.selectedProfile().bindKey(self._keyName, + self._currentBindings) self.hide() diff --git a/g13gui/g13gui/ui/mainwindow.py b/g13gui/g13gui/ui/mainwindow.py new file mode 100644 index 0000000..9fdb167 --- /dev/null +++ b/g13gui/g13gui/ui/mainwindow.py @@ -0,0 +1,186 @@ +#!/usr/bin/python + +import gi + +import g13gui.ui as ui + +from g13gui.g13d import SaveTask +from g13gui.g13d import UploadTask +from g13gui.observer import GtkObserver + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, Gdk, GObject + + +class MainWindow(Gtk.Window, GtkObserver): + def __init__(self, workerQueue, prefs): + Gtk.Window.__init__(self) + GtkObserver.__init__(self) + + self.set_default_size(640, 480) + geometry = Gdk.Geometry() + geometry.max_width = 640 + geometry.max_height = 480 + self.set_geometry_hints(None, geometry, Gdk.WindowHints.MAX_SIZE) + + self._workerQueue = workerQueue + self._prefs = prefs + self._prefs.registerObserver(self, 'selectedProfile') + self._prefs.selectedProfile().registerObserver(self) + self._lastProfileName = self._prefs.selectedProfileName() + + self.setupHeaderBar() + + self._box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + self._box.set_border_width(6) + self.add(self._box) + + self._infoBar = Gtk.InfoBar() + self._infoBar.set_no_show_all(True) + self._infoBarLabel = Gtk.Label() + self._infoBar.get_content_area().add(self._infoBarLabel) + self._infoBarLabel.show() + self._box.add(self._infoBar) + + self.setupG13ButtonGrid() + + def gtkSubjectChanged(self, subject, changeType, key, data=None): + self._doUpload() + self._doSave() + + def setupHeaderBar(self): + self._headerBar = Gtk.HeaderBar() + self._headerBar.set_title("G13 Configurator") + self._headerBar.set_show_close_button(True) + + self._profileComboBox = ui.ProfileComboBox(self._prefs) + self._profileComboBox.connect('changed', self._profileChanged) + self._headerBar.add(self._profileComboBox) + + addProfileButton = Gtk.MenuButton.new() + addProfileButton.add(Gtk.Image.new_from_icon_name( + "document-new-symbolic", 1)) + addProfilePopover = ui.ProfilePopover(self._prefs, + mode=ui.ProfilePopoverMode.ADD) + addProfileButton.set_popover(addProfilePopover) + self._headerBar.add(addProfileButton) + + editProfileButton = Gtk.MenuButton.new() + editProfileButton.add( + Gtk.Image.new_from_icon_name('document-edit-symbolic', 1)) + editProfilePopover = ui.ProfilePopover(self._prefs, + mode=ui.ProfilePopoverMode.EDIT) + editProfileButton.set_popover(editProfilePopover) + self._headerBar.add(editProfileButton) + + self._uploadButton = Gtk.Button.new_from_icon_name( + "document-send-symbolic", 1) + self._uploadButton.connect("clicked", self.uploadClicked) + self._headerBar.add(self._uploadButton) + + Gtk.Window.set_titlebar(self, self._headerBar) + + @GObject.Signal(name='daemon-connection-changed', arg_types=(bool,)) + def daemonConnectionChanged(self, connected): + self._connected = connected + if connected: + self._uploadButton.set_state_flags(Gtk.StateFlags.NORMAL, True) + self._infoBar.hide() + self._doUpload() + else: + self._uploadButton.set_state_flags(Gtk.StateFlags.INSENSITIVE, + True) + self._infoBar.set_message_type(Gtk.MessageType.WARNING) + self._infoBarLabel.set_text( + 'The G13 user space driver is not running. ' + 'Attempting to reconnect.') + self._infoBar.show() + + @GObject.Signal(name='uploading', arg_types=(float,)) + def uploadStatusChanged(self, percentage): + if percentage < 1.0: + self._infoBar.set_message_type(Gtk.MessageType.INFO) + self._infoBarLabel.set_text('Uploading to the G13...') + self._infoBar.show() + else: + self._infoBar.hide() + + def _profileChanged(self, widget): + self._doUpload() + + def _doUpload(self): + config = self._prefs.selectedProfile().toCommandString() + task = UploadTask(config) + self._workerQueue.put(task) + + def _doSave(self): + task = SaveTask(self._prefs.saveToDict()) + self._workerQueue.put(task) + + def uploadClicked(self, widget): + self._doUpload() + self._doSave() + + def setupG13ButtonGrid(self): + self._mButtons = Gtk.ButtonBox( + spacing=3, + orientation=Gtk.Orientation.HORIZONTAL, + baseline_position=Gtk.BaselinePosition.CENTER) + self._mButtons.set_layout(Gtk.ButtonBoxStyle.CENTER) + self._box.pack_start(self._mButtons, False, False, 6) + + self._keyGrid = Gtk.Grid() + self._keyGrid.set_hexpand(False) + self._keyGrid.set_vexpand(False) + self._keyGrid.set_row_spacing(3) + self._keyGrid.set_column_spacing(3) + self._box.pack_start(self._keyGrid, False, False, 6) + + self._stickGrid = Gtk.Grid() + self._stickGrid.set_row_spacing(3) + self._stickGrid.set_column_spacing(3) + self._box.pack_start(self._stickGrid, False, False, 6) + + self._g13Buttons = {} + + self._mButtons.pack_start(self.newG13Button('M1'), False, False, 6) + self._mButtons.pack_start(self.newG13Button('M2'), False, False, 6) + self._mButtons.pack_start(self.newG13Button('M3'), False, False, 6) + + # G1 to G14 + self._buttonNum = 1 + for row in range(0, 2): + for col in range(0, 7): + self._keyGrid.attach(self.newG13NumberedButton(), + col, row, 1, 1) + + # G15 to G19 + self._keyGrid.attach(self.newG13NumberedButton(), 1, 3, 1, 1) + self._keyGrid.attach(self.newG13NumberedButton(), 2, 3, 1, 1) + self._keyGrid.attach(self.newG13NumberedButton(), 3, 3, 1, 1) + self._keyGrid.attach(self.newG13NumberedButton(), 4, 3, 1, 1) + self._keyGrid.attach(self.newG13NumberedButton(), 5, 3, 1, 1) + + # G20 to G22 + self._keyGrid.attach(self.newG13NumberedButton(), 2, 4, 1, 1) + self._keyGrid.attach(self.newG13NumberedButton(), 3, 4, 1, 1) + self._keyGrid.attach(self.newG13NumberedButton(), 4, 4, 1, 1) + + self._stickGrid.attach(self.newG13Button("STICK_UP"), 4, 0, 1, 1) + self._stickGrid.attach(self.newG13Button("LEFT"), 2, 1, 1, 1) + self._stickGrid.attach(self.newG13Button("STICK_LEFT"), 3, 1, 1, 1) + self._stickGrid.attach(self.newG13Button("TOP"), 4, 1, 1, 1) + self._stickGrid.attach(self.newG13Button("STICK_RIGHT"), 5, 1, 1, 1) + self._stickGrid.attach(self.newG13Button("STICK_DOWN"), 4, 2, 1, 1) + self._stickGrid.attach(self.newG13Button("DOWN"), 4, 3, 1, 1) + + def newG13NumberedButton(self): + button = self.newG13Button('G' + str(self._buttonNum)) + self._buttonNum = self._buttonNum + 1 + return button + + def newG13Button(self, name): + button = ui.G13Button(self._prefs, name) + self._g13Buttons[name] = button + return button diff --git a/g13gui/g13gui/ui/profilecombobox.py b/g13gui/g13gui/ui/profilecombobox.py new file mode 100644 index 0000000..ac21ebd --- /dev/null +++ b/g13gui/g13gui/ui/profilecombobox.py @@ -0,0 +1,59 @@ +import gi + +from g13gui.observer import GtkObserver +from g13gui.observer import ChangeType + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, GObject, Gdk + + +def AlphabeticalSort(model, a, b, userData): + print('AlphabeticalSort %s <=> %s' % (a, b)) + if a == b: + return 0 + if a < b: + return -1 + return 1 + + +class ProfileComboBox(Gtk.ComboBoxText, GtkObserver): + def __init__(self, prefs): + Gtk.ComboBoxText.__init__(self) + GtkObserver.__init__(self) + + self._prefs = prefs + self._prefs.registerObserver(self, {'profile'}) + self._isUpdating = False + + self._model = self.get_model() + self._model.set_sort_column_id(0, Gtk.SortType.ASCENDING) + self._model.set_default_sort_func(AlphabeticalSort) + self.connect("changed", self._profileChanged) + + self.update() + + def _profileChanged(self, widget): + selectedProfile = self.get_active_text() + print('Profile changed to %s' % selectedProfile) + if selectedProfile: + self._prefs.setSelectedProfile(selectedProfile) + + def update(self): + profiles = self._prefs.profileNames() + selected = self._prefs.selectedProfileName() + + self._model.clear() + row = 0 + + for name in profiles: + self._model.append([name, name]) + if name == selected: + self.set_active(row) + row = row + 1 + + def gtkSubjectChanged(self, subject, changeType, key, data=None): + name = list(data.keys())[0] + + if changeType == ChangeType.ADD: + self._model.append([name, name]) diff --git a/g13gui/g13gui/ui/profilepopover.py b/g13gui/g13gui/ui/profilepopover.py new file mode 100644 index 0000000..f88032e --- /dev/null +++ b/g13gui/g13gui/ui/profilepopover.py @@ -0,0 +1,129 @@ +import gi +import enum + +import g13gui.model.bindings as bindings +from g13gui.observer import GtkObserver +from g13gui.model import BindingProfile +from g13gui.model.bindings import G13ToGDK +from g13gui.model.bindings import GDKToG13 +from g13gui.model.bindings import G13DKeyIsModifier +from g13gui.model.bindings import StickMode +from g13gui.model.bindings import ALL_STICK_MODES + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, GObject, Gdk + + +class ProfilePopoverMode(enum.Enum): + EDIT = 'edit' + ADD = 'add' + + +class ProfilePopover(Gtk.Popover, GtkObserver): + def __init__(self, prefs, mode=ProfilePopoverMode.EDIT): + Gtk.Popover.__init__(self) + GtkObserver.__init__(self) + + self._prefs = prefs + self._mode = mode + self._lastRow = 0 + + self.build() + self.connect('show', self.shown) + + def updateFromPrefs(self): + self._profileName.set_text(self._prefs.selectedProfileName()) + + profile = self._prefs.selectedProfile() + lcdColor = profile.lcdColor() + self._lcdColorButton.set_rgba(Gdk.RGBA(*lcdColor, alpha=1.0)) + + stickMode = profile.stickMode() + activeIndex = sorted(list(ALL_STICK_MODES)).index(stickMode) + self._stickModeCombo.set_active(activeIndex) + + def commitToPrefs(self): + pass + + def addRow(self, widget, labelText=None): + if labelText: + label = Gtk.Label() + label.set_text(labelText) + self._grid.attach(label, 1, self._lastRow, 1, 1) + self._grid.attach(widget, 2, self._lastRow, 1, 1) + else: + self._grid.attach(widget, 1, self._lastRow, 2, 1) + self._lastRow += 1 + + def build(self): + self._grid = Gtk.Grid() + self._grid.set_row_spacing(6) + self._grid.set_column_spacing(10) + self._grid.set_border_width(6) + self.add(self._grid) + + self._profileName = Gtk.Entry() + self._profileName.set_can_focus(True) + self._profileName.set_activates_default(True) + self.addRow(self._profileName, 'Profile Name') + + self._lcdColorButton = Gtk.ColorButton() + self._lcdColorButton.set_use_alpha(False) + self._lcdColorButton.set_rgba(Gdk.RGBA(*bindings.DEFAULT_LCD_COLOR)) + self._lcdColorButton.set_title('LCD Color') + self.addRow(self._lcdColorButton, 'LCD Color') + + self._stickModeCombo = Gtk.ComboBoxText() + for mode in sorted(list(ALL_STICK_MODES)): + self._stickModeCombo.append_text(mode.capitalize()) + self._stickModeCombo.set_active(1) + self.addRow(self._stickModeCombo, 'Joystick Mode') + + commitButton = Gtk.Button() + commitButton.set_receives_default(True) + commitButton.set_can_default(True) + commitButton.connect('clicked', self.commitClicked) + + if self._mode == ProfilePopoverMode.EDIT: + commitButton.set_label('Update') + commitButton.get_style_context().add_class('suggested-action') + self.addRow(commitButton) + + removeButton = Gtk.Button() + removeButton.set_label('Remove') + removeButton.connect('clicked', self.removeClicked) + removeButton.get_style_context().add_class('destructive-action') + self.addRow(removeButton) + else: + commitButton.set_label('Add') + commitButton.get_style_context().add_class('suggested-action') + self.addRow(commitButton) + + self._grid.show_all() + + def commitClicked(self, widget): + lcdColor = self._lcdColorButton.get_rgba() + lcdColor = (lcdColor.red, lcdColor.green, lcdColor.blue) + profileName = self._profileName.get_text() + stickMode = self._stickModeCombo.get_active_text() + + profile = None + if self._mode == ProfilePopoverMode.ADD: + profile = BindingProfile() + self._prefs.addProfile(profileName, profile) + else: + profile = self._prefs.selectedProfile() + + profile.setLCDColor(*lcdColor) + profile.setStickMode(stickMode.upper()) + + self.hide() + + def removeClicked(self, widget): + pass + + def shown(self, widget): + self._profileName.grab_focus() + if self._mode == ProfilePopoverMode.EDIT: + self.updateFromPrefs()