vial/src/main/python/main_window.py

244 lines
9.3 KiB
Python
Raw Normal View History

# SPDX-License-Identifier: GPL-2.0-or-later
from PyQt5.QtCore import Qt, QSettings
2020-10-17 06:08:52 -04:00
from PyQt5.QtWidgets import QWidget, QComboBox, QToolButton, QHBoxLayout, QVBoxLayout, QMainWindow, QAction, qApp, \
QFileDialog, QDialog, QTabWidget, QActionGroup
2020-10-16 15:16:28 -04:00
2020-10-18 00:58:01 -04:00
import json
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
from keymaps import KEYMAPS
2020-12-20 19:29:48 -05:00
from layout_editor import LayoutEditor
from macro_recorder import MacroRecorder
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
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__()
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 = []
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)
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)
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()
self.keymap_editor = KeymapEditor(self.layout_editor)
self.firmware_flasher = FirmwareFlasher(self)
self.macro_recorder = MacroRecorder()
2020-12-02 02:47:11 -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
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)
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
# make sure initial state is valid
self.on_click_refresh()
2020-10-17 06:08:52 -04:00
def init_menu(self):
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)
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)
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)
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)
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)
keyboard_reset_act = QAction(tr("MenuSecurity", "Reboot to bootloader"), self)
keyboard_reset_act.triggered.connect(self.reboot_to_bootloader)
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)
self.security_menu.addSeparator()
self.security_menu.addAction(keyboard_reset_act)
2020-12-29 15:01:44 -05:00
theme_set_default = QAction(tr("MenuTheme", "System"), self)
theme_set_default.triggered.connect(lambda: self.set_theme("default"))
theme_set_light = QAction(tr("MenuTheme", "Light"), self)
theme_set_light.triggered.connect(lambda: self.set_theme("light"))
theme_set_dark = QAction(tr("MenuTheme", "Dark"), self)
theme_set_dark.triggered.connect(lambda: self.set_theme("dark"))
theme_set_arc = QAction(tr("MenuTheme", "Arc"), self)
theme_set_arc.triggered.connect(lambda: self.set_theme("arc"))
self.theme_menu = self.menuBar().addMenu(tr("Menu", "Theme"))
self.theme_menu.addAction(theme_set_default)
self.theme_menu.addAction(theme_set_light)
self.theme_menu.addAction(theme_set_dark)
self.theme_menu.addAction(theme_set_arc)
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)
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):
2020-12-02 10:10:59 -05:00
self.devices = find_vial_devices(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
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:
self.current_device.open(self.sideload_json if self.current_device.sideload else None)
self.rebuild()
2020-12-02 10:10:59 -05:00
self.refresh_tabs()
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))
# 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()
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
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()
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()
def reboot_to_bootloader(self):
if isinstance(self.current_device, VialKeyboard):
self.unlocker.perform_unlock(self.current_device.keyboard)
self.current_device.keyboard.reset()
def change_keyboard_layout(self, index):
self.keymap_editor.set_keymap_override(KEYMAPS[index][1])
def set_theme(self, theme):
themes.set_theme(theme)
self.settings.setValue("theme", theme)