2020-10-16 21:26:10 -04:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
2021-01-11 01:51:24 -05:00
|
|
|
from PyQt5.QtCore import Qt, QSettings, QStandardPaths
|
2020-10-17 06:08:52 -04:00
|
|
|
from PyQt5.QtWidgets import QWidget, QComboBox, QToolButton, QHBoxLayout, QVBoxLayout, QMainWindow, QAction, qApp, \
|
2021-01-16 13:20:49 -05:00
|
|
|
QFileDialog, QDialog, QTabWidget, QActionGroup, QMessageBox, QLabel
|
2020-10-16 15:16:28 -04:00
|
|
|
|
2020-10-18 00:58:01 -04:00
|
|
|
import json
|
2021-01-11 01:51:24 -05:00
|
|
|
import os
|
|
|
|
|
from urllib.request import urlopen
|
2020-10-18 00:58:01 -04:00
|
|
|
|
2020-12-02 02:47:11 -05:00
|
|
|
from firmware_flasher import FirmwareFlasher
|
2020-12-20 19:21:22 -05:00
|
|
|
from keymap_editor import KeymapEditor
|
2021-01-01 06:27:48 -05:00
|
|
|
from keymaps import KEYMAPS
|
2020-12-20 19:29:48 -05:00
|
|
|
from layout_editor import LayoutEditor
|
2020-12-21 21:52:56 -05:00
|
|
|
from macro_recorder import MacroRecorder
|
2020-12-27 08:09:28 -05:00
|
|
|
from unlocker import Unlocker
|
2020-12-02 10:10:59 -05:00
|
|
|
from util import tr, find_vial_devices
|
2020-12-29 15:01:44 -05:00
|
|
|
from vial_device import VialKeyboard
|
2020-10-16 15:16:28 -04:00
|
|
|
|
2021-01-11 09:41:00 -05:00
|
|
|
import themes
|
|
|
|
|
|
2020-10-16 15:16:28 -04:00
|
|
|
|
2020-10-17 06:08:52 -04:00
|
|
|
class MainWindow(QMainWindow):
|
2020-10-16 15:16:28 -04:00
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
2020-12-27 08:09:28 -05:00
|
|
|
|
2021-01-11 09:41:00 -05:00
|
|
|
self.settings = QSettings("Vial", "Vial")
|
|
|
|
|
themes.set_theme(self.settings.value("theme"))
|
|
|
|
|
|
2020-12-02 10:10:59 -05:00
|
|
|
self.current_device = None
|
2020-10-16 15:16:28 -04:00
|
|
|
self.devices = []
|
2021-01-11 01:51:24 -05:00
|
|
|
# create empty VIA definitions. Easier than setting it to none and handling a bunch of exceptions
|
|
|
|
|
self.via_stack_json = {"definitions": {}}
|
2020-10-18 00:58:01 -04:00
|
|
|
self.sideload_json = None
|
|
|
|
|
self.sideload_vid = self.sideload_pid = -1
|
2020-10-16 15:16:28 -04:00
|
|
|
|
|
|
|
|
self.combobox_devices = QComboBox()
|
|
|
|
|
self.combobox_devices.currentIndexChanged.connect(self.on_device_selected)
|
|
|
|
|
|
2020-12-02 11:37:43 -05:00
|
|
|
self.btn_refresh_devices = QToolButton()
|
|
|
|
|
self.btn_refresh_devices.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
|
|
|
|
self.btn_refresh_devices.setText(tr("MainWindow", "Refresh"))
|
|
|
|
|
self.btn_refresh_devices.clicked.connect(self.on_click_refresh)
|
2020-10-16 15:16:28 -04:00
|
|
|
|
|
|
|
|
layout_combobox = QHBoxLayout()
|
|
|
|
|
layout_combobox.addWidget(self.combobox_devices)
|
2020-12-02 11:37:43 -05:00
|
|
|
layout_combobox.addWidget(self.btn_refresh_devices)
|
2020-10-16 15:16:28 -04:00
|
|
|
|
2020-12-20 19:29:48 -05:00
|
|
|
self.layout_editor = LayoutEditor()
|
2020-12-20 22:13:16 -05:00
|
|
|
self.keymap_editor = KeymapEditor(self.layout_editor)
|
2020-12-02 11:37:43 -05:00
|
|
|
self.firmware_flasher = FirmwareFlasher(self)
|
2020-12-21 21:52:56 -05:00
|
|
|
self.macro_recorder = MacroRecorder()
|
2020-12-02 02:47:11 -05:00
|
|
|
|
2020-12-21 21:52:56 -05:00
|
|
|
self.editors = [(self.keymap_editor, "Keymap"), (self.layout_editor, "Layout"), (self.macro_recorder, "Macros"),
|
2020-12-20 19:29:48 -05:00
|
|
|
(self.firmware_flasher, "Firmware updater")]
|
2020-12-29 14:47:16 -05:00
|
|
|
self.unlocker = Unlocker(self.layout_editor)
|
2020-12-20 19:29:48 -05:00
|
|
|
|
2020-12-02 10:10:59 -05:00
|
|
|
self.tabs = QTabWidget()
|
|
|
|
|
self.refresh_tabs()
|
2020-12-02 02:47:11 -05:00
|
|
|
|
2021-01-16 13:20:49 -05:00
|
|
|
self.lbl_no_devices = QLabel(tr("MainWindow", 'No devices detected. Connect a Vial-compatible device and press '
|
|
|
|
|
'"Refresh"\n'
|
|
|
|
|
'or select "File" → "Download VIA definitions" in order to enable'
|
|
|
|
|
' support for VIA keyboards.'))
|
|
|
|
|
self.lbl_no_devices.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
2020-10-16 15:16:28 -04:00
|
|
|
layout = QVBoxLayout()
|
|
|
|
|
layout.addLayout(layout_combobox)
|
2020-12-02 10:10:59 -05:00
|
|
|
layout.addWidget(self.tabs)
|
2021-01-16 13:20:49 -05:00
|
|
|
layout.addWidget(self.lbl_no_devices)
|
|
|
|
|
layout.setAlignment(self.lbl_no_devices, Qt.AlignHCenter)
|
2020-10-17 06:08:52 -04:00
|
|
|
w = QWidget()
|
|
|
|
|
w.setLayout(layout)
|
|
|
|
|
self.setCentralWidget(w)
|
|
|
|
|
|
|
|
|
|
self.init_menu()
|
2020-10-16 15:16:28 -04:00
|
|
|
|
2021-01-11 01:51:24 -05:00
|
|
|
# cache for via definition files
|
|
|
|
|
self.cache_path = QStandardPaths.writableLocation(QStandardPaths.CacheLocation)
|
|
|
|
|
if not os.path.exists(self.cache_path):
|
|
|
|
|
os.makedirs(self.cache_path)
|
|
|
|
|
# check if the via defitions already exist
|
|
|
|
|
if os.path.isfile(os.path.join(self.cache_path, "via_keyboards.json")):
|
|
|
|
|
with open(os.path.join(self.cache_path, "via_keyboards.json")) as vf:
|
|
|
|
|
self.via_stack_json = json.load(vf)
|
|
|
|
|
vf.close()
|
|
|
|
|
|
2020-10-16 15:16:28 -04:00
|
|
|
# make sure initial state is valid
|
|
|
|
|
self.on_click_refresh()
|
|
|
|
|
|
2020-10-17 06:08:52 -04:00
|
|
|
def init_menu(self):
|
2020-10-18 21:47:47 -04:00
|
|
|
layout_load_act = QAction(tr("MenuFile", "Load saved layout..."), self)
|
2020-10-17 06:08:52 -04:00
|
|
|
layout_load_act.setShortcut("Ctrl+O")
|
|
|
|
|
layout_load_act.triggered.connect(self.on_layout_load)
|
|
|
|
|
|
2020-10-18 21:47:47 -04:00
|
|
|
layout_save_act = QAction(tr("MenuFile", "Save current layout..."), self)
|
2020-10-17 06:08:52 -04:00
|
|
|
layout_save_act.setShortcut("Ctrl+S")
|
|
|
|
|
layout_save_act.triggered.connect(self.on_layout_save)
|
|
|
|
|
|
2020-10-18 21:47:47 -04:00
|
|
|
sideload_json_act = QAction(tr("MenuFile", "Sideload VIA JSON..."), self)
|
2020-10-18 00:58:01 -04:00
|
|
|
sideload_json_act.triggered.connect(self.on_sideload_json)
|
|
|
|
|
|
2021-01-11 01:51:24 -05:00
|
|
|
download_via_stack_act = QAction(tr("MenuFile", "Download VIA definitions"), self)
|
|
|
|
|
download_via_stack_act.triggered.connect(self.load_via_stack_json)
|
|
|
|
|
|
2021-01-11 17:08:21 -05:00
|
|
|
load_dummy_act = QAction(tr("MenuFile", "Load dummy JSON..."), self)
|
|
|
|
|
load_dummy_act.triggered.connect(self.on_load_dummy)
|
|
|
|
|
|
2020-10-17 06:08:52 -04:00
|
|
|
exit_act = QAction(tr("MenuFile", "Exit"), self)
|
|
|
|
|
exit_act.setShortcut("Ctrl+Q")
|
|
|
|
|
exit_act.triggered.connect(qApp.exit)
|
|
|
|
|
|
|
|
|
|
file_menu = self.menuBar().addMenu(tr("Menu", "File"))
|
|
|
|
|
file_menu.addAction(layout_load_act)
|
|
|
|
|
file_menu.addAction(layout_save_act)
|
|
|
|
|
file_menu.addSeparator()
|
2020-10-18 00:58:01 -04:00
|
|
|
file_menu.addAction(sideload_json_act)
|
2021-01-11 01:51:24 -05:00
|
|
|
file_menu.addAction(download_via_stack_act)
|
2021-01-11 17:08:21 -05:00
|
|
|
file_menu.addAction(load_dummy_act)
|
2020-10-18 00:58:01 -04:00
|
|
|
file_menu.addSeparator()
|
2020-10-17 06:08:52 -04:00
|
|
|
file_menu.addAction(exit_act)
|
|
|
|
|
|
2020-12-29 15:01:44 -05:00
|
|
|
keyboard_unlock_act = QAction(tr("MenuSecurity", "Unlock"), self)
|
|
|
|
|
keyboard_unlock_act.triggered.connect(self.unlock_keyboard)
|
|
|
|
|
|
|
|
|
|
keyboard_lock_act = QAction(tr("MenuSecurity", "Lock"), self)
|
|
|
|
|
keyboard_lock_act.triggered.connect(self.lock_keyboard)
|
|
|
|
|
|
2021-01-01 05:15:48 -05:00
|
|
|
keyboard_reset_act = QAction(tr("MenuSecurity", "Reboot to bootloader"), self)
|
|
|
|
|
keyboard_reset_act.triggered.connect(self.reboot_to_bootloader)
|
|
|
|
|
|
2021-01-01 06:27:48 -05:00
|
|
|
keyboard_layout_menu = self.menuBar().addMenu(tr("Menu", "Keyboard layout"))
|
|
|
|
|
keymap_group = QActionGroup(self)
|
|
|
|
|
for idx, keymap in enumerate(KEYMAPS):
|
|
|
|
|
act = QAction(tr("KeyboardLayout", keymap[0]), self)
|
|
|
|
|
act.triggered.connect(lambda checked, x=idx: self.change_keyboard_layout(x))
|
|
|
|
|
act.setCheckable(True)
|
|
|
|
|
if idx == 0:
|
|
|
|
|
act.setChecked(True)
|
|
|
|
|
keymap_group.addAction(act)
|
|
|
|
|
keyboard_layout_menu.addAction(act)
|
|
|
|
|
|
2020-12-29 15:01:44 -05:00
|
|
|
self.security_menu = self.menuBar().addMenu(tr("Menu", "Security"))
|
|
|
|
|
self.security_menu.addAction(keyboard_unlock_act)
|
|
|
|
|
self.security_menu.addAction(keyboard_lock_act)
|
2021-01-01 05:15:48 -05:00
|
|
|
self.security_menu.addSeparator()
|
|
|
|
|
self.security_menu.addAction(keyboard_reset_act)
|
2020-12-29 15:01:44 -05:00
|
|
|
|
2021-01-12 18:41:23 -05:00
|
|
|
self.theme_menu = self.menuBar().addMenu(tr("Menu", "Theme"))
|
2021-01-12 18:50:30 -05:00
|
|
|
theme_group = QActionGroup(self)
|
|
|
|
|
selected_theme = self.settings.value("theme")
|
|
|
|
|
for name, _ in [("System", None)] + themes.themes:
|
2021-01-12 18:41:23 -05:00
|
|
|
act = QAction(tr("MenuTheme", name), self)
|
|
|
|
|
act.triggered.connect(lambda x,name=name: self.set_theme(name))
|
2021-01-12 18:50:30 -05:00
|
|
|
act.setCheckable(True)
|
|
|
|
|
act.setChecked(selected_theme == name)
|
|
|
|
|
theme_group.addAction(act)
|
2021-01-12 18:41:23 -05:00
|
|
|
self.theme_menu.addAction(act)
|
2021-01-12 18:50:30 -05:00
|
|
|
# check "System" if nothing else is selected
|
|
|
|
|
if theme_group.checkedAction() is None:
|
|
|
|
|
theme_group.actions()[0].setChecked(True)
|
2021-01-11 09:41:00 -05:00
|
|
|
|
2020-10-17 06:08:52 -04:00
|
|
|
def on_layout_load(self):
|
|
|
|
|
dialog = QFileDialog()
|
|
|
|
|
dialog.setDefaultSuffix("vil")
|
|
|
|
|
dialog.setAcceptMode(QFileDialog.AcceptOpen)
|
|
|
|
|
dialog.setNameFilters(["Vial layout (*.vil)"])
|
|
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
|
|
|
with open(dialog.selectedFiles()[0], "rb") as inf:
|
|
|
|
|
data = inf.read()
|
2020-12-20 19:21:22 -05:00
|
|
|
self.keymap_editor.restore_layout(data)
|
2020-12-26 20:29:22 -05:00
|
|
|
self.rebuild()
|
2020-10-17 06:08:52 -04:00
|
|
|
|
|
|
|
|
def on_layout_save(self):
|
|
|
|
|
dialog = QFileDialog()
|
|
|
|
|
dialog.setDefaultSuffix("vil")
|
|
|
|
|
dialog.setAcceptMode(QFileDialog.AcceptSave)
|
|
|
|
|
dialog.setNameFilters(["Vial layout (*.vil)"])
|
|
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
|
|
|
with open(dialog.selectedFiles()[0], "wb") as outf:
|
2020-12-20 19:21:22 -05:00
|
|
|
outf.write(self.keymap_editor.save_layout())
|
2020-10-17 06:08:52 -04:00
|
|
|
|
2020-10-16 15:16:28 -04:00
|
|
|
def on_click_refresh(self):
|
2021-01-11 01:51:24 -05:00
|
|
|
self.devices = find_vial_devices(self.via_stack_json, self.sideload_vid, self.sideload_pid)
|
2020-10-16 15:16:28 -04:00
|
|
|
self.combobox_devices.clear()
|
|
|
|
|
|
|
|
|
|
for dev in self.devices:
|
2020-12-02 10:10:59 -05:00
|
|
|
self.combobox_devices.addItem(dev.title())
|
2020-10-16 15:16:28 -04:00
|
|
|
|
2021-01-16 13:20:49 -05:00
|
|
|
if self.devices:
|
|
|
|
|
self.lbl_no_devices.hide()
|
|
|
|
|
self.tabs.show()
|
|
|
|
|
else:
|
|
|
|
|
self.lbl_no_devices.show()
|
|
|
|
|
self.tabs.hide()
|
|
|
|
|
|
2020-10-16 15:16:28 -04:00
|
|
|
def on_device_selected(self):
|
2020-12-02 10:10:59 -05:00
|
|
|
if self.current_device is not None:
|
|
|
|
|
self.current_device.close()
|
|
|
|
|
self.current_device = None
|
2020-10-16 15:16:28 -04:00
|
|
|
idx = self.combobox_devices.currentIndex()
|
|
|
|
|
if idx >= 0:
|
2020-12-02 10:10:59 -05:00
|
|
|
self.current_device = self.devices[idx]
|
|
|
|
|
|
|
|
|
|
if self.current_device is not None:
|
2021-01-11 01:51:24 -05:00
|
|
|
if self.current_device.sideload:
|
|
|
|
|
self.current_device.open(self.sideload_json)
|
|
|
|
|
elif self.current_device.via_stack:
|
|
|
|
|
self.current_device.open(self.via_stack_json["definitions"][self.current_device.via_id])
|
|
|
|
|
else:
|
|
|
|
|
self.current_device.open(None)
|
2020-12-02 10:10:59 -05:00
|
|
|
|
2020-12-26 20:29:22 -05:00
|
|
|
self.rebuild()
|
2020-12-02 10:10:59 -05:00
|
|
|
|
|
|
|
|
self.refresh_tabs()
|
|
|
|
|
|
2020-12-26 20:29:22 -05:00
|
|
|
def rebuild(self):
|
2020-12-29 15:01:44 -05:00
|
|
|
# don't show "Security" menu for bootloader mode, as the bootloader is inherently insecure
|
|
|
|
|
self.security_menu.menuAction().setVisible(isinstance(self.current_device, VialKeyboard))
|
|
|
|
|
|
2021-01-07 09:17:52 -05:00
|
|
|
# if unlock process was interrupted, we must finish it first
|
|
|
|
|
if isinstance(self.current_device, VialKeyboard) and self.current_device.keyboard.get_unlock_in_progress():
|
|
|
|
|
Unlocker.get().perform_unlock(self.current_device.keyboard)
|
|
|
|
|
self.current_device.keyboard.reload()
|
|
|
|
|
|
2020-12-26 20:29:22 -05:00
|
|
|
for e in [self.layout_editor, self.keymap_editor, self.firmware_flasher, self.macro_recorder]:
|
|
|
|
|
e.rebuild(self.current_device)
|
|
|
|
|
|
2020-12-02 10:10:59 -05:00
|
|
|
def refresh_tabs(self):
|
|
|
|
|
self.tabs.clear()
|
2020-12-20 19:29:48 -05:00
|
|
|
for container, lbl in self.editors:
|
2020-12-02 10:10:59 -05:00
|
|
|
if not container.valid():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
w = QWidget()
|
|
|
|
|
w.setLayout(container)
|
|
|
|
|
self.tabs.addTab(w, tr("MainWindow", lbl))
|
2020-10-18 00:58:01 -04:00
|
|
|
|
2021-01-11 01:51:24 -05:00
|
|
|
def load_via_stack_json(self):
|
|
|
|
|
data = urlopen("https://github.com/vial-kb/via-keymap-precompiled/raw/main/via_keyboard_stack.json")
|
|
|
|
|
self.via_stack_json = json.load(data)
|
|
|
|
|
# write to cache
|
|
|
|
|
with open(os.path.join(self.cache_path, "via_keyboards.json"), "w") as cf:
|
|
|
|
|
cf.write(json.dumps(self.via_stack_json, indent=2))
|
|
|
|
|
cf.close()
|
|
|
|
|
|
2020-10-18 00:58:01 -04:00
|
|
|
def on_sideload_json(self):
|
|
|
|
|
dialog = QFileDialog()
|
|
|
|
|
dialog.setDefaultSuffix("json")
|
|
|
|
|
dialog.setAcceptMode(QFileDialog.AcceptOpen)
|
|
|
|
|
dialog.setNameFilters(["VIA layout JSON (*.json)"])
|
|
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
|
|
|
with open(dialog.selectedFiles()[0], "rb") as inf:
|
|
|
|
|
data = inf.read()
|
|
|
|
|
self.sideload_json = json.loads(data)
|
|
|
|
|
self.sideload_vid = int(self.sideload_json["vendorId"], 16)
|
|
|
|
|
self.sideload_pid = int(self.sideload_json["productId"], 16)
|
|
|
|
|
self.on_click_refresh()
|
2020-12-02 11:37:43 -05:00
|
|
|
|
2021-01-11 17:08:21 -05:00
|
|
|
def on_load_dummy(self):
|
|
|
|
|
dialog = QFileDialog()
|
|
|
|
|
dialog.setDefaultSuffix("json")
|
|
|
|
|
dialog.setAcceptMode(QFileDialog.AcceptOpen)
|
|
|
|
|
dialog.setNameFilters(["VIA layout JSON (*.json)"])
|
|
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
|
|
|
with open(dialog.selectedFiles()[0], "rb") as inf:
|
|
|
|
|
data = inf.read()
|
|
|
|
|
self.sideload_json = json.loads(data)
|
|
|
|
|
self.sideload_vid = self.sideload_pid = 0
|
|
|
|
|
self.on_click_refresh()
|
|
|
|
|
|
2020-12-02 11:37:43 -05:00
|
|
|
def lock_ui(self):
|
|
|
|
|
self.tabs.setEnabled(False)
|
|
|
|
|
self.combobox_devices.setEnabled(False)
|
|
|
|
|
self.btn_refresh_devices.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
def unlock_ui(self):
|
|
|
|
|
self.tabs.setEnabled(True)
|
|
|
|
|
self.combobox_devices.setEnabled(True)
|
|
|
|
|
self.btn_refresh_devices.setEnabled(True)
|
2020-12-29 15:01:44 -05:00
|
|
|
|
|
|
|
|
def unlock_keyboard(self):
|
|
|
|
|
if isinstance(self.current_device, VialKeyboard):
|
|
|
|
|
self.unlocker.perform_unlock(self.current_device.keyboard)
|
|
|
|
|
|
|
|
|
|
def lock_keyboard(self):
|
|
|
|
|
if isinstance(self.current_device, VialKeyboard):
|
|
|
|
|
self.current_device.keyboard.lock()
|
2021-01-01 05:15:48 -05:00
|
|
|
|
|
|
|
|
def reboot_to_bootloader(self):
|
|
|
|
|
if isinstance(self.current_device, VialKeyboard):
|
|
|
|
|
self.unlocker.perform_unlock(self.current_device.keyboard)
|
|
|
|
|
self.current_device.keyboard.reset()
|
2021-01-01 06:27:48 -05:00
|
|
|
|
|
|
|
|
def change_keyboard_layout(self, index):
|
|
|
|
|
self.keymap_editor.set_keymap_override(KEYMAPS[index][1])
|
2021-01-11 09:41:00 -05:00
|
|
|
|
|
|
|
|
def set_theme(self, theme):
|
|
|
|
|
themes.set_theme(theme)
|
|
|
|
|
self.settings.setValue("theme", theme)
|
2021-01-12 18:50:30 -05:00
|
|
|
msg = QMessageBox()
|
|
|
|
|
msg.setText(tr("MainWindow", "In order to fully apply the theme you should restart the application."))
|
|
|
|
|
msg.exec_()
|