1
0
mirror of https://github.com/jtgans/g13gui.git synced 2025-07-09 09:27:25 -04:00

g13gui: First rough draft

This represents a full day of work just to get the binding behaviors correct and
the profile behaviors correct. At this point, this should be possible to turn
into something useful for controlling g13d with.

Things left to do:

  - Setup a thread for communicating with g13d with
  - Setup a worker queue to send profile changes or updates to the g13d daemon

Once those are in there, we can consider this to be feature complete for 1.0.
Additional functionality, such as LED colors, drawing to the LCD, and things
like supporting dbus IPC so we can handle applets on the LCD will come later.
This commit is contained in:
June Tate-Gans 2021-04-26 19:13:19 -05:00
parent e97cb2c6c3
commit cdc8fe5139
11 changed files with 620 additions and 0 deletions

1
g13gui/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

18
g13gui/LICENSE.txt Normal file
View File

@ -0,0 +1,18 @@
Copyright (c) 2021, June Tate-Gans <june@theonelab.com>
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.

0
g13gui/MANIFEST.in Normal file
View File

20
g13gui/README.md Normal file
View File

@ -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.

View File

View File

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

203
g13gui/g13gui/bindings.py Normal file
View File

@ -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')

113
g13gui/g13gui/buttonmenu.py Normal file
View File

@ -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("<b>" + buttonName + "</b>")
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("<i>No binding. Press a key to bind.</i>")
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()

15
g13gui/g13gui/main.py Normal file
View File

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

152
g13gui/g13gui/mainwindow.py Normal file
View File

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

40
g13gui/setup.py Executable file
View File

@ -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',
],
},
)