From eccd9d60e6b5d96344ed8c65f224776f445edb55 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 19 Nov 2024 20:17:55 +0800 Subject: [PATCH 01/17] python: Implement tkinter GUI Signed-off-by: Daniel Schaefer --- python/pyproject.toml | 1 + python/qmk_hid/gui.py | 134 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 68fe1b9..9698e28 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -35,6 +35,7 @@ Source = "https://github.com/FrameworkComputer/qmk_hid" [project.gui-scripts] qmk_gui = "qmk_hid.gui:main" +qmk_tk = "qmk_hid.gui:tk_main" #[tool.hatch.version] #source = "vcs" diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index afbd4d6..03ab68d 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -4,7 +4,10 @@ import subprocess import time -import PySimpleGUI as sg +#import PySimpleGUI as sg +import tkinter as tk +from tkinter import ttk, messagebox + import hid if os.name == 'nt': from win32api import GetKeyState, keybd_event @@ -137,6 +140,102 @@ def get_numlock_state(): # Ignore tool not found, just return None pass +def tk_main(): + devices = find_devs(show=False, verbose=False) + # print("Found {} devices".format(len(devices))) + + # TODO: Implement for tkinter + # Only in the pyinstaller bundle are the FW update binaries included + #if is_pyinstaller(): + # releases = find_releases() + # versions = sorted(list(releases.keys()), reverse=True) + # + # bundled_update = [ + # [sg.Text("Update Version")], + # [sg.Text("Version"), sg.Push(), sg.Combo(versions, k='-VERSION-', enable_events=True, default_value=versions[0])], + # [sg.Text("Type"), sg.Push(), sg.Combo(list(releases[versions[0]]), k='-TYPE-', enable_events=True)], + # [sg.Text("Make sure the firmware is compatible with\nALL selected devices!")], + # [sg.Button("Flash", k='-FLASH-', disabled=True)], + # [sg.HorizontalSeparator()], + # ] + #else: + # bundled_update = [] + + root = tk.Tk() + root.title("QMK GUI") + + tabControl = ttk.Notebook(root) + tab1 = ttk.Frame(tabControl) + tab2 = ttk.Frame(tabControl) + tabControl.add(tab1, text="Home") + tabControl.add(tab2, text="Advanced") + tabControl.pack(expand=1, fill="both") + + # Device Checkboxes + detected_devices_frame = ttk.LabelFrame(tab1, text="Detected Devices", style="TLabelframe") + detected_devices_frame.pack(fill="x", padx=10, pady=5) + + global device_checkboxes + device_checkboxes = {} + for dev in devices: + device_info = "{}\nSerial No: {}\nFW Version: {}\n".format( + dev['product_string'], + dev['serial_number'], + format_fw_ver(dev['release_number']) + ) + checkbox_var = tk.BooleanVar(value=True) + checkbox = ttk.Checkbutton(detected_devices_frame, text=device_info, variable=checkbox_var, style="TCheckbutton") + checkbox.pack(anchor="w") + device_checkboxes[dev['path']] = checkbox_var + + # Device Control Buttons + device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") + device_control_frame.pack(fill="x", padx=10, pady=5) + control_buttons = { + "Bootloader": "bootloader", + "Save Changes": "save_changes", + } + for text, action in control_buttons.items(): + ttk.Button(device_control_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) + + # Brightness Slider + brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe") + brightness_frame.pack(fill="x", padx=10, pady=5) + global brightness_scale + brightness_scale = tk.Scale(brightness_frame, from_=0, to=255, orient='horizontal', command=lambda value: perform_action(devices, 'brightness', value=int(value))) + brightness_scale.set(120) # Default value + brightness_scale.pack(fill="x", padx=5, pady=5) + + # RGB color + rgb_color_buttons = { + "Red": "red", + "Green": "green", + "Blue": "blue", + "White": "white", + "Off": "off", + } + btn_frame = ttk.Frame(brightness_frame) + btn_frame.pack(side=tk.TOP) + for text, action in rgb_color_buttons.items(): + btn = ttk.Button(btn_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton") + btn.pack(side="left", padx=5, pady=5) + + # RGB Effect Combo Box + rgb_effect_label = tk.Label(brightness_frame, text="RGB Effect") + rgb_effect_label.pack(side=tk.LEFT, padx=5, pady=5) + rgb_effect_combo = ttk.Combobox(brightness_frame, values=RGB_EFFECTS, style="TCombobox", state="readonly") + rgb_effect_combo.pack(side=tk.LEFT, padx=5, pady=5) + rgb_effect_combo.bind("<>", lambda event: perform_action(devices, 'rgb_effect', value=RGB_EFFECTS.index(rgb_effect_combo.get()))) + + # Tab 2 + # TODO: Add numlock + # TODO: Add BIOS mode, factory mode buttons + # TODO: Add registry controls, maybe hidden behind secret shortbut + + program_ver_label = tk.Label(tab1, text="Program Version: 0.2.0") + program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) + + root.mainloop() def main(): devices = find_devs(show=False, verbose=False) @@ -620,6 +719,11 @@ def set_rgb_brightness(dev, brightness): def set_brightness(dev, brightness): set_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS, brightness) +# Set both +def set_white_rgb_brightness(dev, brightness): + set_brightness(dev, brightness) + set_rgb_brightness(dev, brightness) + def set_rgb_color(dev, hue, saturation): (cur_hue, cur_sat) = get_rgb_color(dev) @@ -736,5 +840,33 @@ def selective_suspend_registry(pid, verbose, set=None): except EnvironmentError as e: raise e +def perform_action(devices, action, value=None): + action_map = { + "bootloader": bootloader_jump, + "save_changes": save, + # TODO: factory_mode, bios_mode + "eeprom_reset": eeprom_reset, + "red": lambda dev: set_rgb_color(dev, RED_HUE, 255), + "green": lambda dev: set_rgb_color(dev, GREEN_HUE, 255), + "blue": lambda dev: set_rgb_color(dev, BLUE_HUE, 255), + "white": lambda dev: set_rgb_color(dev, None, 0), + # TODO: Also window['-BRIGHTNESS-'].Update(0) + "off": lambda dev: set_rgb_brightness(dev, 0), + "brightness": lambda dev: set_white_rgb_brightness(dev, value), + "rgb_effect": lambda dev: set_rgb_u8(dev, RGB_MATRIX_VALUE_EFFECT, value), + } + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + if action in action_map: + action_map[action](dev) + +def get_selected_devices(devices): + return [dev for dev in devices if dev['path'] in device_checkboxes and device_checkboxes[dev['path']].get()] + +def set_pattern(devices, pattern_name): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + pattern(dev, pattern_name) + if __name__ == "__main__": main() From 348a938a8e5dc303ec829edb586d853fd94d4485 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 19 Nov 2024 20:46:26 +0800 Subject: [PATCH 02/17] python: Add advanced page in tkinter Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 03ab68d..be0151b 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -229,8 +229,21 @@ def tk_main(): # Tab 2 # TODO: Add numlock - # TODO: Add BIOS mode, factory mode buttons # TODO: Add registry controls, maybe hidden behind secret shortbut + # Advanced Device Control Buttons + eeprom_frame = ttk.LabelFrame(tab2, text="EEPROM", style="TLabelframe") + eeprom_frame.pack(fill="x", padx=5, pady=5) + ttk.Button(eeprom_frame, text="Reset EEPROM", command=lambda: perform_action(devices, 'reset_eeprom'), style="TButton").pack(side="left", padx=5, pady=5) + + bios_mode_frame = ttk.LabelFrame(tab2, text="BIOS Mode", style="TLabelframe") + bios_mode_frame.pack(fill="x", padx=5, pady=5) + ttk.Button(bios_mode_frame, text="Enable", command=lambda: perform_action(devices, 'bios_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(bios_mode_frame, text="Disable", command=lambda: perform_action(devices, 'bios_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) + + factory_mode_frame = ttk.LabelFrame(tab2, text="Factory Mode", style="TLabelframe") + factory_mode_frame.pack(fill="x", padx=5, pady=5) + ttk.Button(factory_mode_frame, text="Enable", command=lambda: perform_action(devices, 'factory_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(factory_mode_frame, text="Disable", command=lambda: perform_action(devices, 'factory_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) program_ver_label = tk.Label(tab1, text="Program Version: 0.2.0") program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) @@ -842,10 +855,12 @@ def selective_suspend_registry(pid, verbose, set=None): def perform_action(devices, action, value=None): action_map = { + # TODO: Show restart_hint and disable checkbox "bootloader": bootloader_jump, "save_changes": save, - # TODO: factory_mode, bios_mode "eeprom_reset": eeprom_reset, + "bios_mode": lambda dev: bios_mode(dev, value), + "factory_mode": lambda dev: factory_mode(dev, value), "red": lambda dev: set_rgb_color(dev, RED_HUE, 255), "green": lambda dev: set_rgb_color(dev, GREEN_HUE, 255), "blue": lambda dev: set_rgb_color(dev, BLUE_HUE, 255), From 8fc28219742e3e221b337195bec5eafc613cd94e Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 19 Nov 2024 21:21:58 +0800 Subject: [PATCH 03/17] python: Implement tkinter message boxes Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index be0151b..d428176 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -747,13 +747,22 @@ def set_rgb_color(dev, hue, saturation): def restart_hint(): - sg.Popup('After updating a device, \nrestart the application\nto reload the connections.') + parent = tk.Tk() + parent.title("Restart Application") + message = tk.Message(parent, text="After updating a device,\n restart the application to reload the connections.", width=800) + message.pack(padx=20, pady=20) + parent.mainloop() def replug_hint(): - sg.Popup('After changing selective suspend setting, make sure to unplug and re-plug the device to apply the settings.') + parent = tk.Tk() + parent.title("Replug Keyboard") + message = tk.Message(parent, text="After changing selective suspend setting, make sure to unplug and re-plug the device to apply the settings.", width=800) + message.pack(padx=20, pady=20) + parent.mainloop() +# TODO: Show restart_hint() and deselect checkbox def flash_firmware(dev, fw_path): print(f"Flashing {fw_path}") @@ -790,6 +799,7 @@ def flash_firmware(dev, fw_path): print("Flashing finished") +# TODO: Show replug_hint() def selective_suspend_registry(pid, verbose, set=None): # The set of keys we care about (under HKEY_LOCAL_MACHINE) are # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013\Device Parameters\SelectiveSuspendEnabled @@ -854,9 +864,12 @@ def selective_suspend_registry(pid, verbose, set=None): raise e def perform_action(devices, action, value=None): + if action == "bootloader": + # TODO: Disable checkbox of that device + restart_hint() + action_map = { - # TODO: Show restart_hint and disable checkbox - "bootloader": bootloader_jump, + "bootloader": lambda dev: bootloader_jump(dev), "save_changes": save, "eeprom_reset": eeprom_reset, "bios_mode": lambda dev: bios_mode(dev, value), From a7847e3c674ac74a2cef583195d91ab40003b757 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 11:27:01 +0800 Subject: [PATCH 04/17] python: Implement tkinter numlock for windows only Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index d428176..5d85fa4 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -131,6 +131,8 @@ def get_numlock_state(): return GetKeyState(VK_NUMLOCK) else: try: + # TODO: This doesn't work on wayland + # In GNOME we can do gsettings set org.gnome.settings-daemon.peripherals.keyboard numlock-state on output = subprocess.run(['numlockx', 'status'], stdout=subprocess.PIPE).stdout if b'on' in output: return True @@ -228,7 +230,6 @@ def tk_main(): rgb_effect_combo.bind("<>", lambda event: perform_action(devices, 'rgb_effect', value=RGB_EFFECTS.index(rgb_effect_combo.get()))) # Tab 2 - # TODO: Add numlock # TODO: Add registry controls, maybe hidden behind secret shortbut # Advanced Device Control Buttons eeprom_frame = ttk.LabelFrame(tab2, text="EEPROM", style="TLabelframe") @@ -245,11 +246,38 @@ def tk_main(): ttk.Button(factory_mode_frame, text="Enable", command=lambda: perform_action(devices, 'factory_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) ttk.Button(factory_mode_frame, text="Disable", command=lambda: perform_action(devices, 'factory_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) + # Unreliable on Linux + # Different versions of numlockx behave differently + # Xorg vs Wayland is different + if os.name == 'nt': + numlock_frame = ttk.LabelFrame(tab2, text="OS Numlock Setting", style="TLabelframe") + numlock_frame.pack(fill="x", padx=5, pady=5) + numlock_state_var = tk.StringVar() + numlock_state_var.set("State: Unknown") + numlock_state_label = tk.Label(numlock_frame, textvariable=numlock_state_var).pack(side="top", padx=5, pady=5) + refresh_btn = ttk.Button(numlock_frame, text="Refresh", command=lambda: update_numlock_state(numlock_state_var), style="TButton", state=tk.DISABLED) + refresh_btn.pack(side="left", padx=5, pady=5) + toggle_btn = ttk.Button(numlock_frame, text="Emulate numlock button press", command=lambda: toggle_numlock(), style="TButton", state=tk.DISABLED) + toggle_btn.pack(side="left", padx=5, pady=5) + + update_numlock_state(numlock_state_var, refresh_btn, toggle_btn) + program_ver_label = tk.Label(tab1, text="Program Version: 0.2.0") program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) root.mainloop() +def update_numlock_state(state_var, refresh_btn=None, toggle_btn=None): + numlock_on = get_numlock_state() + if numlock_on is None and os != 'nt': + state_var.set("Unknown, please install the 'numlockx' command") + else: + if refresh_btn: + refresh_btn.config(state=tk.NORMAL) + if toggle_btn: + toggle_btn.config(state=tk.NORMAL) + state_var.set("On (Numbers)" if numlock_on else "Off (Arrows)") + def main(): devices = find_devs(show=False, verbose=False) # print("Found {} devices".format(len(devices))) @@ -359,14 +387,6 @@ def main(): window.start_thread(lambda: periodic_event(window), (THREAD_KEY, THREAD_EXITING)) while True: - numlock_on = get_numlock_state() - if numlock_on is None and os != 'nt': - window['-NUMLOCK-STATE-'].update("Unknown, please install the 'numlockx' command") - else: - window['-NUMLOCK-REFRESH-'].update(disabled=False) - window['-NUMLOCK-TOGGLE-'].update(disabled=False) - window['-NUMLOCK-STATE-'].update("On (Numbers)" if numlock_on else "Off (Arrows)") - event, values = window.read() # print('Event', event) # print('Values', values) From 291f38d7e62e62a6cde13fd68ab110de8a3da8ab Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 11:39:11 +0800 Subject: [PATCH 05/17] python: Implement other advanced buttons and add hints Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 5d85fa4..0c54aba 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -230,19 +230,21 @@ def tk_main(): rgb_effect_combo.bind("<>", lambda event: perform_action(devices, 'rgb_effect', value=RGB_EFFECTS.index(rgb_effect_combo.get()))) # Tab 2 - # TODO: Add registry controls, maybe hidden behind secret shortbut # Advanced Device Control Buttons eeprom_frame = ttk.LabelFrame(tab2, text="EEPROM", style="TLabelframe") eeprom_frame.pack(fill="x", padx=5, pady=5) + tk.Label(eeprom_frame, text="Clear user configured settings").pack(side="top", padx=5, pady=5) ttk.Button(eeprom_frame, text="Reset EEPROM", command=lambda: perform_action(devices, 'reset_eeprom'), style="TButton").pack(side="left", padx=5, pady=5) bios_mode_frame = ttk.LabelFrame(tab2, text="BIOS Mode", style="TLabelframe") bios_mode_frame.pack(fill="x", padx=5, pady=5) + tk.Label(bios_mode_frame, text="Disable function buttons, force F1-12").pack(side="top", padx=5, pady=5) ttk.Button(bios_mode_frame, text="Enable", command=lambda: perform_action(devices, 'bios_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) ttk.Button(bios_mode_frame, text="Disable", command=lambda: perform_action(devices, 'bios_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) factory_mode_frame = ttk.LabelFrame(tab2, text="Factory Mode", style="TLabelframe") factory_mode_frame.pack(fill="x", padx=5, pady=5) + tk.Label(factory_mode_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5) ttk.Button(factory_mode_frame, text="Enable", command=lambda: perform_action(devices, 'factory_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) ttk.Button(factory_mode_frame, text="Disable", command=lambda: perform_action(devices, 'factory_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) @@ -262,6 +264,15 @@ def tk_main(): update_numlock_state(numlock_state_var, refresh_btn, toggle_btn) + # TODO: Maybe hide behind secret shortcut + if os.name == 'nt': + registry_frame = ttk.LabelFrame(tab2, text="Windows Registry Tweaks", style="TLabelframe") + registry_frame.pack(fill="x", padx=5, pady=5) + tk.Label(registry_frame, text="Disabled. Only for very advanced debugging").pack(side="top", padx=5, pady=5) + ttk.Button(registry_frame, text="Enable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, True), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) + toggle_btn = ttk.Button(registry_frame, text="Disable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, False), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) + + program_ver_label = tk.Label(tab1, text="Program Version: 0.2.0") program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) @@ -818,8 +829,15 @@ def flash_firmware(dev, fw_path): print("Flashing finished") +def selective_suspend_wrapper(dev, enable): + if enable: + selective_suspend_registry(dev['product_id'], False, set=True) + replug_hint() + else: + selective_suspend_registry(dev['product_id'], False, set=False) + replug_hint() + -# TODO: Show replug_hint() def selective_suspend_registry(pid, verbose, set=None): # The set of keys we care about (under HKEY_LOCAL_MACHINE) are # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013\Device Parameters\SelectiveSuspendEnabled From 83fc4f53c0016ef693068608b8259743b5710a4d Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 12:02:45 +0800 Subject: [PATCH 06/17] python: Sync button/slider with actions Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 0c54aba..41ad152 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -903,8 +903,15 @@ def selective_suspend_registry(pid, verbose, set=None): def perform_action(devices, action, value=None): if action == "bootloader": - # TODO: Disable checkbox of that device + # Disable checkbox of that device + for dev in devices: + for path, checkbox in device_checkboxes.items(): + if path == dev['path']: + checkbox.set(False) + restart_hint() + if action == "off": + brightness_scale.set(0) action_map = { "bootloader": lambda dev: bootloader_jump(dev), @@ -916,7 +923,6 @@ def perform_action(devices, action, value=None): "green": lambda dev: set_rgb_color(dev, GREEN_HUE, 255), "blue": lambda dev: set_rgb_color(dev, BLUE_HUE, 255), "white": lambda dev: set_rgb_color(dev, None, 0), - # TODO: Also window['-BRIGHTNESS-'].Update(0) "off": lambda dev: set_rgb_brightness(dev, 0), "brightness": lambda dev: set_white_rgb_brightness(dev, value), "rgb_effect": lambda dev: set_rgb_u8(dev, RGB_MATRIX_VALUE_EFFECT, value), From 803893b74226fe9dad9c1e15b9835cb773e71271 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 12:06:24 +0800 Subject: [PATCH 07/17] python: Switch from pysimplegui to tkinter All the necessary functionality is implemented. Signed-off-by: Daniel Schaefer --- python/pyproject.toml | 1 - python/qmk_hid/gui.py | 186 +++--------------------------------------- 2 files changed, 10 insertions(+), 177 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 9698e28..68fe1b9 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -35,7 +35,6 @@ Source = "https://github.com/FrameworkComputer/qmk_hid" [project.gui-scripts] qmk_gui = "qmk_hid.gui:main" -qmk_tk = "qmk_hid.gui:tk_main" #[tool.hatch.version] #source = "vcs" diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 41ad152..0a1679a 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -4,7 +4,6 @@ import subprocess import time -#import PySimpleGUI as sg import tkinter as tk from tkinter import ttk, messagebox @@ -142,7 +141,7 @@ def get_numlock_state(): # Ignore tool not found, just return None pass -def tk_main(): +def main(): devices = find_devs(show=False, verbose=False) # print("Found {} devices".format(len(devices))) @@ -289,22 +288,8 @@ def update_numlock_state(state_var, refresh_btn=None, toggle_btn=None): toggle_btn.config(state=tk.NORMAL) state_var.set("On (Numbers)" if numlock_on else "Off (Arrows)") -def main(): - devices = find_devs(show=False, verbose=False) - # print("Found {} devices".format(len(devices))) - - device_checkboxes = [] - for dev in devices: - device_info = "{}\nSerial No: {}\nFW Version: {}\n".format( - dev['product_string'], - dev['serial_number'], - format_fw_ver(dev['release_number']) - ) - checkbox = sg.Checkbox(device_info, default=True, key='-CHECKBOX-{}-'.format(dev['path']), enable_events=True) - device_checkboxes.append([checkbox]) - - - +# Keeping until all features are implemented +def main_sg(): # Only in the pyinstaller bundle are the FW update binaries included if is_pyinstaller(): releases = find_releases() @@ -321,82 +306,6 @@ def main(): else: bundled_update = [] - - layout = [ - [sg.Text("Detected Devices")], - ] + device_checkboxes + [ - [sg.HorizontalSeparator()], - - [sg.Text("Bootloader")], - [sg.Button("Bootloader", k='-BOOTLOADER-')], - [sg.HorizontalSeparator()], - ] + bundled_update + [ - [sg.Text("Backlight Brightness")], - # TODO: Get default from device - [sg.Slider((0, 255), orientation='h', default_value=120, - k='-BRIGHTNESS-', enable_events=True)], - #[sg.Button("Enable Breathing", k='-ENABLE-BREATHING-')], - #[sg.Button("Disable Breathing", k='-DISABLE-BREATHING-')], - - [sg.Text("RGB Color")], - [ - sg.Button("Red", k='-RED-'), - sg.Button("Green", k='-GREEN-'), - sg.Button("Blue", k='-BLUE-'), - sg.Button("White", k='-WHITE-'), - sg.Button("Off", k='-OFF-'), - ], - - [sg.Text("RGB Effect")], - [sg.Combo(RGB_EFFECTS, k='-RGB-EFFECT-', enable_events=True)], - [sg.HorizontalSeparator()], - - [sg.Text("OS Numlock Setting")], - [sg.Text("State: "), sg.Text("", k='-NUMLOCK-STATE-'), sg.Push() ,sg.Button("Refresh", k='-NUMLOCK-REFRESH-', disabled=True)], - [sg.Button("Send Numlock Toggle", k='-NUMLOCK-TOGGLE-', disabled=True)], - - [sg.HorizontalSeparator()], - [ - sg.Column([ - [sg.Text("BIOS Mode")], - [sg.Button("Enable", k='-BIOS-MODE-ENABLE-'), sg.Button("Disable", k='-BIOS-MODE-DISABLE-')], - ]), - sg.VSeperator(), - sg.Column([ - [sg.Text("Factory Mode")], - [sg.Button("Enable", k='-FACTORY-MODE-ENABLE-'), sg.Button("Disable", k='-FACTORY-MODE-DISABLE-')], - ]) - ], - - [sg.HorizontalSeparator()], - [ - sg.Column([ - [sg.Text("Save/Erase Controls")], - [sg.Button("Save", k='-SAVE-'), sg.Button("Clear EEPROM", k='-CLEAR-EEPROM-')], - [sg.Text(f"Program Version: {PROGRAM_VERSION}")], - ]), - sg.VSeperator(), - sg.Column([ - [sg.Text("Registry Controls")], - [sg.Button("Enable Selective Suspend", k='-ENABLE-SELECTIVESUSPEND-')], - [sg.Button("Disable Selective Suspend", k='-DISABLE-SELECTIVESUSPEND-')], - #[sg.Button("Reset Registry", k='-RESET-REGISTRY-')], - ]) - ], - ] - - icon_path = None - if os.name == 'nt': - ICON_NAME = 'logo_cropped_transparent_keyboard_48x48.ico' - icon_path = os.path.join(resource_path(), 'res', ICON_NAME) if is_pyinstaller() else os.path.join('res', ICON_NAME) - window = sg.Window("QMK Keyboard Control", layout, finalize=True, icon=icon_path) - - selected_devices = [] - - # Optionally sync brightness between keyboards - # window.start_thread(lambda: backlight_watcher(window, devices), (THREAD_KEY, THREAD_EXITING)) - window.start_thread(lambda: periodic_event(window), (THREAD_KEY, THREAD_EXITING)) - while True: event, values = window.read() # print('Event', event) @@ -432,79 +341,12 @@ def main(): restart_hint() window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) - if event == "-NUMLOCK-TOGGLE-": - if os.name == 'nt': - keybd_event(VK_NUMLOCK, 0x3A, 0x1, 0) - keybd_event(VK_NUMLOCK, 0x3A, 0x3, 0) - else: - out = subprocess.check_output(['numlockx', 'toggle']) - - # Run commands on all selected devices - hint_shown = False - for dev in selected_devices: - if event == "-BOOTLOADER-": - bootloader_jump(dev) - window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) - if not hint_shown: - restart_hint() - hint_shown = True - - if event == "-BIOS-MODE-ENABLE-": - bios_mode(dev, True) - if event == "-BIOS-MODE-DISABLE-": - bios_mode(dev, False) - - if event == "-FACTORY-MODE-ENABLE-": - factory_mode(dev, True) - if event == "-FACTORY-MODE-DISABLE-": - factory_mode(dev, False) - - if event == '-BRIGHTNESS-': - set_brightness(dev, int(values['-BRIGHTNESS-'])) - set_rgb_brightness(dev, int(values['-BRIGHTNESS-'])) - - if event == '-RGB-EFFECT-': - effect = RGB_EFFECTS.index(values['-RGB-EFFECT-']) - set_rgb_u8(dev, RGB_MATRIX_VALUE_EFFECT, effect) - # TODO: Get effect - - if event == '-RED-': - set_rgb_color(dev, RED_HUE, 255) - if event == '-GREEN-': - set_rgb_color(dev, GREEN_HUE, 255) - if event == '-BLUE-': - set_rgb_color(dev, BLUE_HUE, 255) - if event == '-WHITE-': - set_rgb_color(dev, None, 0) - if event == '-OFF-': - window['-BRIGHTNESS-'].Update(0) - set_rgb_brightness(dev, 0) - - if event == '-SAVE-': - save(dev) - - if event == '-CLEAR-EEPROM-': - eeprom_reset(dev) - - if event == '-RESET-REGISTRY-': - # TODO: Implement completely deleting the relevant registry entries - pass - if event == '-ENABLE-SELECTIVESUSPEND-': - selective_suspend_registry(dev['product_id'], False, set=True) - if not hint_shown: - replug_hint() - hint_shown = True - - if event == '-DISABLE-SELECTIVESUSPEND-': - selective_suspend_registry(dev['product_id'], False, set=False) - if not hint_shown: - replug_hint() - hint_shown = True - - if event == sg.WIN_CLOSED: - break - - window.close() +def toggle_numlock(): + if os.name == 'nt': + keybd_event(VK_NUMLOCK, 0x3A, 0x1, 0) + keybd_event(VK_NUMLOCK, 0x3A, 0x3, 0) + else: + out = subprocess.check_output(['numlockx', 'toggle']) def is_pyinstaller(): @@ -521,15 +363,7 @@ def resource_path(): return base_path - -THREAD_KEY = '-THREAD-' -THREAD_EXITING = '-THREAD EXITING-' -def periodic_event(window): - while True: - window.write_event_value('-PERIODIC-EVENT-', None) - time.sleep(1) - - +# TODO: Possibly use this def backlight_watcher(window, devs): prev_brightness = {} while True: From 3df18ca67a2c76169ecb5eb7c161261dc98fb793 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 12:50:28 +0800 Subject: [PATCH 08/17] python: Port firmware flashing to tkinter Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 139 +++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 0a1679a..6ed87f2 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -13,7 +13,7 @@ from win32con import VK_NUMLOCK, VK_CAPITAL import winreg -import qmk_hid.uf2conv +from qmk_hid import uf2conv # TODO: # - Get current values @@ -145,30 +145,20 @@ def main(): devices = find_devs(show=False, verbose=False) # print("Found {} devices".format(len(devices))) - # TODO: Implement for tkinter - # Only in the pyinstaller bundle are the FW update binaries included - #if is_pyinstaller(): - # releases = find_releases() - # versions = sorted(list(releases.keys()), reverse=True) - # - # bundled_update = [ - # [sg.Text("Update Version")], - # [sg.Text("Version"), sg.Push(), sg.Combo(versions, k='-VERSION-', enable_events=True, default_value=versions[0])], - # [sg.Text("Type"), sg.Push(), sg.Combo(list(releases[versions[0]]), k='-TYPE-', enable_events=True)], - # [sg.Text("Make sure the firmware is compatible with\nALL selected devices!")], - # [sg.Button("Flash", k='-FLASH-', disabled=True)], - # [sg.HorizontalSeparator()], - # ] - #else: - # bundled_update = [] - root = tk.Tk() root.title("QMK GUI") + # TODO: Handle unplug gracefully + # for dev in devices: + # debug_print("Dev '{}' disconnected: {}".format(dev['product_string'], 'disconnected' in dev)) + # if 'disconnected' in dev: + # window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) tabControl = ttk.Notebook(root) tab1 = ttk.Frame(tabControl) + tab_fw_update = ttk.Frame(tabControl) tab2 = ttk.Frame(tabControl) tabControl.add(tab1, text="Home") + tabControl.add(tab_fw_update, text="Firmware Update") tabControl.add(tab2, text="Advanced") tabControl.pack(expand=1, fill="both") @@ -187,7 +177,7 @@ def main(): checkbox_var = tk.BooleanVar(value=True) checkbox = ttk.Checkbutton(detected_devices_frame, text=device_info, variable=checkbox_var, style="TCheckbutton") checkbox.pack(anchor="w") - device_checkboxes[dev['path']] = checkbox_var + device_checkboxes[dev['path']] = (checkbox_var, checkbox) # Device Control Buttons device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") @@ -271,6 +261,26 @@ def main(): ttk.Button(registry_frame, text="Enable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, True), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) toggle_btn = ttk.Button(registry_frame, text="Disable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, False), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) + # Only in the pyinstaller bundle are the FW update binaries included + if is_pyinstaller() or True: + releases = find_releases() + versions = sorted(list(releases.keys()), reverse=True) + + flash_btn = None + fw_type_combo = None + + fw_update_frame = ttk.LabelFrame(tab_fw_update, text="Update Firmware", style="TLabelframe") + fw_update_frame.pack(fill="x", padx=5, pady=5) + #tk.Label(fw_update_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5) + fw_ver_combo = ttk.Combobox(fw_update_frame, values=versions, style="TCombobox", state="readonly") + fw_ver_combo.pack(side=tk.LEFT, padx=5, pady=5) + fw_ver_combo.current(0) + fw_ver_combo.bind("<>", lambda event: select_fw_version(fw_ver_combo.get(), fw_type_combo, releases)) + fw_type_combo = ttk.Combobox(fw_update_frame, values=list(releases[versions[0]]), style="TCombobox", state="readonly") + fw_type_combo.pack(side=tk.LEFT, padx=5, pady=5) + fw_type_combo.bind("<>", lambda event: select_fw_type(fw_type_combo.get(), flash_btn)) + flash_btn = ttk.Button(fw_update_frame, text="Update", command=lambda: tk_flash_firmware(devices, releases, fw_ver_combo.get(), fw_type_combo.get()), state=tk.DISABLED, style="TButton") + flash_btn.pack(side="left", padx=5, pady=5) program_ver_label = tk.Label(tab1, text="Program Version: 0.2.0") program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) @@ -290,56 +300,12 @@ def update_numlock_state(state_var, refresh_btn=None, toggle_btn=None): # Keeping until all features are implemented def main_sg(): - # Only in the pyinstaller bundle are the FW update binaries included - if is_pyinstaller(): - releases = find_releases() - versions = sorted(list(releases.keys()), reverse=True) - - bundled_update = [ - [sg.Text("Update Version")], - [sg.Text("Version"), sg.Push(), sg.Combo(versions, k='-VERSION-', enable_events=True, default_value=versions[0])], - [sg.Text("Type"), sg.Push(), sg.Combo(list(releases[versions[0]]), k='-TYPE-', enable_events=True)], - [sg.Text("Make sure the firmware is compatible with\nALL selected devices!")], - [sg.Button("Flash", k='-FLASH-', disabled=True)], - [sg.HorizontalSeparator()], - ] - else: - bundled_update = [] while True: event, values = window.read() # print('Event', event) # print('Values', values) - for dev in devices: - debug_print("Dev '{}' disconnected: {}".format(dev['product_string'], 'disconnected' in dev)) - if 'disconnected' in dev: - window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) - - selected_devices = [ - dev for dev in devices if - values and values['-CHECKBOX-{}-'.format(dev['path'])] - ] - # print("Selected {} devices".format(len(selected_devices))) - - # Updating firmware - if event == "-VERSION-": - # After selecting a version, we can list the types of firmware available for this version - types = list(releases[values['-VERSION-']]) - window['-TYPE-'].update(value=types[0], values=types) - if event == "-TYPE-": - # Once the user has selected a type, the exact firmware file is known and can be flashed - window['-FLASH-'].update(disabled=False) - if event == "-FLASH-": - if len(selected_devices) != 1: - sg.Popup('To flash select exactly 1 device.') - continue - dev = selected_devices[0] - ver = values['-VERSION-'] - t = values['-TYPE-'] - flash_firmware(dev, releases[ver][t]) - restart_hint() - window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) def toggle_numlock(): if os.name == 'nt': @@ -618,6 +584,13 @@ def restart_hint(): message.pack(padx=20, pady=20) parent.mainloop() +def info_popup(msg): + parent = tk.Tk() + parent.title("Info") + message = tk.Message(parent, text="msg", width=800) + message.pack(padx=20, pady=20) + parent.mainloop() + def replug_hint(): parent = tk.Tk() @@ -627,9 +600,8 @@ def replug_hint(): parent.mainloop() -# TODO: Show restart_hint() and deselect checkbox def flash_firmware(dev, fw_path): - print(f"Flashing {fw_path}") + print(f"Flashing {fw_path} onto {dev['path']}") # First jump to bootloader drives = uf2conv.list_drives() @@ -735,13 +707,17 @@ def selective_suspend_registry(pid, verbose, set=None): except EnvironmentError as e: raise e +def disable_devices(devices): + # Disable checkbox of selected devices + for dev in devices: + for path, (checkbox_var, checkbox) in device_checkboxes.items(): + if path == dev['path']: + checkbox_var.set(False) + checkbox.config(state=tk.DISABLED) + def perform_action(devices, action, value=None): if action == "bootloader": - # Disable checkbox of that device - for dev in devices: - for path, checkbox in device_checkboxes.items(): - if path == dev['path']: - checkbox.set(False) + disable_devices(devices) restart_hint() if action == "off": @@ -767,12 +743,33 @@ def perform_action(devices, action, value=None): action_map[action](dev) def get_selected_devices(devices): - return [dev for dev in devices if dev['path'] in device_checkboxes and device_checkboxes[dev['path']].get()] + return [dev for dev in devices if dev['path'] in device_checkboxes and device_checkboxes[dev['path']][0].get()] def set_pattern(devices, pattern_name): selected_devices = get_selected_devices(devices) for dev in selected_devices: pattern(dev, pattern_name) +def select_fw_version(ver, fw_type_combo, releases): + # After selecting a version, we can list the types of firmware available for this version + types = list(releases[ver]) + fw_type_combo.config(values=types) + fw_type_combo.current(0) + +def select_fw_type(_fw_type, flash_btn): + # Once the user has selected a type, the exact firmware file is known and can be flashed + flash_btn.config(state=tk.NORMAL) + +def tk_flash_firmware(devices, releases, version, fw_type): + selected_devices = get_selected_devices(devices) + if len(selected_devices) != 1: + info_popup('To flash select exactly 1 device.') + return + dev = selected_devices[0] + flash_firmware(dev, releases[version][fw_type]) + # Disable device that we just flashed + disable_devices(devices) + restart_hint() + if __name__ == "__main__": main() From da31fae374770cc86fd0bc4a95a8f18e5dde8422 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 12:59:56 +0800 Subject: [PATCH 09/17] python: Bump version to 0.2.0 Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 6ed87f2..b9a5321 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -19,7 +19,7 @@ # - Get current values # - Set sliders to current values -PROGRAM_VERSION = "0.1.12" +PROGRAM_VERSION = "0.2.0" FWK_VID = 0x32AC DEBUG_PRINT = False From 4fb31c85a41102bee34823beab75b9a73ad304fa Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:08:48 +0800 Subject: [PATCH 10/17] python: Add white backlight breathing button Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index b9a5321..01e0eb8 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -218,6 +218,12 @@ def main(): rgb_effect_combo.pack(side=tk.LEFT, padx=5, pady=5) rgb_effect_combo.bind("<>", lambda event: perform_action(devices, 'rgb_effect', value=RGB_EFFECTS.index(rgb_effect_combo.get()))) + # White backlight keyboard + rgb_effect_label = tk.Label(brightness_frame, text="White Effect") + rgb_effect_label.pack(side=tk.LEFT, padx=5, pady=5) + ttk.Button(brightness_frame, text="Breathing", command=lambda a=action: perform_action(devices, "breathing_on"), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(brightness_frame, text="None", command=lambda a=action: perform_action(devices, "breathing_off"), style="TButton").pack(side="left", padx=5, pady=5) + # Tab 2 # Advanced Device Control Buttons eeprom_frame = ttk.LabelFrame(tab2, text="EEPROM", style="TLabelframe") @@ -563,6 +569,9 @@ def set_rgb_brightness(dev, brightness): def set_brightness(dev, brightness): set_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS, brightness) +def set_white_effect(dev, breathing_on): + set_backlight(dev, BACKLIGHT_VALUE_EFFECT, breathing_on) + # Set both def set_white_rgb_brightness(dev, brightness): set_brightness(dev, brightness) @@ -734,6 +743,8 @@ def perform_action(devices, action, value=None): "blue": lambda dev: set_rgb_color(dev, BLUE_HUE, 255), "white": lambda dev: set_rgb_color(dev, None, 0), "off": lambda dev: set_rgb_brightness(dev, 0), + "breathing_on": lambda dev: set_white_effect(dev, True), + "breathing_off": lambda dev: set_white_effect(dev, False), "brightness": lambda dev: set_white_rgb_brightness(dev, value), "rgb_effect": lambda dev: set_rgb_u8(dev, RGB_MATRIX_VALUE_EFFECT, value), } From d244ed22368a63dbdd938aeb788f549cba5ac869 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:23:26 +0800 Subject: [PATCH 11/17] python: Gracefully handle unplug Disable device Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 01e0eb8..8d100f9 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -147,11 +147,6 @@ def main(): root = tk.Tk() root.title("QMK GUI") - # TODO: Handle unplug gracefully - # for dev in devices: - # debug_print("Dev '{}' disconnected: {}".format(dev['product_string'], 'disconnected' in dev)) - # if 'disconnected' in dev: - # window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) tabControl = ttk.Notebook(root) tab1 = ttk.Frame(tabControl) @@ -304,14 +299,6 @@ def update_numlock_state(state_var, refresh_btn=None, toggle_btn=None): toggle_btn.config(state=tk.NORMAL) state_var.set("On (Numbers)" if numlock_on else "Off (Arrows)") -# Keeping until all features are implemented -def main_sg(): - - while True: - event, values = window.read() - # print('Event', event) - # print('Values', values) - def toggle_numlock(): if os.name == 'nt': @@ -493,12 +480,8 @@ def send_message(dev, message_id, msg, out_len): out_data = h.read(out_len+3) return out_data except (IOError, OSError) as ex: - dev['disconnected'] = True + disable_devices([dev]) debug_print("Error ({}): ".format(dev['path']), ex) - # Doesn't actually exit the process, pysimplegui catches it - # But it avoids the return value being used - # TODO: Get rid of this ugly hack and properly make the caller handle the failure - sys.exit(1) def set_keyboard_value(dev, value, number): msg = [value, number] From 3108ac559b7e4e90854d1797467b3518ad050ab9 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 15:29:38 +0800 Subject: [PATCH 12/17] python: Add online info buttons Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 8d100f9..8711be7 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -13,6 +13,8 @@ from win32con import VK_NUMLOCK, VK_CAPITAL import winreg +import webbrowser + from qmk_hid import uf2conv # TODO: @@ -174,6 +176,24 @@ def main(): checkbox.pack(anchor="w") device_checkboxes[dev['path']] = (checkbox_var, checkbox) + # Online Info + info_frame = ttk.LabelFrame(tab1, text="Online Info", style="TLabelframe") + info_frame.pack(fill="x", padx=10, pady=5) + infos = { + "VIA Web Interface": "https://keyboard.frame.work", + "Firmware Releases": "https://github.com/FrameworkComputer/qmk_firmware/releases", + "Tool Releases": "https://github.com/FrameworkComputer/qmk_hid/releases", + "Keyboard Hotkeys": "https://knowledgebase.frame.work/hotkeys-on-the-framework-laptop-16-keyboard-rkYIwFQPp", + "Macropad Layout": "https://knowledgebase.frame.work/default-keymap-for-the-rgb-macropad-rkBIgqmva", + "Numpad Layout": "https://knowledgebase.frame.work/default-keymap-for-the-numpad-rJZv44owa", + } + for (i, (text, url)) in enumerate(infos.items()): + # Organize in columns of three + row = int(i / 3) + column = i % 3 + btn = ttk.Button(info_frame, text=text, command=lambda: webbrowser.open(url), style="TButton") + btn.grid(row=row, column=column) + # Device Control Buttons device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") device_control_frame.pack(fill="x", padx=10, pady=5) From 3737399f458920651d87ae8724bf49b46051a429 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:01:28 +0800 Subject: [PATCH 13/17] python: Add tkinter icon on windows Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 8711be7..32fe1eb 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -149,6 +149,9 @@ def main(): root = tk.Tk() root.title("QMK GUI") + ico = "logo_cropped_transparent_keyboard_48x48.ico" + res_path = resource_path() + root.iconbitmap(f"{res_path}/res/{ico}") tabControl = ttk.Notebook(root) tab1 = ttk.Frame(tabControl) From 72b8696aa2f7d4ed53f96e7ef7bf79ec9acf64e3 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:02:48 +0800 Subject: [PATCH 14/17] python: Handle no firmware updates in releases folder Signed-off-by: Daniel Schaefer --- python/qmk_hid/gui.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 32fe1eb..5fe7ae0 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -286,8 +286,10 @@ def main(): toggle_btn = ttk.Button(registry_frame, text="Disable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, False), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) # Only in the pyinstaller bundle are the FW update binaries included - if is_pyinstaller() or True: - releases = find_releases() + releases = find_releases() + if not releases: + tk.Label(tab_fw_update, text="Cannot find firmware updates").pack(side="top", padx=5, pady=5) + else: versions = sorted(list(releases.keys()), reverse=True) flash_btn = None @@ -404,9 +406,13 @@ def find_releases(): from os.path import isfile, join import re - res_path = resource_path() - versions = listdir(os.path.join(res_path, "releases")) releases = {} + res_path = resource_path() + try: + versions = listdir(os.path.join(res_path, "releases")) + except FileNotFoundError: + return releases + for version in versions: path = join(res_path, "releases", version) releases[version] = {} From 80b979c7fe7830af603c4ea01f3bc35b7c2a0c3d Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:12:29 +0800 Subject: [PATCH 15/17] python: Add new GUI screenshot Signed-off-by: Daniel Schaefer --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0a32b43..7913275 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,14 @@ It will soon be superceded by QMK XAP, but that isn't ready yet. Tested to work on Windows and Linux, without any drivers or admin privileges. +### GUI + There is also an easy to use GUI tool that does not require commandline interaction. See [GUI README](python/README.md) +![](screenshots\qmk_gui_screenshot.png) + ### Supported devices The tool is generic and works for any device using [QMK Firmware](https://qmk.fm/). From 504c4d2b09e9ce4fac40abdb3d33b11b1b502ec8 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:14:32 +0800 Subject: [PATCH 16/17] Add screenshot file Signed-off-by: Daniel Schaefer --- screenshots/qmk_gui_screenshot.png | Bin 0 -> 41612 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/qmk_gui_screenshot.png diff --git a/screenshots/qmk_gui_screenshot.png b/screenshots/qmk_gui_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..4d63944d6dfe248859faff972c10c9136d17e368 GIT binary patch literal 41612 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 P0}yz+`njxgN@xNAmRAxN literal 0 HcmV?d00001 From bbb209be9b7543aff5f1b8833083b8d7e6cd7fc4 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sat, 23 Nov 2024 13:17:23 +0800 Subject: [PATCH 17/17] readme: Fix screenshot path Signed-off-by: Daniel Schaefer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7913275..3fdb50d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ There is also an easy to use GUI tool that does not require commandline interact See [GUI README](python/README.md) -![](screenshots\qmk_gui_screenshot.png) +![](screenshots/qmk_gui_screenshot.png) ### Supported devices