From 71525d7f5264ac69d4a6f105c9ff6d8c84764269 Mon Sep 17 00:00:00 2001 From: Julianne Swinoga Date: Sat, 10 Feb 2024 21:43:26 -0500 Subject: [PATCH 1/4] Moved QBusyIndicatorGoodBad into it's own file --- OATFWGUI/gui_logic.py | 3 +- OATFWGUI/qbusyindicatorgoodbad.py | 119 ++++++++++++++++++++++++++++++ OATFWGUI/qt_extensions.py | 116 +---------------------------- 3 files changed, 123 insertions(+), 115 deletions(-) create mode 100644 OATFWGUI/qbusyindicatorgoodbad.py diff --git a/OATFWGUI/gui_logic.py b/OATFWGUI/gui_logic.py index 6921711..46e682b 100644 --- a/OATFWGUI/gui_logic.py +++ b/OATFWGUI/gui_logic.py @@ -15,7 +15,8 @@ import requests from log_utils import LogObject, LoggedExternalFile -from qt_extensions import Worker, QBusyIndicatorGoodBad, BusyIndicatorState +from qt_extensions import Worker +from qbusyindicatorgoodbad import QBusyIndicatorGoodBad, 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 diff --git a/OATFWGUI/qbusyindicatorgoodbad.py b/OATFWGUI/qbusyindicatorgoodbad.py new file mode 100644 index 0000000..fb4fef5 --- /dev/null +++ b/OATFWGUI/qbusyindicatorgoodbad.py @@ -0,0 +1,119 @@ +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 + +log = logging.getLogger('') + + +class BusyIndicatorState(enum.Enum): + 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): + super().__init__(parent) + self.wSpn = QtWaitingSpinner(self, centerOnParent=False) + self.wGood = QIndicatorGood(self) + self.wBad = QIndicatorBad(self) + + self.wStacked = QStackedWidget() + self.wStacked.addWidget(self.wSpn) + self.wStacked.addWidget(self.wGood) + self.wStacked.addWidget(self.wBad) + self.wSpn.start() + + self.hbox = QHBoxLayout(self) + self.hbox.addWidget(self.wStacked) + self.hbox.setAlignment(Qt.AlignCenter) + self.setLayout(self.hbox) + + self.setWindowModality(Qt.NonModal) + self.setAttribute(Qt.WA_TranslucentBackground) + + if fixed_size is None: + fixed_size = (50, 50) + self.setFixedSize(QSize(*fixed_size)) + self.size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.setSizePolicy(self.size_policy) + + self.setState(BusyIndicatorState.NONE) + self.setAttribute(Qt.WA_DontShowOnScreen) + self.show() + + def setState(self, state: BusyIndicatorState): + if state == BusyIndicatorState.NONE: + self.wStacked.hide() + elif state == BusyIndicatorState.BUSY: + self.wStacked.setCurrentWidget(self.wSpn) + self.wStacked.show() + # self.wSpn.update() + elif state == BusyIndicatorState.GOOD: + self.wStacked.setCurrentWidget(self.wGood) + self.wStacked.show() + elif state == BusyIndicatorState.BAD: + self.wStacked.setCurrentWidget(self.wBad) + self.wStacked.show() + else: + log.error(f'Invalid busy indicator state {state}') + + +class QIndicatorGood(QWidget): + def __init__(self, parent): + super().__init__(parent) + self.setWindowModality(Qt.NonModal) + self.setAttribute(Qt.WA_TranslucentBackground) + + def paintEvent(self, QPaintEvent): + painter = QPainter(self) + max_width = painter.device().width() + max_height = painter.device().height() + bounding_size = min(max_width, max_height) + pen = QPen() + pen.setWidth(0.05 * bounding_size) + pen.setColor(QColor('green')) + painter.setPen(pen) + + painter.drawEllipse(0, 0, + bounding_size, bounding_size) + bottom_point = (0.5 * bounding_size, 0.9 * bounding_size) + painter.drawLine(0.2 * bounding_size, 0.6 * bounding_size, + *bottom_point) + painter.drawLine(*bottom_point, + 0.8 * bounding_size, 0.2 * bounding_size) + + +class QIndicatorBad(QWidget): + def __init__(self, parent): + super().__init__(parent) + self.setWindowModality(Qt.NonModal) + self.setAttribute(Qt.WA_TranslucentBackground) + + def paintEvent(self, QPaintEvent): + painter = QPainter(self) + max_width = painter.device().width() + max_height = painter.device().height() + bounding_size = min(max_width, max_height) + pen = QPen() + pen.setWidth(0.05 * bounding_size) + pen.setColor(QColor('red')) + painter.setPen(pen) + + painter.drawEllipse(0, 0, + bounding_size, bounding_size) + circle_width_fudge = 0.00001 * bounding_size # move the lines into the circle's width just a little + on_circle_top_half = (0.5 * math.cos(math.pi * 3 / 4) + 0.5) + circle_width_fudge + on_circle_bot_half = (0.5 * math.cos(math.pi * 1 / 4) + 0.5) - circle_width_fudge + painter.drawLine(on_circle_top_half * bounding_size, on_circle_top_half * bounding_size, + on_circle_bot_half * bounding_size, on_circle_bot_half * bounding_size) + painter.drawLine(on_circle_top_half * bounding_size, on_circle_bot_half * bounding_size, + on_circle_bot_half * bounding_size, on_circle_top_half * bounding_size) diff --git a/OATFWGUI/qt_extensions.py b/OATFWGUI/qt_extensions.py index cd86ce8..879f137 100644 --- a/OATFWGUI/qt_extensions.py +++ b/OATFWGUI/qt_extensions.py @@ -1,15 +1,9 @@ import sys import traceback import logging -import math -import enum -from typing import Optional, Tuple +from typing import Optional -from PySide6.QtCore import Slot, Signal, QObject, QRunnable, Qt, QSize, QMetaMethod -from PySide6.QtWidgets import QWidget, QStackedWidget, QHBoxLayout, QSizePolicy -from PySide6.QtGui import QPainter, QColor, QPen - -from waitingspinnerwidget import QtWaitingSpinner +from PySide6.QtCore import Slot, Signal, QObject, QRunnable, QMetaMethod log = logging.getLogger('') @@ -48,112 +42,6 @@ def run(self): self.signals.finished.emit() -class BusyIndicatorState(enum.Enum): - NONE = enum.auto() - BUSY = enum.auto() - GOOD = enum.auto() - BAD = enum.auto() - - -class QBusyIndicatorGoodBad(QWidget): - def __init__(self, fixed_size: Optional[Tuple[int, int]] = None): - super().__init__() - self.wSpn = QtWaitingSpinner(self, centerOnParent=False) - self.wGood = QIndicatorGood(self) - self.wBad = QIndicatorBad(self) - - self.wStacked = QStackedWidget() - self.wStacked.addWidget(self.wSpn) - self.wStacked.addWidget(self.wGood) - self.wStacked.addWidget(self.wBad) - self.wSpn.start() - - self.hbox = QHBoxLayout(self) - self.hbox.addWidget(self.wStacked) - self.hbox.setAlignment(Qt.AlignCenter) - self.setLayout(self.hbox) - - self.setWindowModality(Qt.NonModal) - self.setAttribute(Qt.WA_TranslucentBackground) - - if fixed_size is not None: - self.setFixedSize(QSize(*fixed_size)) - self.size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) - self.setSizePolicy(self.size_policy) - - self.setState(BusyIndicatorState.NONE) - self.setAttribute(Qt.WA_DontShowOnScreen) - self.show() - - def setState(self, state: BusyIndicatorState): - if state == BusyIndicatorState.NONE: - self.wStacked.hide() - elif state == BusyIndicatorState.BUSY: - self.wStacked.setCurrentWidget(self.wSpn) - self.wStacked.show() - self.wSpn.update() - elif state == BusyIndicatorState.GOOD: - self.wStacked.setCurrentWidget(self.wGood) - self.wStacked.show() - elif state == BusyIndicatorState.BAD: - self.wStacked.setCurrentWidget(self.wBad) - self.wStacked.show() - else: - log.error(f'Invalid busy indicator state {state}') - - -class QIndicatorGood(QWidget): - def __init__(self, parent): - super().__init__(parent) - self.setWindowModality(Qt.NonModal) - self.setAttribute(Qt.WA_TranslucentBackground) - - def paintEvent(self, QPaintEvent): - painter = QPainter(self) - max_width = painter.device().width() - max_height = painter.device().height() - bounding_size = min(max_width, max_height) - pen = QPen() - pen.setWidth(0.05 * bounding_size) - pen.setColor(QColor('green')) - painter.setPen(pen) - - painter.drawEllipse(0, 0, - bounding_size, bounding_size) - bottom_point = (0.5 * bounding_size, 0.9 * bounding_size) - painter.drawLine(0.2 * bounding_size, 0.6 * bounding_size, - *bottom_point) - painter.drawLine(*bottom_point, - 0.8 * bounding_size, 0.2 * bounding_size) - - -class QIndicatorBad(QWidget): - def __init__(self, parent): - super().__init__(parent) - self.setWindowModality(Qt.NonModal) - self.setAttribute(Qt.WA_TranslucentBackground) - - def paintEvent(self, QPaintEvent): - painter = QPainter(self) - max_width = painter.device().width() - max_height = painter.device().height() - bounding_size = min(max_width, max_height) - pen = QPen() - pen.setWidth(0.05 * bounding_size) - pen.setColor(QColor('red')) - painter.setPen(pen) - - painter.drawEllipse(0, 0, - bounding_size, bounding_size) - circle_width_fudge = 0.00001 * bounding_size # move the lines into the circle's width just a little - on_circle_top_half = (0.5 * math.cos(math.pi * 3 / 4) + 0.5) + circle_width_fudge - on_circle_bot_half = (0.5 * math.cos(math.pi * 1 / 4) + 0.5) - circle_width_fudge - painter.drawLine(on_circle_top_half * bounding_size, on_circle_top_half * bounding_size, - on_circle_bot_half * bounding_size, on_circle_bot_half * bounding_size) - painter.drawLine(on_circle_top_half * bounding_size, on_circle_bot_half * bounding_size, - on_circle_bot_half * bounding_size, on_circle_top_half * bounding_size) - - # 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]: From 0e24f04018a8112622db546bc2321b01981bb40a Mon Sep 17 00:00:00 2001 From: Julianne Swinoga Date: Sat, 24 Feb 2024 10:49:31 -0500 Subject: [PATCH 2/4] Update README.md to note that the latest supported python version is 3.11, add section on development --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 73cd35d..9a7b4d1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ OpenAstroTech FirmWare Graphical User Interface -- A graphical way to build and ## Supported platforms - Windows 64 bit - Linux 64 bit - - Requires Python 3.7+, git, libc >= 2.28 (check with `ldd --version`) + - Requires Python 3.7..3.11, git, libc >= 2.28 (check with `ldd --version`) MacOS _might_ work, don't have a mac to test on. Drop a line if you're willing to test it! @@ -31,3 +31,11 @@ Windows: Linux: ![](assets/screenshot_Linux.jpg) + +## Development +To run the app: +```shell +$ 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 +``` From f940e678dedabe971fc5013952295424c8153fbc Mon Sep 17 00:00:00 2001 From: Julianne Swinoga Date: Sat, 24 Feb 2024 10:54:39 -0500 Subject: [PATCH 3/4] Change main widget from code to .ui file No more sketchy editing of ui code! Now the main widget is all done in the designer app, probably fixed some UI bugs along the way (but not the painting problem with the spinner/checkbox, #9) --- OATFWGUI/gui_logic.py | 85 +----------- OATFWGUI/main.py | 25 +++- OATFWGUI/main_widget.ui | 183 +++++++++++++++++++++++++ OATFWGUI/qbusyindicatorgoodbad.py | 41 +++++- OATFWGUI/qt_extensions.py | 30 ++++ OATFWGUI/register_custom_qt_widgets.py | 19 +++ README.md | 9 ++ 7 files changed, 300 insertions(+), 92 deletions(-) create mode 100644 OATFWGUI/main_widget.ui create mode 100644 OATFWGUI/register_custom_qt_widgets.py 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..790585f 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) @@ -266,7 +281,7 @@ def main(): else: log.debug('NOT executing app') log.debug('Testing anonymous statistics creation') - anon_stats = create_anon_stats(widget.main_widget.logic.logic_state) + anon_stats = create_anon_stats(widget.logic.logic_state) log.debug(f'Statistics: {json.dumps(anon_stats)}') # Wait a bit before exiting, prevents Qt complaining about deleted objects time.sleep(1.0) 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 +
qbusyindicatorgoodbad
+ 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 From 1df35bd428b824fd9ff8bb74bb26a2f82123c1f0 Mon Sep 17 00:00:00 2001 From: Julianne Swinoga Date: Sat, 24 Feb 2024 11:24:39 -0500 Subject: [PATCH 4/4] Fix Segmentation fault from QUiLoader() by bumping PySide6-Essentials version Also fixed the CI Windows pyside6 pruning. They removed the examples directory, and I think the includes directory can also be pruned. --- .github/workflows/build-and-release.yaml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yaml b/.github/workflows/build-and-release.yaml index 1597dce..781d12f 100644 --- a/.github/workflows/build-and-release.yaml +++ b/.github/workflows/build-and-release.yaml @@ -97,8 +97,8 @@ jobs: rm -v ./dist/.python_local/Lib/site-packages/PySide6/designer.exe rm -v ./dist/.python_local/Lib/site-packages/PySide6/qmllint.exe rm -rv ./dist/.python_local/Lib/site-packages/PySide6/translations - rm -rv ./dist/.python_local/Lib/site-packages/PySide6/examples rm -rv ./dist/.python_local/Lib/site-packages/PySide6/qml + rm -rv ./dist/.python_local/Lib/site-packages/PySide6/include - name: Smoke test run: ./dist/OATFWGUI_Windows.bat --no-gui diff --git a/requirements.txt b/requirements.txt index 254cbad..088f84c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ platformio==6.1.4 -PySide6-Essentials==6.4.2 +PySide6-Essentials==6.5.3 # Last version that supports python 3.7 requests~=2.28.1 semver~=2.13.0 pygments~=2.13.0 \ No newline at end of file