mirror of
https://github.com/jtgans/g13gui.git
synced 2025-06-20 00:14:09 -04:00
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:
parent
cdc8fe5139
commit
bac31a772a
@ -4,11 +4,18 @@ 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
|
||||
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():
|
||||
keys = ' '.join(['KEY_' + key for key in kbdkey])
|
||||
commands.append("bind %s %s" % (gkey, keys))
|
||||
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
|
||||
}
|
||||
|
@ -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
9
g13gui/g13gui/common.py
Normal 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
116
g13gui/g13gui/g13d.py
Normal 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
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user