g13gui: Major functionality additions

This gets us nearly to a proper profile manager and keybinding tool! Very very
close, despite the messiness of the codebase. There's lots of low hanging fruit
for those who are interested in contributing.

  - Made BindingProfile serializable to a python dict
  - Fixed a bug in BindingProfile that named keys in the g13d command stream
    incorrectly.
  - Made ButtonMenu attempt to grab the keyboard when it shows so we can get
    more correct keypresses. This is only half the battle -- need to stop GTK's
    event loop for other widgets from catching events and handling them.
  - Added a g13d communications worker thread, including saving of profiles to
    disk as well as uploading profile configurations to g13d. This uses a Queue
    for incoming tasks from the GUI thread, and dispatches results back by way
    of GObject signals. Could use some work to make this less clunky.
  - Made MainWindow load the profiles from disk. This is done on the GUI thread,
    which isn't ideal.
  - Added an Upload button to push changes over to g13d. This reacts to the
    worker thread's connected/disconnected signals.
This commit is contained in:
June Tate-Gans 2021-04-27 00:00:16 -05:00
parent cdc8fe5139
commit bac31a772a
6 changed files with 256 additions and 16 deletions

View File

@ -4,11 +4,18 @@ import bindings
class BindingProfile(object):
def __init__(self):
def __init__(self, dict=None):
if dict:
self._stickMode = dict['stickMode']
self._stickRegions = dict['stickRegions']
self._stickRegionBindings = dict['stickRegionBindings']
self._keyBindings = dict['keyBindings']
else:
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):
@ -45,8 +52,11 @@ class BindingProfile(object):
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))
if self._stickMode == bindings.GetStickModeNum('KEYS'):
for region, bounds in self._stickRegions.items():
@ -56,3 +66,11 @@ class BindingProfile(object):
commands.append("stickzone action %s %s" % (region, keys))
return '\n'.join(commands)
def toDict(self):
return {
'stickMode': self._stickMode,
'stickRegions': self._stickRegions,
'stickRegionBindings': self._stickRegionBindings,
'keyBindings': self._keyBindings
}

View File

@ -42,11 +42,15 @@ class ButtonMenu(Gtk.Popover):
self.connect("key-press-event", self.keypress)
self.connect("key-release-event", self.keyrelease)
self.connect("show", self.shown)
self.connect("closed", self.closed)
button.connect("pressed", self.clear)
self.rebuildBindingDisplay()
def shown(self, widget):
Gdk.keyboard_grab(self.get_window(), False, Gdk.CURRENT_TIME)
def rebuildBindingDisplay(self):
if self._bindingBox:
self._box.remove(self._bindingBox)

9
g13gui/g13gui/common.py Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/python
import os
import os.path
import xdg.BaseDirectory as basedir
VERSION = '0.1.0'
PROFILES_CONFIG_PATH = os.path.join(basedir.save_config_path('g13', 'g13gui'),
'profiles.json')

116
g13gui/g13gui/g13d.py Normal file
View File

@ -0,0 +1,116 @@
#!/usr/bin/python3
import gi
import os
import os.path
import threading
import time
import traceback
import xdg.BaseDirectory as basedir
import json
from common import PROFILES_CONFIG_PATH
from common import VERSION
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
class UploadTask():
def __init__(self, commands):
self._commands = str.encode(commands)
def run(self, outfp, infp, callback):
bytes_written = 0
while bytes_written < len(self._commands):
result = os.write(outfp, self._commands[bytes_written:])
if result > 0:
bytes_written = result + bytes_written
callback(bytes_written / len(self._commands))
callback(1.0)
class SaveTask():
def __init__(self, profiles, defaultProfileName):
self._profiles = profiles
self._defaultProfileName = defaultProfileName
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.flush()
G13D_IN_FIFO = "/run/g13d/in"
G13D_OUT_FIFO = "/run/g13d/out"
class G13DWorker(threading.Thread):
def __init__(self, q, window):
threading.Thread.__init__(self, daemon=True)
self._mainWindow = window
self._queue = q
self._connected = False
def _connect(self):
try:
self._outfp = os.open(G13D_IN_FIFO, os.O_WRONLY)
self._infp = os.open(G13D_OUT_FIFO, os.O_RDONLY)
except FileNotFoundError:
self._outfp = None
self._infp = None
self._connected = False
print("g13d is not running, or not listening on %s" % (G13D_IN_FIFO))
self._mainWindow.emit("daemon-connection-changed", False)
print("Sleeping for 10 seconds...")
time.sleep(10)
except Exception as err:
self._outfp = None
self._infp = None
self._connected = False
print("Unknown exception occurred: %s %s" % (type(err), err))
self._mainWindow.emit("daemon-connection-changed", False)
print("Sleeping for 10 seconds...")
time.sleep(10)
else:
self._mainWindow.emit("daemon-connection-changed", True)
print("Connected to g13d")
self._connected = True
def run(self):
while True:
while not self._connected:
self._connect()
item = self._queue.get()
try:
item.run(self._outfp, self._infp, self.callback)
except BrokenPipeError as err:
print("g13d connection broken: %s" % (err))
self._connected = False
except Exception as err:
traceback.print_exc()
finally:
self._queue.task_done()
def callback(self, percentage):
self._mainWindow.emit("uploading", percentage)
def getQueue(self):
return self._queue

View File

@ -1,15 +1,26 @@
#!/usr/bin/python
import queue
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Notify', '0.7')
from gi.repository import Gtk
from gi.repository import Gtk, GObject
from mainwindow import MainWindow
from g13d import G13DWorker
VERSION = '0.1.0'
if __name__ == '__main__':
win = MainWindow()
queue = queue.Queue()
win = MainWindow(queue)
win.connect("destroy", Gtk.main_quit)
win.show_all()
worker = G13DWorker(queue, win)
worker.start()
Gtk.main()

View File

@ -1,34 +1,58 @@
#!/usr/bin/python
import gi
import json
import traceback
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
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):
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)
@ -38,6 +62,65 @@ class MainWindow(Gtk.Window):
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
@ -148,5 +231,4 @@ class MainWindow(Gtk.Window):
for key in G13_KEYS:
self.updateG13Button(key)
print("Profile updated to:")
print(self._currentProfile.generateConfigString())
self.uploadClicked(self)