diff --git a/OATFWGUI/gui_logic.py b/OATFWGUI/gui_logic.py
index 46e682b..141a5c2 100644
--- a/OATFWGUI/gui_logic.py
+++ b/OATFWGUI/gui_logic.py
@@ -7,16 +7,14 @@
from typing import List, Optional
from pathlib import Path
-from PySide6.QtCore import Slot, QThreadPool, QFile, QProcess, Qt
-from PySide6.QtGui import QFont
-from PySide6.QtWidgets import QLabel, QComboBox, QWidget, QFileDialog, QPushButton, QPlainTextEdit, QGridLayout, \
- QHBoxLayout, QCheckBox
+from PySide6.QtCore import Slot, QThreadPool, QFile, QProcess
+from PySide6.QtWidgets import QWidget, QFileDialog
import requests
-from log_utils import LogObject, LoggedExternalFile
+from log_utils import LoggedExternalFile
from qt_extensions import Worker
-from qbusyindicatorgoodbad import QBusyIndicatorGoodBad, BusyIndicatorState
+from qbusyindicatorgoodbad import BusyIndicatorState
from external_processes import external_processes, get_install_dir
from gui_state import LogicState, PioEnv, FWVersion
from anon_usage_data import AnonStatsDialog, create_anon_stats, upload_anon_stats
@@ -89,7 +87,7 @@ def extract_fw(zipfile_name: Path) -> Path:
class BusinessLogic:
- def __init__(self, main_app: 'MainWidget'):
+ def __init__(self, main_app: QWidget):
self.logic_state = LogicState()
self.main_app = main_app
@@ -369,76 +367,3 @@ def pio_upload_finished(self):
def modal_show_stats(self):
dlg = AnonStatsDialog(self.logic_state, self.main_app)
dlg.exec_()
-
-
-class MainWidget(QWidget):
- def __init__(self, log_object: LogObject):
- QWidget.__init__(self)
-
- # widgets
- self.wMsg_fw_version = QLabel('Select firmware version:')
- self.wCombo_fw_version = QComboBox()
- self.wCombo_fw_version.setPlaceholderText('Grabbing FW Versions...')
- self.wBtn_download_fw = QPushButton('Download')
- self.wBtn_download_fw.setEnabled(False)
- self.wSpn_download = QBusyIndicatorGoodBad(fixed_size=(50, 50))
-
- self.wMsg_pio_env = QLabel('Select board:')
- self.wCombo_pio_env = QComboBox()
- self.wCombo_pio_env.setPlaceholderText('No FW downloaded yet...')
- self.wBtn_select_local_config = QPushButton('Select local config file')
- self.wBtn_build_fw = QPushButton('Build FW')
- self.wBtn_build_fw.setEnabled(False)
- self.wMsg_config_path = QLabel('No config file selected')
- self.wSpn_build = QBusyIndicatorGoodBad(fixed_size=(50, 50))
-
- self.wBtn_refresh_ports = QPushButton('Refresh ports')
- self.wCombo_serial_port = QComboBox()
- self.wCombo_serial_port.setPlaceholderText('No port selected')
- self.wBtn_upload_fw = QPushButton('Upload FW')
- self.wBtn_upload_fw.setEnabled(False)
- self.wSpn_upload = QBusyIndicatorGoodBad(fixed_size=(50, 50))
-
- self.wChk_upload_stats = QCheckBox('Upload anonymous statistics?',
- toolTip='After a successful firmware update, upload anonymous firmware details to the OAT devs')
- self.wBtn_what_stats = QPushButton('What will be uploaded?')
-
- self.logText = QPlainTextEdit()
- self.logText.setLineWrapMode(QPlainTextEdit.NoWrap)
- self.logText.setReadOnly(True)
- log_font = QFont('this-font-does-not-exist')
- log_font.setStyleHint(QFont.Monospace) # Let Qt pick a monospace font
- self.logText.setFont(log_font)
-
- # layout
- self.g_layout = QGridLayout()
-
- layout_arr = [
- [self.wMsg_fw_version, self.wCombo_fw_version, self.wBtn_download_fw, self.wSpn_download],
- [self.wMsg_pio_env, self.wCombo_pio_env, self.wBtn_select_local_config, self.wBtn_build_fw],
- [self.wMsg_config_path, None, None, self.wSpn_build],
- [self.wBtn_refresh_ports, self.wCombo_serial_port, self.wBtn_upload_fw, self.wSpn_upload],
- [None, None, self.wChk_upload_stats, None],
- [None, None, self.wBtn_what_stats, None],
- ]
- for y, row_arr in enumerate(layout_arr):
- for x, widget in enumerate(row_arr):
- rowSpan = 1
- colSpan = 1
- while x + colSpan < len(row_arr) and row_arr[x + colSpan] is None:
- # next widget is None, expand column
- colSpan += 1
- if widget is not None:
- self.g_layout.addWidget(widget, y, x, rowSpan, colSpan)
- self.g_layout.setAlignment(Qt.AlignTop)
-
- # log window will take up the entire right side
- self.h_layout = QHBoxLayout(self)
- self.h_layout.addLayout(self.g_layout)
- self.h_layout.addWidget(self.logText)
-
- # signals
- log_object.log_signal.connect(self.logText.appendHtml)
-
- # business logic will connect signals as well
- self.logic = BusinessLogic(self)
diff --git a/OATFWGUI/main.py b/OATFWGUI/main.py
index b36adf1..a492a21 100755
--- a/OATFWGUI/main.py
+++ b/OATFWGUI/main.py
@@ -14,13 +14,14 @@
from typing import Dict, Tuple, Optional
import semver
-from PySide6.QtCore import Slot, Qt
+from PySide6.QtCore import Slot, Qt, QFile
from PySide6.QtWidgets import QApplication, QMainWindow, QStatusBar, QLabel
from PySide6.QtGui import QAction, QActionGroup
+from PySide6.QtUiTools import QUiLoader
from _version import __version__
from log_utils import LogObject, setup_logging
-from gui_logic import MainWidget
+from gui_logic import BusinessLogic
from platform_check import get_platform, PlatformEnum
from external_processes import external_processes, add_external_process, get_install_dir
from anon_usage_data import create_anon_stats
@@ -191,10 +192,24 @@ def __init__(self):
self.bug_hyperlink.setOpenExternalLinks(True)
self.status_bar.addPermanentWidget(self.bug_hyperlink) # addPermanentWidget == right side
- log.debug('Creating main widget')
- self.main_widget = MainWidget(l_o)
+ # Load the main widget from the .ui file
+ # Need to tell the UI loader where our custom widgets are
+ os.environ['PYSIDE_DESIGNER_PLUGINS'] = str(Path(get_install_dir(), 'OATFWGUI'))
+
+ main_widget_ui_path = Path(get_install_dir(), 'OATFWGUI', 'main_widget.ui')
+ log.debug(f'Loading main widget UI from {main_widget_ui_path}')
+ ui_file = QFile(main_widget_ui_path)
+ ui_file.open(QFile.ReadOnly)
+ loader = QUiLoader()
+ self.main_widget = loader.load(ui_file)
+ ui_file.close()
self.setCentralWidget(self.main_widget)
+ # signals
+ l_o.log_signal.connect(self.main_widget.logText.appendHtml)
+ # business logic will connect signals as well
+ self.logic = BusinessLogic(self.main_widget)
+
def add_log_menu_helper(self, name: str, cb_fn, is_checked=False):
action = QAction(name)
action.setCheckable(True)
diff --git a/OATFWGUI/main_widget.ui b/OATFWGUI/main_widget.ui
new file mode 100644
index 0000000..8ee85c7
--- /dev/null
+++ b/OATFWGUI/main_widget.ui
@@ -0,0 +1,183 @@
+
+
+ TopLevelWidget
+
+
+ true
+
+
+
+ 0
+ 0
+ 766
+ 513
+
+
+
+ Form
+
+
+ -
+
+
+ QLayout::SetMaximumSize
+
+
-
+
+
-
+
+
+ Select local config file
+
+
+
+ -
+
+
+ What will be uploaded?
+
+
+
+ -
+
+
+ false
+
+
+ Upload FW
+
+
+
+ -
+
+
+ No port selected
+
+
+
+ -
+
+
+ No FW downloaded yet...
+
+
+
+ -
+
+
+ Select firmware version:
+
+
+
+ -
+
+
+ false
+
+
+ Download
+
+
+
+ -
+
+
+ After a successful firmware update, upload anonymous firmware details to the OAT devs
+
+
+ Upload anonymous statistics?
+
+
+
+ -
+
+
+ Select board:
+
+
+
+ -
+
+
+ Grabbing FW Versions...
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ -
+
+
+ false
+
+
+ Build FW
+
+
+
+ -
+
+
+ Refresh ports
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ No config file selected
+
+
+
+
+
+ -
+
+
+
+ Monospace
+
+
+
+ QPlainTextEdit::NoWrap
+
+
+ true
+
+
+
+
+
+
+
+
+
+ QBusyIndicatorGoodBad
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/OATFWGUI/qbusyindicatorgoodbad.py b/OATFWGUI/qbusyindicatorgoodbad.py
index fb4fef5..1a3931a 100644
--- a/OATFWGUI/qbusyindicatorgoodbad.py
+++ b/OATFWGUI/qbusyindicatorgoodbad.py
@@ -1,26 +1,51 @@
import math
import enum
import logging
-from typing import Optional, Tuple
from PySide6.QtCore import Qt, QSize
from PySide6.QtWidgets import QWidget, QStackedWidget, QHBoxLayout, QSizePolicy
from PySide6.QtGui import QPainter, QColor, QPen
from waitingspinnerwidget import QtWaitingSpinner
+from qt_extensions import RegisteredCustomWidget
log = logging.getLogger('')
class BusyIndicatorState(enum.Enum):
+ # designer_dom_xml = '''
+ #
+ #
+ #
+ #
+ # 10
+ # 10
+ #
+ #
+ #
+ #
+ # '''
+ #
+ # def get_fixed_size(self) -> QSize:
+ # return self._fixed_size
+ #
+ # def set_fixed_size(self, fixed_size: QSize):
+ # self._fixed_size = fixed_size
+ # self.setFixedSize(fixed_size)
+ #
+ # fixed_size = Property(QSize, get_fixed_size, set_fixed_size)
+ #
+ # ...
+ #
+ # self._fixed_size = QSize(200, 200)
NONE = enum.auto()
BUSY = enum.auto()
GOOD = enum.auto()
BAD = enum.auto()
-class QBusyIndicatorGoodBad(QWidget):
- def __init__(self, parent=None, fixed_size: Optional[Tuple[int, int]] = None):
+class QBusyIndicatorGoodBad(RegisteredCustomWidget):
+ def __init__(self, parent=None):
super().__init__(parent)
self.wSpn = QtWaitingSpinner(self, centerOnParent=False)
self.wGood = QIndicatorGood(self)
@@ -40,13 +65,15 @@ def __init__(self, parent=None, fixed_size: Optional[Tuple[int, int]] = None):
self.setWindowModality(Qt.NonModal)
self.setAttribute(Qt.WA_TranslucentBackground)
- if fixed_size is None:
- fixed_size = (50, 50)
- self.setFixedSize(QSize(*fixed_size))
+ self.setFixedSize(QSize(50, 50))
+
self.size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.setSizePolicy(self.size_policy)
+ if self.running_in_designer():
+ self.setState(BusyIndicatorState.BUSY)
+ else:
+ self.setState(BusyIndicatorState.NONE)
- self.setState(BusyIndicatorState.NONE)
self.setAttribute(Qt.WA_DontShowOnScreen)
self.show()
diff --git a/OATFWGUI/qt_extensions.py b/OATFWGUI/qt_extensions.py
index 879f137..6c4a775 100644
--- a/OATFWGUI/qt_extensions.py
+++ b/OATFWGUI/qt_extensions.py
@@ -4,6 +4,7 @@
from typing import Optional
from PySide6.QtCore import Slot, Signal, QObject, QRunnable, QMetaMethod
+from PySide6.QtWidgets import QWidget
log = logging.getLogger('')
@@ -42,6 +43,35 @@ def run(self):
self.signals.finished.emit()
+class RegisteredCustomWidget(QWidget):
+ """
+ See https://doc.qt.io/qt-6/designer-ui-file-format.html for the XML format (it's kind of hard to read tho :/)
+ Also see https://doc.qt.io/qt-6/designer-creating-custom-widgets.html
+ """
+ designer_tooltip = ''
+ designer_dom_xml = ''
+
+ @classmethod
+ def factory(cls: 'RegisteredCustomWidget'):
+ if not cls.designer_tooltip:
+ cls.designer_tooltip = f'{cls.__name__} tooltip'
+ if cls.designer_dom_xml:
+ cls.designer_dom_xml = f'''
+
+
+
+
+'''
+
+ cls.designer_module = cls.__name__.lower()
+
+ return cls
+
+ def running_in_designer(self):
+ # TODO: This is specific to the .ui file, is there a cleaner way to make it generic?
+ return self.window().objectName() != 'TopLevelWidget'
+
+
# https://stackoverflow.com/a/68621792/1313872
# Idk why this is so hard for Qt
def get_signal(o_object: QObject, str_signal_name: str) -> Optional[QMetaMethod]:
diff --git a/OATFWGUI/register_custom_qt_widgets.py b/OATFWGUI/register_custom_qt_widgets.py
new file mode 100644
index 0000000..8bdfbda
--- /dev/null
+++ b/OATFWGUI/register_custom_qt_widgets.py
@@ -0,0 +1,19 @@
+from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
+
+from qbusyindicatorgoodbad import QBusyIndicatorGoodBad
+
+"""
+Set the environment variable PYSIDE_DESIGNER_PLUGINS to this directory and load the plugin,
+both in the app and with pyside6-designer
+
+"""
+
+if __name__ == '__main__':
+ # See RegisteredCustomWidget for this factory paradigm
+ qbusyindicatorgoodbad = QBusyIndicatorGoodBad.factory()
+ QPyDesignerCustomWidgetCollection.registerCustomWidget(
+ qbusyindicatorgoodbad,
+ module=qbusyindicatorgoodbad.designer_module, # idk what this actually affects
+ tool_tip=qbusyindicatorgoodbad.designer_tooltip,
+ xml=qbusyindicatorgoodbad.designer_dom_xml,
+ )
diff --git a/README.md b/README.md
index 9a7b4d1..5278c41 100644
--- a/README.md
+++ b/README.md
@@ -39,3 +39,12 @@ $ python3 -m venv .venv # Create a virtual environment in .venv
$ ./.venv/bin/pip install -r requirements.txt # Install requirements
$ source .venv/bin/activate && ./OATFWGUI/main.py # Run the app
```
+
+To run the widget designer you need python development libraries:
+- Fedora: `sudo dnf install python3-devel`
+- Ubuntu (probably, not tested): `sudo apt install python3-dev`
+
+- then:
+`source .venv/bin/activate && env PYSIDE_DESIGNER_PLUGINS=$(realpath ./OATFWGUI) pyside6-designer`
+
+Open `main_widget.ui` in the designer