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()