diff --git a/g13gui/.gitignore b/g13gui/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/g13gui/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/g13gui/LICENSE.txt b/g13gui/LICENSE.txt new file mode 100644 index 0000000..12b9b11 --- /dev/null +++ b/g13gui/LICENSE.txt @@ -0,0 +1,18 @@ +Copyright (c) 2021, June Tate-Gans + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/g13gui/MANIFEST.in b/g13gui/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/g13gui/README.md b/g13gui/README.md new file mode 100644 index 0000000..0e7bea9 --- /dev/null +++ b/g13gui/README.md @@ -0,0 +1,20 @@ +The G13 Configurator +==================== + +## What is this? + +This is the companion application for configuring a Logitech G13 using the G13 +user space driver originally written by ecraven, and available at +https://github.com/jtgans/g13. + +This tool allows you to: + + - Graphically plan out a keymapping + - Save multiple profiles + - Use the LCD with pluggable dbus-based applications + - Switch profiles using the LCD + +All wrapped up in a glorious Gtk 3.0 + libappindicator interface. + +Please note: this is an early version of the application and as such it is still +in heavy development. diff --git a/g13gui/g13gui/__init__.py b/g13gui/g13gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/g13gui/g13gui/bindingprofile.py b/g13gui/g13gui/bindingprofile.py new file mode 100644 index 0000000..5b58cd8 --- /dev/null +++ b/g13gui/g13gui/bindingprofile.py @@ -0,0 +1,58 @@ +#!/usr/bin/python + +import bindings + + +class BindingProfile(object): + def __init__(self): + self._stickMode = bindings.GetStickModeNum('KEYS') + self._stickRegions = bindings.DEFAULT_STICK_REGIONS + self._stickRegionBindings = bindings.DEFAULT_STICK_REGION_BINDINGS + self._keyBindings = bindings.DEFAULT_KEY_BINDINGS + self._observers = [] + + def registerObserver(self, observer): + self._observers.append(observer) + + def getStickRegions(self): + return self._stickRegions + + def getBoundKey(self, gkey): + gkey = gkey.upper() + + if gkey in self._stickRegions.keys(): + if gkey in self._stickRegionBindings.keys(): + return self._stickRegionBindings[gkey] + + if gkey in self._keyBindings.keys(): + return self._keyBindings[gkey] + + return [] + + def bindKey(self, gkey, keybinding): + if gkey in self._stickRegions.keys(): + self._stickRegionBindings[gkey] = keybinding + return + + self._keyBindings[gkey] = keybinding + self._notify() + + def _notify(self): + for observer in self._observers: + observer.on_changed(self) + + def generateConfigString(self): + commands = [] + + for gkey, kbdkey in self._keyBindings.items(): + keys = ' '.join(['KEY_' + key for key in kbdkey]) + commands.append("bind %s %s" % (gkey, keys)) + + if self._stickMode == bindings.GetStickModeNum('KEYS'): + for region, bounds in self._stickRegions.items(): + commands.append("stickzone add %s" % (region)) + commands.append("stickzone bounds %s %0.1f %0.1f %0.1f %0.1f" % (region, bounds[0], bounds[1], bounds[2], bounds[3])) + keys = ' '.join(['KEY_' + key for key in self._stickRegionBindings[region]]) + commands.append("stickzone action %s %s" % (region, keys)) + + return '\n'.join(commands) diff --git a/g13gui/g13gui/bindings.py b/g13gui/g13gui/bindings.py new file mode 100644 index 0000000..ca11c52 --- /dev/null +++ b/g13gui/g13gui/bindings.py @@ -0,0 +1,203 @@ +#!/usr/bin/python3 + +G13D_TO_GDK_KEYBINDS = { + '0': '0', + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + '6': '6', + '7': '7', + '8': '8', + '9': '9', + 'A': 'A', + 'B': 'B', + 'C': 'C', + 'D': 'D', + 'E': 'E', + 'F': 'F', + 'G': 'G', + 'H': 'H', + 'I': 'I', + 'J': 'J', + 'K': 'K', + 'L': 'L', + 'M': 'M', + 'N': 'N', + 'O': 'O', + 'P': 'P', + 'Q': 'Q', + 'R': 'R', + 'S': 'S', + 'T': 'T', + 'U': 'U', + 'V': 'V', + 'W': 'W', + 'X': 'X', + 'Y': 'Y', + 'Z': 'Z', + + 'LEFT': 'Left', + 'RIGHT': 'Right', + 'UP': 'Up', + 'DOWN': 'Down', + + 'APOSTROPHE': 'apostrophe', + 'BACKSLASH': 'backslash', + 'BACKSPACE': 'backspace', + 'CAPSLOCK': 'capslock', + 'COMMA': 'comma', + 'DOT': 'period', + 'ENTER': 'enter', + 'EQUAL': 'equals', + 'ESC': 'Escape', + 'F1': 'F1', + 'F2': 'F2', + 'F3': 'F3', + 'F4': 'F4', + 'F5': 'F5', + 'F6': 'F6', + 'F7': 'F7', + 'F8': 'F8', + 'F9': 'F9', + 'F10': 'F10', + 'F11': 'F11', + 'F12': 'F12', + 'GRAVE': 'grave', + + 'INSERT': 'insert', + 'HOME': 'home', + 'PAGEUP': 'pageup', + 'DELETE': 'delete', + 'END': 'end', + 'PAGEDOWN': 'pagedown', + + 'NUMLOCK': 'numlock', + 'KPASTERISK': 'kpasterisk', + 'KPMINUS': '0', + 'KP7': '0', + 'KP8': '0', + 'KP9': '0', + 'KPPLUS': '0', + 'KP4': '0', + 'KP5': '0', + 'KP6': '0', + 'KP1': '0', + 'KP2': '0', + 'KP3': '0', + 'KP0': '0', + 'KPDOT': '0', + + 'LEFTBRACE': 'braceleft', + 'RIGHTBRACE': 'braceright', + 'MINUS': 'minus', + 'SEMICOLON': 'semicolon', + 'SLASH': 'slash', + 'SPACE': 'space', + 'TAB': 'Tab', + + 'LEFTALT': 'Alt_L', + 'LEFTCTRL': 'Control_L', + 'LEFTSHIFT': 'Shift_L', + 'RIGHTALT': 'Alt_R', + 'RIGHTCTRL': 'Control_R', + 'RIGHTSHIFT': 'Shift_R', + 'SCROLLLOCK': 'ScrollLock', +} + +GDK_TO_G13D_KEYBINDS = {} +for g13d_key, gdk_key in G13D_TO_GDK_KEYBINDS.items(): + GDK_TO_G13D_KEYBINDS[gdk_key] = g13d_key + +GDK_TO_G13D_KEYBINDS['asciitilde'] = 'GRAVE' +GDK_TO_G13D_KEYBINDS['braceleft'] = 'LEFTBRACE' +GDK_TO_G13D_KEYBINDS['braceright'] = 'RIGHTBRACE' +GDK_TO_G13D_KEYBINDS['bracketleft'] = 'LEFTBRACE' +GDK_TO_G13D_KEYBINDS['bracketright'] = 'RIGHTBRACE' +GDK_TO_G13D_KEYBINDS['quotedbl'] = 'APOSTROPHE' +GDK_TO_G13D_KEYBINDS['less'] = 'COMMA' +GDK_TO_G13D_KEYBINDS['greater'] = 'DOT' +GDK_TO_G13D_KEYBINDS['bar'] = 'BACKSLASH' +GDK_TO_G13D_KEYBINDS['question'] = 'SLASH' +GDK_TO_G13D_KEYBINDS['colon'] = 'SEMICOLON' +GDK_TO_G13D_KEYBINDS['plus'] = 'EQUALS' +GDK_TO_G13D_KEYBINDS['exclam'] = '1' +GDK_TO_G13D_KEYBINDS['at'] = '2' +GDK_TO_G13D_KEYBINDS['numbersign'] = '3' +GDK_TO_G13D_KEYBINDS['dollar'] = '4' +GDK_TO_G13D_KEYBINDS['percent'] = '5' +GDK_TO_G13D_KEYBINDS['asciicircum'] = '6' +GDK_TO_G13D_KEYBINDS['ampersand'] = '7' +GDK_TO_G13D_KEYBINDS['asterisk'] = '8' +GDK_TO_G13D_KEYBINDS['parenleft'] = '9' +GDK_TO_G13D_KEYBINDS['parenright'] = '0' +GDK_TO_G13D_KEYBINDS['ISO_Left_Tab'] = 'TAB' + +G13_KEYS = [ + 'BD', 'L1', 'L2', 'L3', 'L4', 'LIGHT', + 'M1', 'M2', 'M3', 'MR', + 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', + 'G8', 'G9', 'G10', 'G11', 'G12', 'G13', 'G14', + 'G15', 'G16', 'G17', 'G18', 'G19', + 'G20', 'G21', 'G22', + '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'], + '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'], +} + +DEFAULT_STICK_REGION_BINDINGS = { + 'STICK_UP': ['W'], + 'STICK_DOWN': ['S'], + 'STICK_LEFT': ['A'], + 'STICK_RIGHT': ['D'] +} + +STICK_MODES = [ + 'ABSOLUTE', + 'RELATIVE', + 'KEYS' +] + + +def GetStickModeNum(modeName): + return STICK_MODES.index(modeName.upper()) + + +def G13DKeyIsModifier(key): + key = key.upper() + return (key == 'LEFTSHIFT' or key == 'RIGHTSHIFT' or + key == 'LEFTALT' or key == 'RIGHTALT' or + key == 'LEFTCTRL' or key == 'RIGHTCTRL') diff --git a/g13gui/g13gui/buttonmenu.py b/g13gui/g13gui/buttonmenu.py new file mode 100644 index 0000000..d6362e5 --- /dev/null +++ b/g13gui/g13gui/buttonmenu.py @@ -0,0 +1,113 @@ +#!/usr/bin/python3 + +import gi + +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 + + +MAX_DELAY_BETWEEN_PRESSES_MILLIS = 250 + + +class ButtonMenu(Gtk.Popover): + def __init__(self, profile, buttonName): + Gtk.Popover.__init__(self) + + self._profile = profile + self._buttonName = buttonName + self._currentBindings = self._profile.getBoundKey(buttonName) + self._bindingBox = None + self._modifiers = {} + self._consonantKey = None + self._lastPressTime = 0 + + self._box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + self.add(self._box) + + label = Gtk.Label() + label.set_markup("" + buttonName + "") + self._box.pack_start(label, True, True, 6) + + button = Gtk.Button(label="Clear Binding") + 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("closed", self.closed) + button.connect("pressed", self.clear) + + self.rebuildBindingDisplay() + + def rebuildBindingDisplay(self): + if self._bindingBox: + self._box.remove(self._bindingBox) + + 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] + 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) + else: + label = Gtk.Label() + label.set_markup("No binding. Press a key to bind.") + self._bindingBox.add(label) + + 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 = {} + 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)) + + if G13DKeyIsModifier(binding): + self._modifiers[binding] = True + print("Modifiers are now %s" % (repr(self._modifiers.keys()))) + else: + self._consonantKey = binding + + self._lastPressTime = eventKey.time + + def keyrelease(self, buttonMenu, eventKey): + self._currentBindings = [modifier for modifier in self._modifiers.keys()] + if self._consonantKey: + self._currentBindings = self._currentBindings + [self._consonantKey] + + self.rebuildBindingDisplay() + print("Bindings are now %s" % (self._currentBindings)) + + def clear(self, button): + self._currentBindings = [] + self.rebuildBindingDisplay() + + def closed(self, buttonMenu): + self._profile.bindKey(self._buttonName, self._currentBindings) + self.hide() diff --git a/g13gui/g13gui/main.py b/g13gui/g13gui/main.py new file mode 100644 index 0000000..668eec1 --- /dev/null +++ b/g13gui/g13gui/main.py @@ -0,0 +1,15 @@ +#!/usr/bin/python + +import gi + +gi.require_version('Gtk', '3.0') +gi.require_version('Notify', '0.7') + +from gi.repository import Gtk +from mainwindow import MainWindow + +if __name__ == '__main__': + win = MainWindow() + win.connect("destroy", Gtk.main_quit) + win.show_all() + Gtk.main() diff --git a/g13gui/g13gui/mainwindow.py b/g13gui/g13gui/mainwindow.py new file mode 100644 index 0000000..6554b04 --- /dev/null +++ b/g13gui/g13gui/mainwindow.py @@ -0,0 +1,152 @@ +#!/usr/bin/python + +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +from bindings import G13D_TO_GDK_KEYBINDS +from bindings import G13_KEYS +from bindingprofile import BindingProfile +from buttonmenu import ButtonMenu + +class MainWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self) + + default_profile = BindingProfile() + default_profile.registerObserver(self) + self._profiles = {'Default Profile': default_profile} + self._currentProfile = self._profiles['Default Profile'] + + self.headerBar = Gtk.HeaderBar() + self.headerBar.set_title("G13 Configurator") + self.headerBar.set_show_close_button(True) + + self.profileComboBox = Gtk.ComboBoxText() + self.headerBar.add(self.profileComboBox) + addProfileButton = Gtk.Button.new_from_icon_name("add", 1) + self.headerBar.add(addProfileButton) + + 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 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) + + print("Profile updated to:") + print(self._currentProfile.generateConfigString()) diff --git a/g13gui/setup.py b/g13gui/setup.py new file mode 100755 index 0000000..2ccb5c5 --- /dev/null +++ b/g13gui/setup.py @@ -0,0 +1,40 @@ +from setuptools import setup, find_packages +from os import path +from io import open + +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='g13gui', + version='0.1.0', + description='A Gtk 3 application to configure the Logitech G13 gameboard', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/jtgans/g13', + author='June Tate-Gans', + author_email='june@theonelab.com', + license='MIT', + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Topic :: Utilities', + 'License :: OSI Approved :: MIT', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 3', + ], + keywords='gaming', + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + python_requires='>=3.5.0', + install_requires=[ + 'gi', + ], + package_data={ + }, + entry_points={ + 'console_scripts': [ + 'g13gui=g13gui.main:main', + ], + }, +)