From dcb13a38327d52b9a3b9ea66ea8065472d96d796 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 22 Mar 2020 01:43:03 +0300 Subject: [PATCH 01/20] fix README, help. Some useful QML improvements --- README.md | 6 +- TODO.md | 10 +- stm32pio-gui/app.py | 37 ++++++- stm32pio-gui/main.qml | 220 ++++++++++++++++++++++-------------------- stm32pio/app.py | 8 +- 5 files changed, 168 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index ae30518..cc50033 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,10 @@ $ pip uninstall stm32pio ## Usage Basically, you need to follow such a pattern: - 1. Create CubeMX project (.ioc file), set-up your hardware configuration, save + 1. Create CubeMX project (.ioc file), set-up your hardware configuration, save with the compatible parameters 2. Run the stm32pio that automatically invokes CubeMX to generate the code, creates PlatformIO project, patches a `platformio.ini` file and so on 3. Work on the project in your editor as usual, compile/upload/debug etc. - 4. Edit the configuration in CubeMX when necessary, then run stm32pio to re-generate the code. + 4. Edit the configuration in CubeMX when necessary, then run stm32pio to re-generate the code Refer to Example section on more detailed steps. If you face off with some error try to enable a verbose output to get more information about a problem: ```shell script @@ -111,7 +111,7 @@ You can also use stm32pio as an ordinary Python package and embed it in your own ![Project tab](/screenshots/tab_Project.png) -4. Use a copied string as a `-d` argument for stm32pio. So it is assumed that the name of the project folder matches the name of `.ioc` file. (`-d` argument can be omitted if your current working directory is already a project directory) +4. Use a copied string (project folder) as a `-d` argument for stm32pio (can be omitted if your current working directory is already a project directory). 5. Run `platformio boards` (`pio boards`) or go to [boards](https://docs.platformio.org/en/latest/boards) to list all supported devices. Pick one and use its ID as a `-b` argument (for example, `nucleo_f031k6`) 6. All done! You can now run ```shell script diff --git a/TODO.md b/TODO.md index ab83361..d616199 100644 --- a/TODO.md +++ b/TODO.md @@ -7,9 +7,12 @@ - [ ] GUI. Reduce number of calls to 'state' (many IO operations) - [ ] GUI. Drag and drop the new folder into the app window - [ ] GUI. Implement some other methods for Qt abstract models - - [ ] GUI. Warning on 'Clean' action - - [ ] GUI. On 'Clean' clean the log too - - [ ] GUI. Stop the chain of commands if someone drops -1 or an exception + - [x] GUI. Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) + - [x] GUI. On 'Clean' clean the log too + - [x] GUI. Stop the chain of commands if someone drops -1 or an exception + - [ ] GUI. 2 types of logging formatters for 2 verbosity levels + - [ ] GUI. Check for projects duplication + - [ ] GUI. Projects are not destructed until quit (something preserving the link probably...) - [ ] Create VSCode plugin - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') @@ -30,3 +33,4 @@ - [ ] Mb store the last occurred exception traceback in .ini file and show on some CLI command (so we don't necessarily need to turn on the verbose mode). And, in general, we should show the error reason right off - [ ] 'verbose' and 'non-verbose' tests as `subTest` (also `should_log_error_...`) - [ ] the lib sometimes raising, sometimes returning the code and it is not consistent. While the reasons behind such behaviour are clear, would be great to always return a result code and raise the exceptions in the outer scope, if there is need to + - [ ] check board (no sense to go further on 'new' if the board in config.ini is not correct) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index af4fed6..a0b21ce 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -4,6 +4,7 @@ # from __future__ import annotations import collections +import functools import logging import pathlib import sys @@ -101,7 +102,10 @@ class ProjectListItem(QObject): stageChanged = Signal() logAdded = Signal(str, int, arguments=['message', 'level']) # send the log message to the front-end + + actionStarted = Signal(str, arguments=['action']) actionDone = Signal(str, bool, arguments=['action', 'success']) # emit when the action has executed + actionRunningChanged = Signal() def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None): @@ -121,6 +125,7 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren self.workers_pool = QThreadPool() self.workers_pool.setMaxThreadCount(1) self.workers_pool.setExpiryTimeout(-1) # tasks forever wait for the available spot + self._is_action_running = False # These values are valid till the Stm32pio project does not initialize itself (or failed to) self.project = None @@ -204,6 +209,23 @@ def current_stage(self): else: return self._current_stage + @Property(bool, notify=actionRunningChanged) + def actionRunning(self): + return self._is_action_running + + @Slot(str) + def actionStartedSlot(self, action: str): + self.actionStarted.emit(action) + self._is_action_running = True + self.actionRunningChanged.emit() + + @Slot(str, bool) + def actionDoneSlot(self, action: str, success: bool): + if not success: + self.workers_pool.clear() # clear the queue - prevent further execution + self._is_action_running = False + self.actionRunningChanged.emit() + self.actionDone.emit(action, success) @Slot() def qmlLoaded(self): @@ -225,9 +247,10 @@ def run(self, action: str, args: list): """ worker = ProjectActionWorker(getattr(self.project, action), args, self.logger) + worker.actionStarted.connect(self.actionStartedSlot) + worker.actionDone.connect(self.actionDoneSlot) worker.actionDone.connect(self.stateChanged) worker.actionDone.connect(self.stageChanged) - worker.actionDone.connect(self.actionDone) self.workers_pool.start(worker) # will automatically place to the queue @@ -239,6 +262,7 @@ class ProjectActionWorker(QObject, QRunnable): second is compatible with QThreadPool. """ + actionStarted = Signal(str, arguments=['action']) actionDone = Signal(str, bool, arguments=['action', 'success']) def __init__(self, func, args: list = None, logger: logging.Logger = None, parent: QObject = None): @@ -253,19 +277,30 @@ def __init__(self, func, args: list = None, logger: logging.Logger = None, paren self.args = args self.name = func.__name__ + def run(self): + self.actionStarted.emit(self.name) # notify the caller + try: result = self.func(*self.args) except Exception as e: if self.logger is not None: self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) result = -1 + if result is None or (type(result) == int and result == 0): success = True else: success = False + self.actionDone.emit(self.name, success) # notify the caller + if not success: + # Pause the thread and, therefore, the parent QThreadPool queue so the caller can decide whether we should + # proceed or stop. This should not cause any problems as we've already perform all necessary tasks and this + # just delaying the QRunnable removal + time.sleep(1.0) + diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 3129a79..25e68e8 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -190,18 +190,13 @@ ApplicationWindow { Loader { onLoaded: setInitInfo(index) sourceComponent: RowLayout { - id: projectsListItem - property bool initloading: true // initial waiting for the backend-side - property bool actionRunning: false + property bool initLoading: true // initial waiting for the backend-side property ProjectListItem project: projectsModel.getProject(index) Connections { target: project // (newbie hint) sender // Currently, this event is equivalent to the complete initialization of the backend side of the project onNameChanged: { - initloading = false; - } - onActionDone: { - actionRunning = false; + initLoading = false; } } ColumnLayout { @@ -236,7 +231,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height Layout.preferredHeight: parent.height - running: projectsListItem.initloading || projectsListItem.actionRunning + running: parent.initLoading || project.actionRunning } MouseArea { @@ -244,7 +239,7 @@ ApplicationWindow { y: parent.y width: parent.width height: parent.height - enabled: !parent.initloading + enabled: !parent.initLoading onClicked: { projectsListView.currentIndex = index; projectsWorkspaceView.currentIndex = index; @@ -325,10 +320,6 @@ ApplicationWindow { } // Currently, this event is equivalent to the complete initialization of the backend side of the project onNameChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - projActionsRow.children[i].enabled = true; - } - const state = project.state; const completedStages = Object.keys(state).filter(stateName => state[stateName]); if (completedStages.length === 1 && completedStages[0] === 'EMPTY') { @@ -341,12 +332,46 @@ ApplicationWindow { } /* + Detect and reflect changes of a project outside of the app + */ + QtDialogs.MessageDialog { + id: projectIncorrectDialog + text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
+ The project will be removed from the app. It will not affect any real content` + icon: QtDialogs.StandardIcon.Critical + onAccepted: { + moveToNextAndRemove(); + projActionsButtonGroup.lock = false; + } + } + + /* + Index: 0. Project initialization "screen" + Prompt a user to perform initial setup */ Loader { id: initScreenLoader active: false sourceComponent: Column { + signal stateReceived() + property bool lock: false // TODO: is it necessary? mb make a dialog modal or smth. + onStateReceived: { // TODO: cache state! + if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock) { + const state = project.state; + if (!state['EMPTY']) { + lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialog.open(); + } + } + } + Component.onCompleted: { + // Several events lead to a single handler: + // - the project was selected in the list + // - the app window has got the focus + projectsWorkspaceView.currentIndexChanged.connect(stateReceived); + mainWindow.activeChanged.connect(stateReceived); + } Text { text: "To complete initialization you can provide PlatformIO name of the board" padding: 10 @@ -356,6 +381,7 @@ ApplicationWindow { spacing: 10 ComboBox { id: board + width: 200 editable: true model: boardsModel // backend-side (simple string model) textRole: 'display' @@ -410,7 +436,7 @@ ApplicationWindow { id: openEditor text: 'Open editor' ToolTip { - text: 'Start the editor specified in the Settings after the completion' + text: "Start the editor specified in the Settings after the completion" visible: openEditor.hovered } } @@ -422,28 +448,26 @@ ApplicationWindow { topPadding: 20 leftPadding: 18 onClicked: { - // All operations will be queued - projectsListView.currentItem.item.actionRunning = true; - + // All 'run' operations will be queued project.run('save_config', [{ 'project': { 'board': board.editText === board.textAt(0) ? '' : board.editText } }]); + if (board.editText === board.textAt(0)) { + project.logAdded("WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO \ + project creation. You can set it in 'stm32pio.ini' file in the project directory", + Logging.WARNING); + } if (runCheckBox.checked) { - for (let i = 3; i < buttonsModel.count - 1; ++i) { - buttonsModel.setProperty(i, 'shouldRunNext', true); + for (let i = 3; i < buttonsModel.count; ++i) { + project.run(buttonsModel.get(i).action, []); } - projActionsRow.children[3].clicked(); } if (openEditor.checked) { - if (runCheckBox.checked) { - buttonsModel.setProperty(buttonsModel.count - 1, 'shouldStartEditor', true); - } else { - projActionsRow.children[1].clicked(); - } + project.run('start_editor', [settings.get('editor')]); } mainOrInitScreen.currentIndex = 1; // go to main screen @@ -453,25 +477,13 @@ ApplicationWindow { } } + /* + Index: 1. Main "screen" + */ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - /* - Detect and reflect changes of a project outside of the app - */ - QtDialogs.MessageDialog { - // TODO: case: .ioc file can be removed on init stage too (i.e. when initDialog is active) - id: projectIncorrectDialog - text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
- The project will be removed from the app. It will not affect any real content` - icon: QtDialogs.StandardIcon.Critical - onAccepted: { - moveToNextAndRemove(); - projActionsButtonGroup.lock = false; - } - } - /* Show this or action buttons */ @@ -494,16 +506,16 @@ ApplicationWindow { id: projActionsButtonGroup buttons: projActionsRow.children signal stateReceived() - signal actionDone(string actionDone, bool success) + signal actionStarted(string actionName) + signal actionDone(string actionName, bool success) + signal nameChanged() property bool lock: false // TODO: is it necessary? mb make a dialog modal or smth. onStateReceived: { // TODO: cache state! - if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock) { + if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock && !project.actionRunning) { const state = project.state; project.stageChanged(); - if (state['LOADING']) { - // - } else if (state['INIT_ERROR']) { + if (state['INIT_ERROR']) { projActionsRow.visible = false; initErrorMessage.visible = true; } else if (!state['EMPTY']) { @@ -511,25 +523,33 @@ ApplicationWindow { projectIncorrectDialog.open(); } else if (state['EMPTY']) { for (let i = 0; i < buttonsModel.count; ++i) { - projActionsRow.children[i].palette.button = 'lightgray'; - if (state[buttonsModel.get(i).state]) { + if (state[buttonsModel.get(i).stateRepresented]) { projActionsRow.children[i].palette.button = 'lightgreen'; + } else { + projActionsRow.children[i].palette.button = 'lightgray'; } } } } } - onActionDone: { + onNameChanged: { for (let i = 0; i < buttonsModel.count; ++i) { + // Looks like 'enabled' property should be managed from the outside of the element + // (i.e there, not in the button itself) projActionsRow.children[i].enabled = true; } } - onClicked: { + onActionStarted: { for (let i = 0; i < buttonsModel.count; ++i) { projActionsRow.children[i].enabled = false; projActionsRow.children[i].glowVisible = false; } } + onActionDone: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = true; + } + } Component.onCompleted: { // Several events lead to a single handler: // - the state has changed and explicitly informs about it @@ -539,7 +559,9 @@ ApplicationWindow { projectsWorkspaceView.currentIndexChanged.connect(stateReceived); mainWindow.activeChanged.connect(stateReceived); + project.actionStarted.connect(actionStarted); project.actionDone.connect(actionDone); + project.nameChanged.connect(nameChanged); } } RowLayout { @@ -553,7 +575,7 @@ ApplicationWindow { ListElement { name: 'Clean' action: 'clean' - shouldStartEditor: false + tooltip: "WARNING: this will delete ALL content of the project folder except the current .ioc file and clear all logs" } ListElement { name: 'Open editor' @@ -562,38 +584,28 @@ ApplicationWindow { } ListElement { name: 'Initialize' - state: 'INITIALIZED' + stateRepresented: 'INITIALIZED' // the project state that this button is representing action: 'save_config' - shouldRunNext: false - shouldStartEditor: false } ListElement { name: 'Generate' - state: 'GENERATED' + stateRepresented: 'GENERATED' action: 'generate_code' - shouldRunNext: false - shouldStartEditor: false } ListElement { name: 'Init PlatformIO' - state: 'PIO_INITIALIZED' + stateRepresented: 'PIO_INITIALIZED' action: 'pio_init' - shouldRunNext: false - shouldStartEditor: false } ListElement { name: 'Patch' - state: 'PATCHED' + stateRepresented: 'PATCHED' action: 'patch' - shouldRunNext: false - shouldStartEditor: false } ListElement { name: 'Build' - state: 'BUILT' + stateRepresented: 'BUILT' action: 'build' - shouldRunNext: false - shouldStartEditor: false } } delegate: Button { @@ -601,24 +613,36 @@ ApplicationWindow { Layout.rightMargin: model.margin enabled: false // turn on after project initialization property alias glowVisible: glow.visible - function runOwnAction() { - projectsListView.currentItem.item.actionRunning = true; - palette.button = 'gold'; - let args = []; // JS array cannot be attached to a ListElement (at least in a non-hacky manner) - if (model.action === 'start_editor') { - args.push(settings.get('editor')); + onClicked: { + const args = []; // JS array cannot be attached to a ListElement (at least in a non-hacky manner) + switch (model.action) { + case 'start_editor': + args.push(settings.get('editor')); + break; + case 'clean': + log.clear(); + break; + default: + break; } project.run(model.action, args); } - onClicked: { - runOwnAction(); + ToolTip { + visible: parent.hovered + Component.onCompleted: { + if (model.tooltip) { + text = model.tooltip; + } else { + this.destroy(); + } + } } /* Detect modifier keys: - Ctrl: start the editor after an operation(s) - Shift: continuous actions run */ - MouseArea { + MouseArea { // TODO: overlays the button so the pressed state (darker color) is not shown anchors.fill: parent hoverEnabled: true property bool ctrlPressed: false @@ -635,18 +659,15 @@ ApplicationWindow { } } onClicked: { - if (ctrlPressed && model.action !== 'start_editor') { - model.shouldStartEditor = true; - } if (shiftPressed && index >= 2) { - // run all actions in series for (let i = 2; i < index; ++i) { - buttonsModel.setProperty(i, 'shouldRunNext', true); + project.run(buttonsModel.get(i).action, []); } - projActionsRow.children[2].clicked(); - return; } parent.clicked(); // propagateComposedEvents doesn't work... + if (ctrlPressed && model.action !== 'start_editor') { + project.run('start_editor', [settings.get('editor')]); + } } onPositionChanged: { ctrlPressed = mouse.modifiers & Qt.ControlModifier; // bitwise AND @@ -661,10 +682,16 @@ ApplicationWindow { } } onEntered: { - statusBar.text = - `Ctrl-click to open the editor specified in the Settings after the operation, - Shift-click to perform all actions prior this one (including). - Ctrl-Shift-click for both`; + if (model.action !== 'start_editor') { + let preparedText = + `Ctrl-click to open the editor specified in the Settings after the operation`; + if (index >= 2) { + preparedText += + `, Shift-click to perform all actions prior this one (including). + Ctrl-Shift-click for both`; + } + statusBar.text = preparedText; + } } onExited: { statusBar.text = ''; @@ -681,8 +708,13 @@ ApplicationWindow { } Connections { target: projActionsButtonGroup + onActionStarted: { + if (actionName === model.action) { + palette.button = 'gold'; + } + } onActionDone: { - if (actionDone === model.action) { + if (actionName === model.action) { if (success) { glow.color = 'lightgreen'; } else { @@ -690,22 +722,6 @@ ApplicationWindow { glow.color = 'lightcoral'; } glow.visible = true; - - if (model.shouldRunNext) { - model.shouldRunNext = false; - projActionsRow.children[index + 1].clicked(); - } - - if (model.shouldStartEditor) { - model.shouldStartEditor = false; - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).action === 'start_editor') { - // Use runOwnAction for no additional actions in parent handlers - projActionsRow.children[i].runOwnAction(); - break; - } - } - } } } } diff --git a/stm32pio/app.py b/stm32pio/app.py index e135dfa..ef8fd64 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -23,9 +23,9 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: """ parser = argparse.ArgumentParser(description="Automation of creating and updating STM32CubeMX-PlatformIO projects. " - "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Run " - "'init' command to create config file and set the path to STM32CubeMX " - "and other tools (if defaults doesn't work for you)") + "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Visit " + "https://github.com/ussserrr/stm32pio for more information. Use " + "'help' command to take a glimpse on the available functionality") # Global arguments (there is also an automatically added '-h, --help' option) parser.add_argument('--version', action='version', version=f"stm32pio v{__version__}") parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count') @@ -35,7 +35,7 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: parser_init = subparsers.add_parser('init', help="create config .ini file so you can tweak parameters before " "proceeding") - parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project") + parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project, glue them") parser_generate = subparsers.add_parser('generate', help="generate CubeMX code only") parser_status = subparsers.add_parser('status', help="get the description of the current project state") parser_clean = subparsers.add_parser('clean', help="clean-up the project (delete ALL content of 'path' " From 29660a2ee59ece4328383643532ec0793c501607 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 22 Mar 2020 23:08:49 +0300 Subject: [PATCH 02/20] GUI working --- stm32pio-gui/app.py | 1 + stm32pio-gui/main.qml | 211 +++++++++++++++++++++--------------------- 2 files changed, 108 insertions(+), 104 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index a0b21ce..67b341c 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -195,6 +195,7 @@ def name(self): @Property('QVariant', notify=stateChanged) def state(self): + # print(time.time(), self.project.path.name) if self.project is not None: # Convert to normal dict (JavaScript object) and exclude UNDEFINED key return { stage.name: value for stage, value in self.project.state.items() diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 25e68e8..e1c32a7 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -288,14 +288,17 @@ ApplicationWindow { Layout.leftMargin: 5 Layout.rightMargin: 10 Layout.topMargin: 10 - // clip: true // do not use as it'll clip glow animation Repeater { // Use similar to ListView pattern (same projects model, Loader component) model: projectsModel delegate: Component { Loader { - onLoaded: setInitInfo(index) + property int projectIndex: -1 + onLoaded: { + projectIndex = index; + setInitInfo(projectIndex) + } /* Use another one StackLayout to separate Project initialization "screen" and Main one */ @@ -307,6 +310,7 @@ ApplicationWindow { Layout.fillHeight: true property ProjectListItem project: projectsModel.getProject(index) + Connections { target: project // sender onLogAdded: { @@ -332,18 +336,41 @@ ApplicationWindow { } /* - Detect and reflect changes of a project outside of the app + Detect changes of a project outside of the app */ QtDialogs.MessageDialog { id: projectIncorrectDialog text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
- The project will be removed from the app. It will not affect any real content` + The project will be removed from the app. It will not affect any real content` icon: QtDialogs.StandardIcon.Critical onAccepted: { moveToNextAndRemove(); - projActionsButtonGroup.lock = false; + mainOrInitScreen.projectIncorrectDialogIsOpen = false; } } + signal handleState() + property bool projectIncorrectDialogIsOpen: false + property var stateCached: ({}) + onHandleState: { + if (mainWindow.active && (projectIndex === projectsWorkspaceView.currentIndex) && !projectIncorrectDialogIsOpen && !project.actionRunning) { + const state = project.state; + project.stageChanged(); + stateCached = state; + + if (!state['EMPTY']) { // i.e. no .ioc file, see backend code + // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialogIsOpen = true; + projectIncorrectDialog.open(); + } + } + } + Component.onCompleted: { + // Several events lead to a single handler + project.stateChanged.connect(handleState); + projectsWorkspaceView.currentIndexChanged.connect(handleState); // the project was selected in the list + mainWindow.activeChanged.connect(handleState); // the app window has got the focus + } + /* Index: 0. Project initialization "screen" @@ -354,24 +381,6 @@ ApplicationWindow { id: initScreenLoader active: false sourceComponent: Column { - signal stateReceived() - property bool lock: false // TODO: is it necessary? mb make a dialog modal or smth. - onStateReceived: { // TODO: cache state! - if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock) { - const state = project.state; - if (!state['EMPTY']) { - lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialog.open(); - } - } - } - Component.onCompleted: { - // Several events lead to a single handler: - // - the project was selected in the list - // - the app window has got the focus - projectsWorkspaceView.currentIndexChanged.connect(stateReceived); - mainWindow.activeChanged.connect(stateReceived); - } Text { text: "To complete initialization you can provide PlatformIO name of the board" padding: 10 @@ -412,7 +421,7 @@ ApplicationWindow { visible: runCheckBox.hovered Component.onCompleted: { const actions = []; - for (let i = 3; i < buttonsModel.count; ++i) { + for (let i = buttonsModel.statefulActionsStartIndex; i < buttonsModel.count; ++i) { actions.push(`${buttonsModel.get(i).name}`); } text = `Do: ${actions.join(' → ')}`; @@ -461,7 +470,7 @@ ApplicationWindow { } if (runCheckBox.checked) { - for (let i = 3; i < buttonsModel.count; ++i) { + for (let i = buttonsModel.statefulActionsStartIndex + 1; i < buttonsModel.count; ++i) { project.run(buttonsModel.get(i).action, []); } } @@ -484,6 +493,14 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true + property var stateCachedNotifier: stateCached + onStateCachedNotifierChanged: { + if (stateCached['INIT_ERROR']) { + projActionsRow.visible = false; + initErrorMessage.visible = true; + } + } + /* Show this or action buttons */ @@ -502,76 +519,36 @@ ApplicationWindow { - yellow: in progress right now - red: an error has occured during the last execution */ - ButtonGroup { - id: projActionsButtonGroup - buttons: projActionsRow.children - signal stateReceived() - signal actionStarted(string actionName) - signal actionDone(string actionName, bool success) - signal nameChanged() - property bool lock: false // TODO: is it necessary? mb make a dialog modal or smth. - onStateReceived: { // TODO: cache state! - if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock && !project.actionRunning) { - const state = project.state; - project.stageChanged(); - - if (state['INIT_ERROR']) { - projActionsRow.visible = false; - initErrorMessage.visible = true; - } else if (!state['EMPTY']) { - lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialog.open(); - } else if (state['EMPTY']) { - for (let i = 0; i < buttonsModel.count; ++i) { - if (state[buttonsModel.get(i).stateRepresented]) { - projActionsRow.children[i].palette.button = 'lightgreen'; - } else { - projActionsRow.children[i].palette.button = 'lightgray'; - } - } - } - } - } - onNameChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - // Looks like 'enabled' property should be managed from the outside of the element - // (i.e there, not in the button itself) - projActionsRow.children[i].enabled = true; - } - } - onActionStarted: { - for (let i = 0; i < buttonsModel.count; ++i) { - projActionsRow.children[i].enabled = false; - projActionsRow.children[i].glowVisible = false; - } - } - onActionDone: { - for (let i = 0; i < buttonsModel.count; ++i) { - projActionsRow.children[i].enabled = true; - } - } - Component.onCompleted: { - // Several events lead to a single handler: - // - the state has changed and explicitly informs about it - // - the project was selected in the list - // - the app window has got the focus - project.stateChanged.connect(stateReceived); - projectsWorkspaceView.currentIndexChanged.connect(stateReceived); - mainWindow.activeChanged.connect(stateReceived); - - project.actionStarted.connect(actionStarted); - project.actionDone.connect(actionDone); - project.nameChanged.connect(nameChanged); - } - } RowLayout { id: projActionsRow Layout.fillWidth: true Layout.bottomMargin: 7 z: 1 // for the glowing animation + Connections { + target: project + onNameChanged: { + for (let i = 0; i < buttonsModel.count; ++i) { + // Looks like 'enabled' property should be managed from outside of the element + // (i.e there, not in the button itself) + projActionsRow.children[i].enabled = true; + } + } + onActionStarted: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = false; + projActionsRow.children[i].glowVisible = false; + } + } + onActionDone: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = true; + } + } + } Repeater { model: ListModel { id: buttonsModel + readonly property int statefulActionsStartIndex: 2 ListElement { name: 'Clean' action: 'clean' @@ -609,10 +586,22 @@ ApplicationWindow { } } delegate: Button { - text: name + text: model.name Layout.rightMargin: model.margin enabled: false // turn on after project initialization property alias glowVisible: glow.visible + property var stateCachedNotifier: stateCached + onStateCachedNotifierChanged: { + if (stateCached[model.stateRepresented]) { + palette.button = 'lightgreen'; + } else { + palette.button = 'lightgray'; + } + } + property int buttonIndex: -1 + Component.onCompleted: { + buttonIndex = index; + } onClicked: { const args = []; // JS array cannot be attached to a ListElement (at least in a non-hacky manner) switch (model.action) { @@ -637,12 +626,24 @@ ApplicationWindow { } } } + property string currentColor: '' + function highlight(flag) { + if (flag) { + if (!currentColor) { + currentColor = palette.button; + } + palette.button = Qt.lighter('lightgreen', 1.2); + } else { + palette.button = currentColor; + currentColor = ''; + } + } /* Detect modifier keys: - - Ctrl: start the editor after an operation(s) + - Ctrl (Cmd): start the editor after an operation(s) - Shift: continuous actions run */ - MouseArea { // TODO: overlays the button so the pressed state (darker color) is not shown + MouseArea { anchors.fill: parent hoverEnabled: true property bool ctrlPressed: false @@ -650,21 +651,20 @@ ApplicationWindow { property bool shiftPressed: false property bool shiftPressedLastState: false function shiftHandler() { - for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... - if (shiftPressed) { - projActionsRow.children[i].palette.button = Qt.lighter('lightgreen', 1.2); - } else { - projActionsButtonGroup.stateReceived(); - } + for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + projActionsRow.children[i].highlight(shiftPressed); } } onClicked: { - if (shiftPressed && index >= 2) { - for (let i = 2; i < index; ++i) { + if (shiftPressed && buttonIndex >= buttonsModel.statefulActionsStartIndex) { + for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + projActionsRow.children[i].highlight(false); + } + for (let i = buttonsModel.statefulActionsStartIndex; i < buttonIndex; ++i) { project.run(buttonsModel.get(i).action, []); } } - parent.clicked(); // propagateComposedEvents doesn't work... + parent.clicked(); if (ctrlPressed && model.action !== 'start_editor') { project.run('start_editor', [settings.get('editor')]); } @@ -685,10 +685,10 @@ ApplicationWindow { if (model.action !== 'start_editor') { let preparedText = `Ctrl-click to open the editor specified in the Settings after the operation`; - if (index >= 2) { + if (buttonIndex >= buttonsModel.statefulActionsStartIndex) { preparedText += `, Shift-click to perform all actions prior this one (including). - Ctrl-Shift-click for both`; + Ctrl-Shift-click for both`; } statusBar.text = preparedText; } @@ -707,14 +707,14 @@ ApplicationWindow { } } Connections { - target: projActionsButtonGroup + target: project onActionStarted: { - if (actionName === model.action) { + if (action === model.action) { palette.button = 'gold'; } } onActionDone: { - if (actionName === model.action) { + if (action === model.action) { if (success) { glow.color = 'lightgreen'; } else { @@ -741,6 +741,9 @@ ApplicationWindow { SequentialAnimation { id: glowAnimation loops: 3 + onStopped: { + glow.visible = false; + } OpacityAnimator { target: glow from: 0 From eec3956a126f573543a65e0ddc83e50d6b1ee6d9 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 23 Mar 2020 00:23:43 +0300 Subject: [PATCH 03/20] test on Linux, small adjustments --- TODO.md | 1 + stm32pio-gui/app.py | 15 ++++++++++----- stm32pio-gui/main.qml | 5 +++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index d616199..98448b9 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,7 @@ - [ ] GUI. 2 types of logging formatters for 2 verbosity levels - [ ] GUI. Check for projects duplication - [ ] GUI. Projects are not destructed until quit (something preserving the link probably...) + - [ ] GUI. Fix settings (window doesn't match real) - [ ] Create VSCode plugin - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 67b341c..a6a54ed 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -197,18 +197,23 @@ def name(self): def state(self): # print(time.time(), self.project.path.name) if self.project is not None: + state = self.project.state + + # Side-effect: caching the current stage at the same time to avoid the flooding of calls to the 'state' + # getter (many IO operations). Requests to 'state' and 'stage' are usually goes together so there is no need + # to necessarily keeps them separated + self._current_stage = str(state.current_stage) + # Convert to normal dict (JavaScript object) and exclude UNDEFINED key - return { stage.name: value for stage, value in self.project.state.items() + return { stage.name: value for stage, value in state.items() if stage != stm32pio.lib.ProjectStage.UNDEFINED } + else: return self._state @Property(str, notify=stageChanged) def current_stage(self): - if self.project is not None: - return str(self.project.state.current_stage) - else: - return self._current_stage + return self._current_stage @Property(bool, notify=actionRunningChanged) def actionRunning(self): diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index e1c32a7..b09617d 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -354,10 +354,11 @@ ApplicationWindow { onHandleState: { if (mainWindow.active && (projectIndex === projectsWorkspaceView.currentIndex) && !projectIncorrectDialogIsOpen && !project.actionRunning) { const state = project.state; - project.stageChanged(); stateCached = state; - if (!state['EMPTY']) { // i.e. no .ioc file, see backend code + project.stageChanged(); // side-effect: get the stage at the same time + + if (!state['INIT_ERROR'] && !state['EMPTY']) { // i.e. no .ioc file but the project was able to initialize itself // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) projectIncorrectDialogIsOpen = true; projectIncorrectDialog.open(); From f094d2343401cb385b694e01e67a2df47df4ac82 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 24 Mar 2020 23:41:28 +0300 Subject: [PATCH 04/20] fix settings dialog --- TODO.md | 3 ++- stm32pio-gui/main.qml | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index 98448b9..03167ea 100644 --- a/TODO.md +++ b/TODO.md @@ -12,8 +12,9 @@ - [x] GUI. Stop the chain of commands if someone drops -1 or an exception - [ ] GUI. 2 types of logging formatters for 2 verbosity levels - [ ] GUI. Check for projects duplication + - [ ] GUI. Use QML State - [ ] GUI. Projects are not destructed until quit (something preserving the link probably...) - - [ ] GUI. Fix settings (window doesn't match real) + - [x] GUI. Fix settings (window doesn't match real) - [ ] Create VSCode plugin - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index b09617d..1854bf8 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -56,7 +56,6 @@ ApplicationWindow { } TextField { id: editor - text: settings.get('editor') } Label { @@ -66,7 +65,13 @@ ApplicationWindow { CheckBox { id: verbose leftPadding: -3 - checked: settings.get('verbose') + } + } + // Set UI values there so they are always reflect actual parameters + onVisibleChanged: { + if (visible) { + editor.text = settings.get('editor'); + verbose.checked = settings.get('verbose'); } } onAccepted: { @@ -537,12 +542,14 @@ ApplicationWindow { onActionStarted: { for (let i = 0; i < buttonsModel.count; ++i) { projActionsRow.children[i].enabled = false; + projActionsRow.children[i].palette.buttonText = 'darkgray'; projActionsRow.children[i].glowVisible = false; } } onActionDone: { for (let i = 0; i < buttonsModel.count; ++i) { projActionsRow.children[i].enabled = true; + projActionsRow.children[i].palette.buttonText = 'black'; } } } @@ -598,6 +605,7 @@ ApplicationWindow { } else { palette.button = 'lightgray'; } + palette.buttonText = 'black'; } property int buttonIndex: -1 Component.onCompleted: { @@ -627,7 +635,7 @@ ApplicationWindow { } } } - property string currentColor: '' + property string currentColor: '' // for highlighting only function highlight(flag) { if (flag) { if (!currentColor) { @@ -706,6 +714,12 @@ ApplicationWindow { shiftHandler(); } } + onPressed: { + palette.button = Qt.darker(palette.button, 1.2); + } + onReleased: { + palette.button = Qt.lighter(palette.button, 1.2);; + } } Connections { target: project From fddf22e0b8c0d0778298f415563361640ded4247 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 29 Mar 2020 22:55:12 +0300 Subject: [PATCH 05/20] GUI installation process for setup.py, GUI fixes and improvements --- TODO.md | 12 +- setup.py | 12 +- stm32pio/__main__.py | 1 + stm32pio/lib.py | 2 +- {stm32pio-gui => stm32pio_gui}/README.md | 0 {stm32pio-gui => stm32pio_gui}/__init__.py | 0 stm32pio_gui/__main__.py | 7 + {stm32pio-gui => stm32pio_gui}/app.py | 220 +++++++++++------- {stm32pio-gui => stm32pio_gui}/icons/LICENSE | 0 {stm32pio-gui => stm32pio_gui}/icons/add.svg | 82 +++---- {stm32pio-gui => stm32pio_gui}/icons/icon.svg | 0 .../icons/remove.svg | 80 +++---- {stm32pio-gui => stm32pio_gui}/main.qml | 25 +- 13 files changed, 261 insertions(+), 180 deletions(-) rename {stm32pio-gui => stm32pio_gui}/README.md (100%) rename {stm32pio-gui => stm32pio_gui}/__init__.py (100%) create mode 100644 stm32pio_gui/__main__.py rename {stm32pio-gui => stm32pio_gui}/app.py (74%) rename {stm32pio-gui => stm32pio_gui}/icons/LICENSE (100%) rename {stm32pio-gui => stm32pio_gui}/icons/add.svg (94%) rename {stm32pio-gui => stm32pio_gui}/icons/icon.svg (100%) rename {stm32pio-gui => stm32pio_gui}/icons/remove.svg (93%) rename {stm32pio-gui => stm32pio_gui}/main.qml (96%) diff --git a/TODO.md b/TODO.md index 03167ea..60fce5d 100644 --- a/TODO.md +++ b/TODO.md @@ -4,17 +4,19 @@ - [ ] Arduino framework support (needs research to check if it is possible) - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [ ] GUI. Tests (research approaches and patterns) - - [ ] GUI. Reduce number of calls to 'state' (many IO operations) + - [x] GUI. Reduce number of calls to 'state' (many IO operations) - [ ] GUI. Drag and drop the new folder into the app window - - [ ] GUI. Implement some other methods for Qt abstract models + - [ ] GUI. Implement other methods for Qt abstract models - [x] GUI. Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) - [x] GUI. On 'Clean' clean the log too - [x] GUI. Stop the chain of commands if someone drops -1 or an exception - [ ] GUI. 2 types of logging formatters for 2 verbosity levels - [ ] GUI. Check for projects duplication - - [ ] GUI. Use QML State - - [ ] GUI. Projects are not destructed until quit (something preserving the link probably...) + - [ ] GUI. Maybe use QML State for action buttons appearance + - [x] GUI. Projects are not destructed until quit (something preserving the link probably...) - [x] GUI. Fix settings (window doesn't match real) + - [x] GUI. TypeError: Cannot read property 'actionRunning' of null + - [ ] GUI. QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - [ ] Create VSCode plugin - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') @@ -29,7 +31,7 @@ - [ ] `__init__`' `parameters` dict argument schema (Python 3.8 feature). - [ ] See https://docs.python.org/3/howto/logging-cookbook.html#context-info to maybe remade current logging schema (current is, perhaps, a cause of the strange error while testing (in the logging thread)) - [ ] UML diagrams (core, GUI back- and front-ends) - - [ ] CI is possible (Arch's AUR has the STM32CubeMX package, also there is a direct link). Deploy Docker in Azure Pipelines, basic at Travis CI + - [ ] CI is possible (Arch's AUR has the STM32CubeMX package, also there is a direct link). Deploy Docker one in Azure Pipelines, basic at Travis CI - [ ] Test preserving user files and folders on regeneration and mb other operations - [ ] Move special formatters inside the library. It is an implementation detail actually that we use subprocesses and so on - [ ] Mb store the last occurred exception traceback in .ini file and show on some CLI command (so we don't necessarily need to turn on the verbose mode). And, in general, we should show the error reason right off diff --git a/setup.py b/setup.py index 7ce4b28..b53bffd 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'tests' ] ), + include_package_data=True, classifiers=[ "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License", @@ -50,10 +51,19 @@ setup_requires=[ 'wheel' ], - include_package_data=True, + # GUI module (stm32pio_gui) will be installed in any case (because find_packages() will find it) but its + # dependencies will be installed only when this option is given + extras_require={ + 'GUI': [ + 'PySide2' + ] + }, entry_points={ 'console_scripts': [ 'stm32pio = stm32pio.app:main' + ], + 'gui_scripts': [ + 'stm32pio_gui = stm32pio_gui.app:main [GUI]' ] } ) diff --git a/stm32pio/__main__.py b/stm32pio/__main__.py index b424b14..1c9b88c 100644 --- a/stm32pio/__main__.py +++ b/stm32pio/__main__.py @@ -2,5 +2,6 @@ import stm32pio.app + if __name__ == '__main__': sys.exit(stm32pio.app.main()) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 49c2f08..4a0ac92 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -329,7 +329,7 @@ def _save_config(config: configparser.ConfigParser, path: pathlib.Path, logger: def save_config(self, parameters: dict = None) -> int: """ - Invokes base _save_config function. Preliminarily, updates the config with given parameters dictionary. It + Invokes base _save_config function. Preliminarily, updates the config with the given 'parameters' dictionary. It should has the following format: { 'section1_name': { diff --git a/stm32pio-gui/README.md b/stm32pio_gui/README.md similarity index 100% rename from stm32pio-gui/README.md rename to stm32pio_gui/README.md diff --git a/stm32pio-gui/__init__.py b/stm32pio_gui/__init__.py similarity index 100% rename from stm32pio-gui/__init__.py rename to stm32pio_gui/__init__.py diff --git a/stm32pio_gui/__main__.py b/stm32pio_gui/__main__.py new file mode 100644 index 0000000..619d9a0 --- /dev/null +++ b/stm32pio_gui/__main__.py @@ -0,0 +1,7 @@ +import sys + +import stm32pio_gui.app + + +if __name__ == '__main__': + sys.exit(stm32pio_gui.app.main()) diff --git a/stm32pio-gui/app.py b/stm32pio_gui/app.py similarity index 74% rename from stm32pio-gui/app.py rename to stm32pio_gui/app.py index a6a54ed..de8e126 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio_gui/app.py @@ -4,31 +4,40 @@ # from __future__ import annotations import collections -import functools +import gc import logging import pathlib +import platform import sys import threading import time import weakref -sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) -import stm32pio.settings -import stm32pio.lib -import stm32pio.util - -from PySide2.QtCore import QUrl, Property, QAbstractListModel, QModelIndex, QObject, Qt, Slot, Signal, QThread,\ - qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, QThreadPool, QRunnable,\ - QStringListModel, QSettings -if stm32pio.settings.my_os == 'Linux': - # Most UNIX systems does not provide QtDialogs implementation... - from PySide2.QtWidgets import QApplication -else: - from PySide2.QtGui import QGuiApplication -from PySide2.QtGui import QIcon -from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine - - +try: + from PySide2.QtCore import QUrl, Property, QAbstractListModel, QModelIndex, QObject, Qt, Slot, Signal, QThread,\ + qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, QThreadPool, QRunnable,\ + QStringListModel, QSettings + if platform.system() == 'Linux': + # Most UNIX systems does not provide QtDialogs implementation... + from PySide2.QtWidgets import QApplication + else: + from PySide2.QtGui import QGuiApplication + from PySide2.QtGui import QIcon + from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine +except IndexError as e: + print(e) + print("\nGUI version requires PySide2 to be installed. You can re-install stm32pio as 'pip install stm32pio[GUI]' " + "or manually install its dependencies by yourself") + +try: + import stm32pio.settings + import stm32pio.lib + import stm32pio.util +except ModuleNotFoundError: + sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) + import stm32pio.settings + import stm32pio.lib + import stm32pio.util @@ -83,10 +92,9 @@ def routine(self) -> None: The worker constantly querying the buffer on the new log messages availability. """ while not self.stopped.wait(timeout=0.050): - if self.can_flush_log.is_set(): - if len(self.buffer): - record = self.buffer.popleft() - self.sendLog.emit(self.logging_handler.format(record), record.levelno) + if self.can_flush_log.is_set() and len(self.buffer): + record = self.buffer.popleft() + self.sendLog.emit(self.logging_handler.format(record), record.levelno) module_logger.debug('exit logging worker') self.thread.quit() @@ -122,7 +130,7 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren self.logging_worker.sendLog.connect(self.logAdded) # QThreadPool can automatically queue new incoming tasks if a number of them are larger than maxThreadCount - self.workers_pool = QThreadPool() + self.workers_pool = QThreadPool(parent=self) self.workers_pool.setMaxThreadCount(1) self.workers_pool.setExpiryTimeout(-1) # tasks forever wait for the available spot self._is_action_running = False @@ -135,20 +143,19 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren self.qml_ready = threading.Event() # the front and the back both should know when each other is initialized - self._finalizer = weakref.finalize(self, self.at_exit) # register some kind of deconstruction handler + self._finalizer = None # register some kind of the deconstruction handler - if project_args is not None: - if 'instance_options' not in project_kwargs: - project_kwargs['instance_options'] = { - 'logger': self.logger - } - elif 'logger' not in project_kwargs['instance_options']: - project_kwargs['instance_options']['logger'] = self.logger + if 'instance_options' not in project_kwargs: + project_kwargs['instance_options'] = { + 'logger': self.logger + } + elif 'logger' not in project_kwargs['instance_options']: + project_kwargs['instance_options']['logger'] = self.logger - # Start the Stm32pio part initialization right after. It can take some time so we schedule it in a dedicated - # thread - self.init_thread = threading.Thread(target=self.init_project, args=project_args, kwargs=project_kwargs) - self.init_thread.start() + # Start the Stm32pio part initialization right after. It can take some time so we schedule it in a dedicated + # thread + init_thread = threading.Thread(target=self.init_project, args=project_args, kwargs=project_kwargs) + init_thread.start() def init_project(self, *args, **kwargs) -> None: @@ -165,26 +172,31 @@ def init_project(self, *args, **kwargs) -> None: # Error during the initialization self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) if len(args): - self._name = args[0] # use project path string (probably) as a name + self._name = args[0] # use a project path string (as it should be a first argument) as a name else: - self._name = 'No name' + self._name = 'Undefined' self._state = { 'INIT_ERROR': True } self._current_stage = 'Initializing error' else: - self._name = 'Project' # successful initialization. These values should not be used anymore + # Successful initialization. These values should not be used anymore but we "reset" them anyway + self._name = 'Project' self._state = {} self._current_stage = 'Initialized' finally: + # Register some kind of the deconstruction handler + self._finalizer = weakref.finalize(self, self.at_exit, self.workers_pool, self.logging_worker, + self.name if self.project is None else str(self.project)) self.qml_ready.wait() # wait for the GUI to initialized self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending self.stageChanged.emit() self.stateChanged.emit() - def at_exit(self): - module_logger.info(f"destroy {self.project}") - self.workers_pool.waitForDone(msecs=-1) # wait for all jobs to complete. Currently, we cannot abort them gracefully - self.logging_worker.stopped.set() # post the event in the logging worker to inform it... - self.logging_worker.thread.wait() # ...and wait for it to exit + @staticmethod + def at_exit(workers_pool: QThreadPool, logging_worker: LoggingWorker, name: str): + module_logger.info(f"destroy {name}") + workers_pool.waitForDone(msecs=-1) # wait for all jobs to complete. Currently, we cannot abort them gracefully + logging_worker.stopped.set() # post the event in the logging worker to inform it... + logging_worker.thread.wait() # ...and wait for it to exit @Property(str, notify=nameChanged) def name(self): @@ -228,7 +240,7 @@ def actionStartedSlot(self, action: str): @Slot(str, bool) def actionDoneSlot(self, action: str, success: bool): if not success: - self.workers_pool.clear() # clear the queue - prevent further execution + self.workers_pool.clear() # clear the queue - stop further execution self._is_action_running = False self.actionRunningChanged.emit() self.actionDone.emit(action, success) @@ -304,7 +316,7 @@ def run(self): if not success: # Pause the thread and, therefore, the parent QThreadPool queue so the caller can decide whether we should # proceed or stop. This should not cause any problems as we've already perform all necessary tasks and this - # just delaying the QRunnable removal + # just delaying the QRunnable removal from the pool time.sleep(1.0) @@ -358,7 +370,12 @@ def addProjectByPath(self, path: QUrl): Args: path: QUrl path to the project folder (absolute by default) """ + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + + if any([list_item.project.path.samefile(pathlib.Path(path.toLocalFile())) for list_item in self.projects]): + module_logger.warning("This project is already in the list") + project = ProjectListItem(project_args=[path.toLocalFile()], parent=self) self.projects.append(project) @@ -380,7 +397,7 @@ def removeProject(self, index: int): self.beginRemoveRows(QModelIndex(), index, index) project = self.projects.pop(index) - # TODO: destruct both Qt and Python objects (seems like now they are not destroyed till the program termination) + project.deleteLater() settings.beginGroup('app') @@ -393,14 +410,14 @@ def removeProject(self, index: int): # ... drop the index ... settings_projects_list.pop(index) - settings.remove('projects') # ... and overwrite the list. We don't use self.projects[i].project.path as there is a chance that 'path' # doesn't exist (e.g. not initialized for some reason project) + settings.remove('projects') settings.beginWriteArray('projects') - for idx in range(len(settings_projects_list)): + for idx, path in enumerate(settings_projects_list): settings.setArrayIndex(idx) - settings.setValue('path', settings_projects_list[idx]) + settings.setValue('path', path) settings.endArray() settings.endGroup() @@ -425,7 +442,6 @@ def qt_message_handler(mode, context, message): -# TODO: there is a bug - checkbox in the window doesn't correctly represent the current settings class Settings(QSettings): """ Extend the class by useful get/set methods allowing to avoid redundant code lines and also are callable from the @@ -437,47 +453,57 @@ class Settings(QSettings): 'verbose': False } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for key, value in self.DEFAULT_SETTINGS.items(): - if not self.contains('app/settings/' + key): - self.setValue('app/settings/' + key, value) + def __init__(self, prefix: str, defaults: dict, qs_args: list = None, qs_kwargs: dict = None, + external_triggers: dict = None): + + qs_args = qs_args if qs_args is not None else [] + qs_kwargs = qs_kwargs if qs_kwargs is not None else {} + + super().__init__(*qs_args, **qs_kwargs) + + self.prefix = prefix + self.external_triggers = external_triggers if external_triggers is not None else {} + + for key, value in defaults.items(): + if not self.contains(self.prefix + key): + self.setValue(self.prefix + key, value) + @Slot(str, result='QVariant') def get(self, key): - return self.value('app/settings/' + key) + return self.value(self.prefix + key) + @Slot(str, 'QVariant') def set(self, key, value): - self.setValue('app/settings/' + key, value) + self.setValue(self.prefix + key, value) - if key == 'verbose': - module_logger.setLevel(logging.DEBUG if value else logging.INFO) - for project in projects_model.projects: - project.logger.setLevel(logging.DEBUG if value else logging.INFO) + if key in self.external_triggers.keys(): + self.external_triggers['key'](value) -if __name__ == '__main__': - # Use it as a console logger for whatever you want to - module_logger = logging.getLogger(__name__) +def main(): + global module_logger module_log_handler = logging.StreamHandler() module_log_handler.setFormatter(logging.Formatter("%(levelname)s %(funcName)s %(message)s")) module_logger.addHandler(module_log_handler) module_logger.setLevel(logging.INFO) - module_logger.info('Starting stm32pio-gui...') + module_logger.info('Starting stm32pio_gui...') # Apparently Windows version of PySide2 doesn't have QML logging feature turn on so we fill this gap # TODO: set up for other platforms too (separate console.debug, console.warn, etc.) - if stm32pio.settings.my_os == 'Windows': - qml_logger = logging.getLogger('qml') + global qml_logger + if platform.system() == 'Windows': qml_log_handler = logging.StreamHandler() qml_log_handler.setFormatter(logging.Formatter("[QML] %(levelname)s %(message)s")) qml_logger.addHandler(qml_log_handler) qInstallMessageHandler(qt_message_handler) + # Most Linux distros should be linked with the QWidgets' QApplication instead of the QGuiApplication to enable # QtDialogs - if stm32pio.settings.my_os == 'Linux': + global app + if platform.system() == 'Linux': app = QApplication(sys.argv) else: app = QGuiApplication(sys.argv) @@ -485,9 +511,27 @@ def set(self, key, value): # Used as a settings identifier too app.setOrganizationName('ussserrr') app.setApplicationName('stm32pio') - app.setWindowIcon(QIcon('stm32pio-gui/icons/icon.svg')) - - settings = Settings(parent=app) + app.setWindowIcon(QIcon('stm32pio_gui/icons/icon.svg')) + + + global settings + + def verbose_setter(value): + module_logger.setLevel(logging.DEBUG if value else logging.INFO) + for project in projects_model.projects: + project.logger.setLevel(logging.DEBUG if value else logging.INFO) + + settings = Settings(prefix='app/settings/', + defaults={ + 'editor': '', + 'verbose': False + }, + qs_kwargs={ + 'parent': app + }, + external_triggers={ + 'verbose': verbose_setter + }) # settings.remove('app/settings') # settings.remove('app/projects') @@ -501,6 +545,7 @@ def set(self, key, value): settings.endArray() settings.endGroup() + engine = QQmlApplicationEngine(parent=app) qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') @@ -522,7 +567,7 @@ def set(self, key, value): engine.rootContext().setContextProperty('boardsModel', boards_model) engine.rootContext().setContextProperty('appSettings', settings) - engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) + engine.load(QUrl.fromLocalFile('stm32pio_gui/main.qml')) main_window = engine.rootObjects()[0] @@ -532,26 +577,35 @@ def set(self, key, value): # start-up operations here if there will be need to. Use the same ProjectActionWorker to spawn the thread at pool. def loading(): - global boards + nonlocal boards boards = ['None'] + stm32pio.util.get_platformio_boards('platformio') - def on_loading(_, success): + def loaded(_, success): # TODO: somehow handle an initialization error boards_model.setStringList(boards) - projects = [ProjectListItem( - project_args=[path], - project_kwargs=dict( - instance_options={'save_on_destruction': False} - ), - parent=projects_model - ) for path in projects_paths] + projects = [ProjectListItem(project_args=[path], parent=projects_model) for path in projects_paths] for p in projects: projects_model.addProject(p) main_window.backendLoaded.emit() # inform the GUI loader = ProjectActionWorker(loading, logger=module_logger) - loader.actionDone.connect(on_loading) + loader.actionDone.connect(loaded) QThreadPool.globalInstance().start(loader) - sys.exit(app.exec_()) + return app.exec_() + + + +# Globals + +module_logger = logging.getLogger(__name__) # use it as a console logger for whatever you want to +qml_logger = logging.getLogger('qml') + +app = None +settings = QSettings() + + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/stm32pio-gui/icons/LICENSE b/stm32pio_gui/icons/LICENSE similarity index 100% rename from stm32pio-gui/icons/LICENSE rename to stm32pio_gui/icons/LICENSE diff --git a/stm32pio-gui/icons/add.svg b/stm32pio_gui/icons/add.svg similarity index 94% rename from stm32pio-gui/icons/add.svg rename to stm32pio_gui/icons/add.svg index 7a55656..87a4b46 100644 --- a/stm32pio-gui/icons/add.svg +++ b/stm32pio_gui/icons/add.svg @@ -1,41 +1,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stm32pio-gui/icons/icon.svg b/stm32pio_gui/icons/icon.svg similarity index 100% rename from stm32pio-gui/icons/icon.svg rename to stm32pio_gui/icons/icon.svg diff --git a/stm32pio-gui/icons/remove.svg b/stm32pio_gui/icons/remove.svg similarity index 93% rename from stm32pio-gui/icons/remove.svg rename to stm32pio_gui/icons/remove.svg index 0445e5c..858b71c 100644 --- a/stm32pio-gui/icons/remove.svg +++ b/stm32pio_gui/icons/remove.svg @@ -1,40 +1,40 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stm32pio-gui/main.qml b/stm32pio_gui/main.qml similarity index 96% rename from stm32pio-gui/main.qml rename to stm32pio_gui/main.qml index 1854bf8..786717f 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio_gui/main.qml @@ -111,11 +111,12 @@ ApplicationWindow { } /* - Project representation is, in fact, split in two main parts: one in a list and one is an actual workspace. - To avoid some possible bloopers we should make sure that both of them are loaded before performing - any actions with the project. To not reveal QML-side implementation details to the backend we define - this helper function that counts number of widgets currently loaded for each project in model and informs - the Qt-side right after all necessary components went ready. + The project visual representation is, in fact, split in two main parts: one in a list and one is + an actual workspace. To avoid some possible bloopers we should make sure that both of them are loaded + (at least at the subsistence level) before performing any actions with the project. To not reveal these + QML-side implementation details to the backend we define this helper function that counts and stores + a number of widgets currently loaded for each project in model and informs the Qt-side right after all + necessary components become ready. */ property var initInfo: ({}) function setInitInfo(projectIndex) { @@ -146,6 +147,9 @@ ApplicationWindow { projectsListView.currentIndex = indexToMoveTo; projectsWorkspaceView.currentIndex = indexToMoveTo; + // There is some strange bug when the workspace view (highest level StackLayout) disappears after + // the project deletion (even when the removal is performed in a separated Timer after some delay + // and the current index is definitely has already changed for both widgets) projectsModel.removeProject(indexToRemove); } @@ -155,13 +159,14 @@ ApplicationWindow { Action { text: '&Settings'; onTriggered: settingsDialog.open() } Action { text: '&About'; onTriggered: aboutDialog.open() } MenuSeparator { } - Action { text: '&Quit'; onTriggered: Qt.quit() } + // Use this instead of Qt.qiut() to prevent segfaults (messed up shutdown order) + Action { text: '&Quit'; onTriggered: mainWindow.close() } } } /* All layouts and widgets try to be adaptive to variable parents, siblings, window and whatever else sizes - so we extensively using Grid, Column and Row layouts. The most high-level one is a composition of the list + so we extensively use Grid, Column and Row layouts. The most high-level one is a composition of the list and the workspace in two columns */ GridLayout { @@ -236,7 +241,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height Layout.preferredHeight: parent.height - running: parent.initLoading || project.actionRunning + running: parent.initLoading || (project && project.actionRunning) } MouseArea { @@ -284,7 +289,7 @@ ApplicationWindow { /* Main workspace. StackLayout's Repeater component seamlessly uses the same projects model (showing one - - current - project per screen) so all data is synchronized without any additional effort. + current - project per screen) as the list so all data is synchronized without any additional effort. */ StackLayout { id: projectsWorkspaceView @@ -642,8 +647,10 @@ ApplicationWindow { currentColor = palette.button; } palette.button = Qt.lighter('lightgreen', 1.2); + palette.buttonText = 'dimgray'; } else { palette.button = currentColor; + palette.buttonText = 'black'; currentColor = ''; } } From bb3e2f42db606f6574c5af6642b3d3779867af99 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 4 Apr 2020 02:17:26 +0300 Subject: [PATCH 06/20] duplication check and drag'n'drop feature --- TODO.md | 6 +++-- stm32pio_gui/app.py | 26 ++++++++++++++++----- stm32pio_gui/icons/LICENSE | 8 ++++++- stm32pio_gui/main.qml | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/TODO.md b/TODO.md index 60fce5d..c35adee 100644 --- a/TODO.md +++ b/TODO.md @@ -11,12 +11,13 @@ - [x] GUI. On 'Clean' clean the log too - [x] GUI. Stop the chain of commands if someone drops -1 or an exception - [ ] GUI. 2 types of logging formatters for 2 verbosity levels - - [ ] GUI. Check for projects duplication + - [x] GUI. Check for projects duplication - [ ] GUI. Maybe use QML State for action buttons appearance - [x] GUI. Projects are not destructed until quit (something preserving the link probably...) - [x] GUI. Fix settings (window doesn't match real) - [x] GUI. TypeError: Cannot read property 'actionRunning' of null - [ ] GUI. QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on + - [ ] GUI. Fix high CPU usage (probably some thread consuming) - [ ] Create VSCode plugin - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') @@ -30,7 +31,7 @@ - [ ] Two words about a synchronous nature of the lib and user's responsibility of async wrapping (if needed). Also, maybe migrate to async/await approach in the future - [ ] `__init__`' `parameters` dict argument schema (Python 3.8 feature). - [ ] See https://docs.python.org/3/howto/logging-cookbook.html#context-info to maybe remade current logging schema (current is, perhaps, a cause of the strange error while testing (in the logging thread)) - - [ ] UML diagrams (core, GUI back- and front-ends) + - [ ] UML diagrams (core, GUI back- and front-ends, thread flows, events, etc.) - [ ] CI is possible (Arch's AUR has the STM32CubeMX package, also there is a direct link). Deploy Docker one in Azure Pipelines, basic at Travis CI - [ ] Test preserving user files and folders on regeneration and mb other operations - [ ] Move special formatters inside the library. It is an implementation detail actually that we use subprocesses and so on @@ -38,3 +39,4 @@ - [ ] 'verbose' and 'non-verbose' tests as `subTest` (also `should_log_error_...`) - [ ] the lib sometimes raising, sometimes returning the code and it is not consistent. While the reasons behind such behaviour are clear, would be great to always return a result code and raise the exceptions in the outer scope, if there is need to - [ ] check board (no sense to go further on 'new' if the board in config.ini is not correct) + - [ ] check if `platformio.ini` config will be successfully parsed when there are interpolation and/or empty parameters diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index de8e126..31f3780 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -186,7 +186,7 @@ def init_project(self, *args, **kwargs) -> None: # Register some kind of the deconstruction handler self._finalizer = weakref.finalize(self, self.at_exit, self.workers_pool, self.logging_worker, self.name if self.project is None else str(self.project)) - self.qml_ready.wait() # wait for the GUI to initialized + self.qml_ready.wait() # wait for the GUI to initialize self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending self.stageChanged.emit() self.stateChanged.emit() @@ -328,6 +328,8 @@ class ProjectsList(QAbstractListModel): ProjectListItem. """ + duplicateFound = Signal(int, arguments=['duplicateIndex']) + def __init__(self, projects: list = None, parent: QObject = None): """ Args: @@ -361,8 +363,8 @@ def addProject(self, project: ProjectListItem): self.projects.append(project) self.endInsertRows() - @Slot(QUrl) - def addProjectByPath(self, path: QUrl): + @Slot(str, str) + def addProjectByPath(self, path: str, arg_type: str): """ Create, append and save in QSettings a new ProjectListItem instance with a given QUrl path (typically sent from the QML GUI). @@ -371,10 +373,22 @@ def addProjectByPath(self, path: QUrl): path: QUrl path to the project folder (absolute by default) """ - self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + print(type(path), path, arg_type) + return + + if arg_type == '[text/plain]': + path = str(pathlib.Path(path.replace('file://', '')).resolve()) + elif arg_type == '[text/uri-list]': + path = QUrl(path).toLocalFile() - if any([list_item.project.path.samefile(pathlib.Path(path.toLocalFile())) for list_item in self.projects]): - module_logger.warning("This project is already in the list") + duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if + list_item.project.path.samefile(pathlib.Path(path.toLocalFile()))), -1) + if duplicate_index >= 0: + module_logger.warning(f"This project is already in the list: {path.toLocalFile()}") + self.duplicateFound.emit(duplicate_index) + return + + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) project = ProjectListItem(project_args=[path.toLocalFile()], parent=self) self.projects.append(project) diff --git a/stm32pio_gui/icons/LICENSE b/stm32pio_gui/icons/LICENSE index 9aa1c60..17ec736 100644 --- a/stm32pio_gui/icons/LICENSE +++ b/stm32pio_gui/icons/LICENSE @@ -1 +1,7 @@ -Icons by Flat Icons, Google from www.flaticon.com +Icons by + + - Flat Icons + - Google + - Pixel Perfect + +from www.flaticon.com diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 786717f..b4115c7 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -164,6 +164,45 @@ ApplicationWindow { } } + DropArea { + id: dropArea + anchors.fill: parent + Popup { + visible: dropArea.containsDrag + parent: Overlay.overlay + anchors.centerIn: Overlay.overlay + modal: true + background: Rectangle { opacity: 0.0 } + // closePolicy: Popup.NoAutoClose + + contentItem: Column { + spacing: 20 + Image { + anchors.horizontalCenter: parent.horizontalCenter + source: 'icons/drop-here.svg' + fillMode: Image.PreserveAspectFit + sourceSize.width: 64 + } + Text { + // anchors.topMargin: 20 + text: "Drop project folder to add..." + } + } + } + // onEntered: console.log("entered", drag.urls, drag.text, drag.formats); + // onExited: console.log("exited") + onDropped: { + console.log(drop.urls, drop.text, drop.formats); + if (drop.urls.length) { + projectsModel.addProjectByPath(drop.urls[0], drop.formats); + } else if (drop.text) { + projectsModel.addProjectByPath(drop.text, drop.formats); // TODO: check path! + } else { + console.log("Incorrect drag'n'drop event"); + } + } + } + /* All layouts and widgets try to be adaptive to variable parents, siblings, window and whatever else sizes so we extensively use Grid, Column and Row layouts. The most high-level one is a composition of the list @@ -269,6 +308,13 @@ ApplicationWindow { Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter Layout.fillWidth: true + Connections { + target: projectsModel + onDuplicateFound: { + projectsListView.currentIndex = duplicateIndex; + projectsWorkspaceView.currentIndex = duplicateIndex; + } + } Button { text: 'Add' Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter From 5d892bb02c22a750e8f870983f6001de31185b6d Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 4 Apr 2020 22:03:44 +0300 Subject: [PATCH 07/20] drag'n'drop feature, add icon --- stm32pio_gui/app.py | 21 ++++++++------ stm32pio_gui/icons/drop-here.svg | 47 ++++++++++++++++++++++++++++++++ stm32pio_gui/main.qml | 10 +++---- 3 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 stm32pio_gui/icons/drop-here.svg diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index 31f3780..a7781e8 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -23,7 +23,7 @@ else: from PySide2.QtGui import QGuiApplication from PySide2.QtGui import QIcon - from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine + from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine, QJSValue except IndexError as e: print(e) print("\nGUI version requires PySide2 to be installed. You can re-install stm32pio as 'pip install stm32pio[GUI]' " @@ -363,8 +363,8 @@ def addProject(self, project: ProjectListItem): self.projects.append(project) self.endInsertRows() - @Slot(str, str) - def addProjectByPath(self, path: str, arg_type: str): + @Slot('QStringList') + def addProjectByPath(self, path): """ Create, append and save in QSettings a new ProjectListItem instance with a given QUrl path (typically sent from the QML GUI). @@ -373,14 +373,17 @@ def addProjectByPath(self, path: str, arg_type: str): path: QUrl path to the project folder (absolute by default) """ - print(type(path), path, arg_type) + print(type(path), path) + print(QUrl.fromStringList(path)) + paths_list = [] + for p in QUrl.fromStringList(path): + if p.isLocalFile(): + paths_list.append(p.toLocalFile()) + elif p.isRelative(): + paths_list.append(p.toString(QUrl.FormattingOptions(QUrl.None_))) + print(paths_list) return - if arg_type == '[text/plain]': - path = str(pathlib.Path(path.replace('file://', '')).resolve()) - elif arg_type == '[text/uri-list]': - path = QUrl(path).toLocalFile() - duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if list_item.project.path.samefile(pathlib.Path(path.toLocalFile()))), -1) if duplicate_index >= 0: diff --git a/stm32pio_gui/icons/drop-here.svg b/stm32pio_gui/icons/drop-here.svg new file mode 100644 index 0000000..8fa1a94 --- /dev/null +++ b/stm32pio_gui/icons/drop-here.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index b4115c7..e976f0d 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -189,14 +189,14 @@ ApplicationWindow { } } } - // onEntered: console.log("entered", drag.urls, drag.text, drag.formats); - // onExited: console.log("exited") onDropped: { - console.log(drop.urls, drop.text, drop.formats); + console.log(drop.urls, typeof(drop.urls), drop.text, drop.formats); if (drop.urls.length) { - projectsModel.addProjectByPath(drop.urls[0], drop.formats); + // typeof(drop.urls) === 'object' so we need to convert to the array of strings + projectsModel.addProjectByPath(Object.keys(drop.urls).map(u => drop.urls[u])); } else if (drop.text) { - projectsModel.addProjectByPath(drop.text, drop.formats); // TODO: check path! + // wrap into the array for consistency + projectsModel.addProjectByPath([drop.text]); } else { console.log("Incorrect drag'n'drop event"); } From bc6b3fa83301aa68df478a1113ce626c79ac0996 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 5 Apr 2020 21:24:14 +0300 Subject: [PATCH 08/20] projects highlighting, can specify .ioc file, fix exceptions messages --- TODO.md | 19 ++++++++++++- stm32pio/app.py | 2 +- stm32pio/lib.py | 66 ++++++++++++++++++++++++++++++------------- stm32pio_gui/app.py | 60 ++++++++++++++++++++++++--------------- stm32pio_gui/main.qml | 39 +++++++++++++++++++++---- tests/test_unit.py | 15 ++++++++++ 6 files changed, 152 insertions(+), 49 deletions(-) diff --git a/TODO.md b/TODO.md index c35adee..f6f21e5 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [ ] GUI. Tests (research approaches and patterns) - [x] GUI. Reduce number of calls to 'state' (many IO operations) - - [ ] GUI. Drag and drop the new folder into the app window + - [x] GUI. Drag and drop the new folder into the app window - [ ] GUI. Implement other methods for Qt abstract models - [x] GUI. Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) - [x] GUI. On 'Clean' clean the log too @@ -18,6 +18,22 @@ - [x] GUI. TypeError: Cannot read property 'actionRunning' of null - [ ] GUI. QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - [ ] GUI. Fix high CPU usage (probably some thread consuming) + - [ ] GUI. Relative resource paths: + + ``` + ⌘ python3 Documents/GitHub/stm32pio/stm32pio_gui/app.py + INFO main Starting stm32pio_gui... + qt.svg: Cannot open file '/Users/chufyrev/stm32pio_gui/icons/icon.svg', because: No such file or directory + qt.svg: Cannot open file '/Users/chufyrev/stm32pio_gui/icons/icon.svg', because: No such file or directory + QQmlApplicationEngine failed to load component + file:///Users/chufyrev/stm32pio_gui/main.qml: No such file or directory + Traceback (most recent call last): + File "Documents/GitHub/stm32pio/stm32pio_gui/app.py", line 629, in + sys.exit(main()) + File "Documents/GitHub/stm32pio/stm32pio_gui/app.py", line 590, in main + main_window = engine.rootObjects()[0] + IndexError: list index out of range + ``` - [ ] Create VSCode plugin - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') @@ -40,3 +56,4 @@ - [ ] the lib sometimes raising, sometimes returning the code and it is not consistent. While the reasons behind such behaviour are clear, would be great to always return a result code and raise the exceptions in the outer scope, if there is need to - [ ] check board (no sense to go further on 'new' if the board in config.ini is not correct) - [ ] check if `platformio.ini` config will be successfully parsed when there are interpolation and/or empty parameters + - [x] check if `.ioc` file is a text file on project initialization. Let `_find_ioc_file()` method to use explicitly provided file (useful for GUI). Maybe let user specify it via CLI diff --git a/stm32pio/app.py b/stm32pio/app.py index ef8fd64..b931d97 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -159,7 +159,7 @@ def main(sys_argv=None) -> int: # Library is designed to throw the exception in bad cases so we catch here globally except Exception: - # ExceptionName: message + # Print format is: "ExceptionName: message" logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1], exc_info=logger.isEnabledFor(logging.DEBUG)) return -1 diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 4a0ac92..c0dc023 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -78,7 +78,7 @@ class ProjectState(collections.OrderedDict): The class has no special constructor so its filling - both stages and their order - is a responsibility of the external code. It also has no protection nor checks for its internal correctness. Anyway, it is intended to be used - (i.e. creating) only by the internal code of this library so there should not be any worries. + (i.e. creating) only by the internal code of this library so there shouldn't be any worries. """ def __str__(self): @@ -167,13 +167,20 @@ def __init__(self, dirty_path: str, parameters: dict = None, instance_options: d else: self.logger = logging.getLogger(f"{__name__}.{id(self)}") # use id() as uniqueness guarantee - # The path is a unique identifier of the project. Handle 'path/to/proj', 'path/to/proj/', '.', '../proj', etc., - # make the path absolute and check for existence - self.path = pathlib.Path(dirty_path).expanduser().resolve(strict=True) + # The path is a primary identifier of the project so we process it first and foremost. Handle 'path/to/proj', + # 'path/to/proj/', '.', '../proj', etc., make the path absolute and check for existence. Also, the .ioc file can + # be specified instead of the directory. In this case it is assumed that the parent path is an actual project + # path and the provided .ioc file is used on a priority basis + path = pathlib.Path(dirty_path).expanduser().resolve(strict=True) + ioc_file = None + if path.is_file() and path.suffix == '.ioc': # if .ioc file was supplied instead of the directory + ioc_file = path + path = path.parent + self.path = path self.config = self._load_config(parameters) - self.ioc_file = self._find_ioc_file() + self.ioc_file = self._find_ioc_file(explicit_file=ioc_file) self.config.set('project', 'ioc_file', self.ioc_file.name) if 'board' in parameters and parameters['board'] is not None: @@ -239,7 +246,7 @@ def state(self) -> ProjectState: return conditions_results - def _find_ioc_file(self) -> pathlib.Path: + def _find_ioc_file(self, explicit_file: pathlib.Path = None) -> pathlib.Path: """ Find and return an .ioc file. If there are more than one return first. If no .ioc file is present raise FileNotFoundError exception @@ -248,22 +255,41 @@ def _find_ioc_file(self) -> pathlib.Path: absolute path to the .ioc file """ - ioc_file = self.config.get('project', 'ioc_file', fallback=None) - if ioc_file: - ioc_file = self.path.joinpath(ioc_file).resolve(strict=True) - self.logger.debug(f"using '{ioc_file.name}' file from the INI config") - return ioc_file + result_file = None + + # 1. If explicit file was provided use it + if explicit_file is not None: + self.logger.debug(f"using explicitly provided '{explicit_file.name}' file") + result_file = explicit_file + else: - self.logger.debug("searching for any .ioc file...") - candidates = list(self.path.glob('*.ioc')) - if len(candidates) == 0: # TODO: good candidate for the new Python 3.8 assignment expression feature :) - raise FileNotFoundError("CubeMX project .ioc file") - elif len(candidates) == 1: - self.logger.debug(f"{candidates[0].name} is selected") - return candidates[0] + # 2. Check the value from the config file + ioc_file = self.config.get('project', 'ioc_file', fallback=None) # TODO: Python 3.8 walrus operator (elif ...) + if ioc_file: + ioc_file = self.path.joinpath(ioc_file).resolve(strict=True) + self.logger.debug(f"using '{ioc_file.name}' file from the INI config") + result_file = ioc_file + + # 3. Otherwise search for an appropriate file by yourself else: - self.logger.warning(f"there are multiple .ioc files, {candidates[0].name} is selected") - return candidates[0] + self.logger.debug("searching for any .ioc file...") + candidates = list(self.path.glob('*.ioc')) + if len(candidates) == 0: # TODO: good candidate for the new Python 3.8 assignment expression feature :) + raise FileNotFoundError("CubeMX project .ioc file") + elif len(candidates) == 1: + self.logger.debug(f"{candidates[0].name} is selected") + result_file = candidates[0] + else: + self.logger.warning(f"there are multiple .ioc files, {candidates[0].name} is selected") + result_file = candidates[0] + + # Check file correctness + try: + content = result_file.read_text() + assert len(content) > 0 + return result_file + except Exception as e: + raise Exception(f"{result_file.name} is incorrect") from e def _load_config(self, runtime_parameters: dict = None) -> configparser.ConfigParser: diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index a7781e8..fa822c8 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -11,6 +11,7 @@ import sys import threading import time +import traceback import weakref try: @@ -116,7 +117,7 @@ class ProjectListItem(QObject): actionRunningChanged = Signal() - def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None): + def __init__(self, project_args: list = None, project_kwargs: dict = None, from_startup: bool = False, parent: QObject = None): super().__init__(parent=parent) if project_args is None: @@ -124,6 +125,8 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren if project_kwargs is None: project_kwargs = {} + self._from_startup = from_startup + self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") self.logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO) self.logging_worker = LoggingWorker(self.logger) @@ -169,8 +172,9 @@ def init_project(self, *args, **kwargs) -> None: try: self.project = stm32pio.lib.Stm32pio(*args, **kwargs) except Exception as e: - # Error during the initialization - self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) + # Error during the initialization. Print format is: "ExceptionName: message" + self.logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1], + exc_info=self.logger.isEnabledFor(logging.DEBUG)) if len(args): self._name = args[0] # use a project path string (as it should be a first argument) as a name else: @@ -191,6 +195,10 @@ def init_project(self, *args, **kwargs) -> None: self.stageChanged.emit() self.stateChanged.emit() + @Property(bool) + def fromStartup(self): + return self._from_startup + @staticmethod def at_exit(workers_pool: QThreadPool, logging_worker: LoggingWorker, name: str): module_logger.info(f"destroy {name}") @@ -303,7 +311,9 @@ def run(self): result = self.func(*self.args) except Exception as e: if self.logger is not None: - self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) + # Print format is: "ExceptionName: message" + self.logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1], + exc_info=self.logger.isEnabledFor(logging.DEBUG)) result = -1 if result is None or (type(result) == int and result == 0): @@ -364,42 +374,45 @@ def addProject(self, project: ProjectListItem): self.endInsertRows() @Slot('QStringList') - def addProjectByPath(self, path): + def addProjectByPath(self, str_list: list): """ Create, append and save in QSettings a new ProjectListItem instance with a given QUrl path (typically sent from the QML GUI). - - Args: - path: QUrl path to the project folder (absolute by default) """ - print(type(path), path) - print(QUrl.fromStringList(path)) + print(type(str_list), str_list) + print(QUrl.fromStringList(str_list)) paths_list = [] - for p in QUrl.fromStringList(path): - if p.isLocalFile(): - paths_list.append(p.toLocalFile()) - elif p.isRelative(): - paths_list.append(p.toString(QUrl.FormattingOptions(QUrl.None_))) + for path_str in str_list: + path_qurl = QUrl(path_str) + if path_qurl.isLocalFile(): + paths_list.append(path_qurl.toLocalFile()) + elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix + paths_list.append(path_str) # just use source string print(paths_list) - return + + if len(paths_list): + path = paths_list[0] # for now just respond on one item even if a list was provided + else: + module_logger.warning("No path were given") + return duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if - list_item.project.path.samefile(pathlib.Path(path.toLocalFile()))), -1) + list_item.project.path.samefile(pathlib.Path(path))), -1) if duplicate_index >= 0: - module_logger.warning(f"This project is already in the list: {path.toLocalFile()}") + module_logger.warning(f"This project is already in the list: {path}") self.duplicateFound.emit(duplicate_index) return self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) - project = ProjectListItem(project_args=[path.toLocalFile()], parent=self) + project = ProjectListItem(project_args=[path], parent=self) self.projects.append(project) settings.beginGroup('app') settings.beginWriteArray('projects') settings.setArrayIndex(len(self.projects) - 1) - settings.setValue('path', path.toLocalFile()) + settings.setValue('path', path) settings.endArray() settings.endGroup() @@ -496,7 +509,7 @@ def set(self, key, value): self.setValue(self.prefix + key, value) if key in self.external_triggers.keys(): - self.external_triggers['key'](value) + self.external_triggers[key](value) def main(): @@ -600,7 +613,7 @@ def loading(): def loaded(_, success): # TODO: somehow handle an initialization error boards_model.setStringList(boards) - projects = [ProjectListItem(project_args=[path], parent=projects_model) for path in projects_paths] + projects = [ProjectListItem(project_args=[path], from_startup=True, parent=projects_model) for path in projects_paths] for p in projects: projects_model.addProject(p) main_window.backendLoaded.emit() # inform the GUI @@ -625,4 +638,7 @@ def loaded(_, success): if __name__ == '__main__': + # import os + # os.chdir(str(pathlib.Path(sys.path[0]))) + # print(pathlib.Path.cwd()) sys.exit(main()) diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index e976f0d..6fa37c4 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -172,12 +172,16 @@ ApplicationWindow { parent: Overlay.overlay anchors.centerIn: Overlay.overlay modal: true - background: Rectangle { opacity: 0.0 } - // closePolicy: Popup.NoAutoClose - + background: Rectangle { + opacity: 0.0 + } + Overlay.modal: Rectangle { + color: "#aaffffff" + } contentItem: Column { spacing: 20 Image { + id: dropPopupContent anchors.horizontalCenter: parent.horizontalCenter source: 'icons/drop-here.svg' fillMode: Image.PreserveAspectFit @@ -186,6 +190,8 @@ ApplicationWindow { Text { // anchors.topMargin: 20 text: "Drop project folder to add..." + font.pointSize: 24 // different on different platforms, Qt's bug + font.weight: Font.Black // heaviest } } } @@ -242,16 +248,38 @@ ApplicationWindow { property bool initLoading: true // initial waiting for the backend-side property ProjectListItem project: projectsModel.getProject(index) Connections { - target: project // (newbie hint) sender + target: project // (newbie hint) sender which signals we want to catch below // Currently, this event is equivalent to the complete initialization of the backend side of the project onNameChanged: { initLoading = false; + + // Appropriately highlight an item depending on its initialization result + const state = project.state; + if (state['INIT_ERROR']) { + projectName.color = 'indianred'; + projectCurrentStage.color = 'indianred'; + } else if (!project.fromStartup) { + // Do not touch those projects that have been loaded on startup (from the QSettings), + // only new ones added during this session + projectName.color = 'seagreen'; + projectCurrentStage.color = 'seagreen'; + } + } + } + Connections { + target: projectsListView + onCurrentIndexChanged: { + if (projectsListView.currentIndex === index && Qt.colorEqual(projectName.color, 'seagreen')) { + projectName.color = 'black'; + projectCurrentStage.color = 'black'; + } } } ColumnLayout { Layout.preferredHeight: 50 Text { + id: projectName leftPadding: 5 rightPadding: busy.running ? 0 : leftPadding Layout.alignment: Qt.AlignBottom @@ -263,6 +291,7 @@ ApplicationWindow { text: `${display.name}` } Text { + id: projectCurrentStage leftPadding: 5 rightPadding: busy.running ? 0 : leftPadding Layout.alignment: Qt.AlignTop @@ -302,7 +331,7 @@ ApplicationWindow { QtLabs.FolderDialog { id: addProjectFolderDialog currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] - onAccepted: projectsModel.addProjectByPath(folder) + onAccepted: projectsModel.addProjectByPath([folder]) } RowLayout { Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter diff --git a/tests/test_unit.py b/tests/test_unit.py index 92308d1..03c93bf 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -206,3 +206,18 @@ def test_get_platformio_boards(self): PlatformIO identifiers of boards are requested using PlatformIO CLI in JSON format """ self.assertIsInstance(stm32pio.util.get_platformio_boards(platformio_cmd='platformio'), list) + + def test_ioc_file_provided(self): + """ + Test a correct handling of a case when the .ioc file was specified instead of the containing directory + """ + + # Create multiple .ioc files + shutil.copy(FIXTURE_PATH.joinpath('stm32pio-test-project.ioc'), FIXTURE_PATH.joinpath('42.ioc')) + shutil.copy(FIXTURE_PATH.joinpath('stm32pio-test-project.ioc'), FIXTURE_PATH.joinpath('Abracadabra.ioc')) + + project = stm32pio.lib.Stm32pio(FIXTURE_PATH.joinpath('42.ioc')) + self.assertTrue(project.ioc_file.samefile(FIXTURE_PATH.joinpath('42.ioc')), + msg="Provided .ioc file wasn't chosen") + self.assertEqual(project.config.get('project', 'ioc_file'), '42.ioc', + msg="Provided .ioc file is not in the config") From 3c366ab05ca8487a8cd85b391fcd682a3fc274ae Mon Sep 17 00:00:00 2001 From: usserr Date: Mon, 6 Apr 2020 01:34:22 +0300 Subject: [PATCH 09/20] add settings reset, fix for Windows --- stm32pio_gui/app.py | 19 ++++++++++++++++--- stm32pio_gui/main.qml | 14 +++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index fa822c8..e43def9 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -4,7 +4,6 @@ # from __future__ import annotations import collections -import gc import logging import pathlib import platform @@ -215,7 +214,6 @@ def name(self): @Property('QVariant', notify=stateChanged) def state(self): - # print(time.time(), self.project.path.name) if self.project is not None: state = self.project.state @@ -498,10 +496,19 @@ def __init__(self, prefix: str, defaults: dict, qs_args: list = None, qs_kwargs: if not self.contains(self.prefix + key): self.setValue(self.prefix + key, value) + @Slot() + def clear(self): + super().clear() + @Slot(str, result='QVariant') def get(self, key): - return self.value(self.prefix + key) + value = self.value(self.prefix + key) + if value == 'false': + value = False + elif value == 'true': + value = True + return value @Slot(str, 'QVariant') @@ -562,10 +569,16 @@ def verbose_setter(value): external_triggers={ 'verbose': verbose_setter }) + # settings.clear() # clear all # settings.remove('app/settings') # settings.remove('app/projects') module_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO) + qml_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO) + if module_logger.isEnabledFor(logging.DEBUG): + module_logger.debug("App QSettings:") + for key in settings.allKeys(): + module_logger.debug(f"{key}: {settings.value(key)} (type: {type(settings.value(key))})") settings.beginGroup('app') projects_paths = [] diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 6fa37c4..2da9a32 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -46,7 +46,7 @@ ApplicationWindow { QtDialogs.Dialog { id: settingsDialog title: 'Settings' - standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel + standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel | QtDialogs.StandardButton.Reset GridLayout { columns: 2 @@ -66,6 +66,15 @@ ApplicationWindow { id: verbose leftPadding: -3 } + + Text { + Layout.columnSpan: 2 + Layout.maximumWidth: parent.width + topPadding: 30 + bottomPadding: 30 + wrapMode: Text.Wrap + text: "To clear ALL app settings including the list of added projects click \"Reset\" then restart the app" + } } // Set UI values there so they are always reflect actual parameters onVisibleChanged: { @@ -78,6 +87,9 @@ ApplicationWindow { settings.set('editor', editor.text); settings.set('verbose', verbose.checked); } + onReset: { + settings.clear(); + } } QtDialogs.Dialog { From 25c98270b17ad5be79eed4df8e570992f27402a8 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 12 Apr 2020 21:17:34 +0300 Subject: [PATCH 10/20] use QML StateMachine to control the visuals of action buttons --- TODO.md | 3 +- stm32pio/app.py | 4 +- stm32pio/lib.py | 2 - stm32pio_gui/app.py | 13 ++-- stm32pio_gui/main.qml | 163 +++++++++++++++++++++++------------------- 5 files changed, 101 insertions(+), 84 deletions(-) diff --git a/TODO.md b/TODO.md index f6f21e5..516ad88 100644 --- a/TODO.md +++ b/TODO.md @@ -12,12 +12,13 @@ - [x] GUI. Stop the chain of commands if someone drops -1 or an exception - [ ] GUI. 2 types of logging formatters for 2 verbosity levels - [x] GUI. Check for projects duplication - - [ ] GUI. Maybe use QML State for action buttons appearance + - [x] GUI. Maybe use QML State for action buttons appearance - [x] GUI. Projects are not destructed until quit (something preserving the link probably...) - [x] GUI. Fix settings (window doesn't match real) - [x] GUI. TypeError: Cannot read property 'actionRunning' of null - [ ] GUI. QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - [ ] GUI. Fix high CPU usage (probably some thread consuming) + - [ ] GUI. Bug in log box scrolling behavior - [ ] GUI. Relative resource paths: ``` diff --git a/stm32pio/app.py b/stm32pio/app.py index b931d97..584ea19 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -63,7 +63,7 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: return parser.parse_args(args) -def main(sys_argv=None) -> int: +def main(sys_argv: Optional[list] = None) -> int: """ Can be used as a high-level wrapper to do complete tasks @@ -168,5 +168,5 @@ def main(sys_argv=None) -> int: if __name__ == '__main__': - sys.path.append(str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 app.py' + sys.path.append(str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python app.py' sys.exit(main()) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index c0dc023..c9bc615 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -2,8 +2,6 @@ Core library """ -from __future__ import annotations - import collections import configparser import contextlib diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index e43def9..ce372ff 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -240,6 +240,7 @@ def actionRunning(self): @Slot(str) def actionStartedSlot(self, action: str): self.actionStarted.emit(action) + # print('actionRunning TRUE') self._is_action_running = True self.actionRunningChanged.emit() @@ -247,6 +248,7 @@ def actionStartedSlot(self, action: str): def actionDoneSlot(self, action: str, success: bool): if not success: self.workers_pool.clear() # clear the queue - stop further execution + # print('actionRunning FALSE') self._is_action_running = False self.actionRunningChanged.emit() self.actionDone.emit(action, success) @@ -378,8 +380,6 @@ def addProjectByPath(self, str_list: list): the QML GUI). """ - print(type(str_list), str_list) - print(QUrl.fromStringList(str_list)) paths_list = [] for path_str in str_list: path_qurl = QUrl(path_str) @@ -387,7 +387,6 @@ def addProjectByPath(self, str_list: list): paths_list.append(path_qurl.toLocalFile()) elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix paths_list.append(path_str) # just use source string - print(paths_list) if len(paths_list): path = paths_list[0] # for now just respond on one item even if a list was provided @@ -575,10 +574,10 @@ def verbose_setter(value): module_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO) qml_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO) - if module_logger.isEnabledFor(logging.DEBUG): - module_logger.debug("App QSettings:") - for key in settings.allKeys(): - module_logger.debug(f"{key}: {settings.value(key)} (type: {type(settings.value(key))})") + # if module_logger.isEnabledFor(logging.DEBUG): + # module_logger.debug("App QSettings:") + # for key in settings.allKeys(): + # module_logger.debug(f"{key}: {settings.value(key)} (type: {type(settings.value(key))})") settings.beginGroup('app') projects_paths = [] diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 2da9a32..2c14223 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtGraphicalEffects 1.12 import QtQuick.Dialogs 1.3 as QtDialogs +import QtQml.StateMachine 1.12 as DSM import Qt.labs.platform 1.1 as QtLabs @@ -89,6 +90,7 @@ ApplicationWindow { } onReset: { settings.clear(); + this.close(); } } @@ -208,12 +210,11 @@ ApplicationWindow { } } onDropped: { - console.log(drop.urls, typeof(drop.urls), drop.text, drop.formats); if (drop.urls.length) { - // typeof(drop.urls) === 'object' so we need to convert to the array of strings + // We need to convert to the array of strings as typeof(drop.urls) === 'object' projectsModel.addProjectByPath(Object.keys(drop.urls).map(u => drop.urls[u])); } else if (drop.text) { - // wrap into the array for consistency + // Wrap into the array for consistency projectsModel.addProjectByPath([drop.text]); } else { console.log("Incorrect drag'n'drop event"); @@ -453,7 +454,7 @@ ApplicationWindow { const state = project.state; stateCached = state; - project.stageChanged(); // side-effect: get the stage at the same time + project.stageChanged(); // side-effect: update the stage at the same time if (!state['INIT_ERROR'] && !state['EMPTY']) { // i.e. no .ioc file but the project was able to initialize itself // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) @@ -622,29 +623,6 @@ ApplicationWindow { Layout.fillWidth: true Layout.bottomMargin: 7 z: 1 // for the glowing animation - Connections { - target: project - onNameChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - // Looks like 'enabled' property should be managed from outside of the element - // (i.e there, not in the button itself) - projActionsRow.children[i].enabled = true; - } - } - onActionStarted: { - for (let i = 0; i < buttonsModel.count; ++i) { - projActionsRow.children[i].enabled = false; - projActionsRow.children[i].palette.buttonText = 'darkgray'; - projActionsRow.children[i].glowVisible = false; - } - } - onActionDone: { - for (let i = 0; i < buttonsModel.count; ++i) { - projActionsRow.children[i].enabled = true; - projActionsRow.children[i].palette.buttonText = 'black'; - } - } - } Repeater { model: ListModel { id: buttonsModel @@ -657,52 +635,53 @@ ApplicationWindow { ListElement { name: 'Open editor' action: 'start_editor' - margin: 15 // margin to visually separate first 2 actions as they doesn't represent any state + margin: 15 // margin to visually separate first 2 actions as they doesn't represent any stage } ListElement { name: 'Initialize' - stateRepresented: 'INITIALIZED' // the project state that this button is representing + stageRepresented: 'INITIALIZED' // the project stage this button is representing action: 'save_config' } ListElement { name: 'Generate' - stateRepresented: 'GENERATED' + stageRepresented: 'GENERATED' action: 'generate_code' } ListElement { name: 'Init PlatformIO' - stateRepresented: 'PIO_INITIALIZED' + stageRepresented: 'PIO_INITIALIZED' action: 'pio_init' } ListElement { name: 'Patch' - stateRepresented: 'PATCHED' + stageRepresented: 'PATCHED' action: 'patch' } ListElement { name: 'Build' - stateRepresented: 'BUILT' + stageRepresented: 'BUILT' action: 'build' } } delegate: Button { + id: actionButton text: model.name Layout.rightMargin: model.margin - enabled: false // turn on after project initialization - property alias glowVisible: glow.visible - property var stateCachedNotifier: stateCached - onStateCachedNotifierChanged: { - if (stateCached[model.stateRepresented]) { - palette.button = 'lightgreen'; - } else { - palette.button = 'lightgray'; - } - palette.buttonText = 'black'; - } + property bool highlight: false property int buttonIndex: -1 Component.onCompleted: { buttonIndex = index; } + ToolTip { + visible: parent.hovered + Component.onCompleted: { + if (model.tooltip) { + text = model.tooltip; + } else { + this.destroy(); + } + } + } onClicked: { const args = []; // JS array cannot be attached to a ListElement (at least in a non-hacky manner) switch (model.action) { @@ -717,28 +696,74 @@ ApplicationWindow { } project.run(model.action, args); } - ToolTip { - visible: parent.hovered - Component.onCompleted: { - if (model.tooltip) { - text = model.tooltip; - } else { - this.destroy(); + DSM.StateMachine { + initialState: main + running: true + DSM.State { + id: main + initialState: normal + DSM.SignalTransition { + targetState: disabled + signal: project.actionStarted + } + DSM.SignalTransition { + targetState: highlighted + signal: actionButton.highlightChanged + guard: actionButton.highlight + } + onEntered: { + actionButton.enabled = true; + actionButton.palette.buttonText = 'black'; + } + DSM.State { + id: normal + DSM.SignalTransition { + targetState: stageFulfilled + signal: stateCachedChanged + guard: stateCached[model.stageRepresented] ? true : false // explicitly convert to boolean + } + onEntered: { + actionButton.palette.button = 'lightgray'; + } + } + DSM.State { + id: stageFulfilled + DSM.SignalTransition { + targetState: normal + signal: stateCachedChanged + guard: stateCached[model.stageRepresented] ? false : true // explicitly convert to boolean + } + onEntered: { + actionButton.palette.button = 'lightgreen'; + } + } + DSM.HistoryState { + id: mainHistory + defaultState: normal } } - } - property string currentColor: '' // for highlighting only - function highlight(flag) { - if (flag) { - if (!currentColor) { - currentColor = palette.button; + DSM.State { + id: disabled + DSM.SignalTransition { + targetState: mainHistory + signal: project.actionDone + } + onEntered: { + actionButton.enabled = false; + actionButton.palette.buttonText = 'darkgray'; + } + } + DSM.State { + id: highlighted + DSM.SignalTransition { + targetState: mainHistory + signal: actionButton.highlightChanged + guard: !actionButton.highlight + } + onEntered: { + actionButton.palette.button = Qt.lighter('lightgreen', 1.2); + palette.buttonText = 'dimgray'; } - palette.button = Qt.lighter('lightgreen', 1.2); - palette.buttonText = 'dimgray'; - } else { - palette.button = currentColor; - palette.buttonText = 'black'; - currentColor = ''; } } /* @@ -755,13 +780,13 @@ ApplicationWindow { property bool shiftPressedLastState: false function shiftHandler() { for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { - projActionsRow.children[i].highlight(shiftPressed); + projActionsRow.children[i].highlight = shiftPressed; } } onClicked: { if (shiftPressed && buttonIndex >= buttonsModel.statefulActionsStartIndex) { for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { - projActionsRow.children[i].highlight(false); + projActionsRow.children[i].highlight = false; } for (let i = buttonsModel.statefulActionsStartIndex; i < buttonIndex; ++i) { project.run(buttonsModel.get(i).action, []); @@ -808,12 +833,6 @@ ApplicationWindow { shiftHandler(); } } - onPressed: { - palette.button = Qt.darker(palette.button, 1.2); - } - onReleased: { - palette.button = Qt.lighter(palette.button, 1.2);; - } } Connections { target: project @@ -821,13 +840,13 @@ ApplicationWindow { if (action === model.action) { palette.button = 'gold'; } + glow.visible = false; } onActionDone: { if (action === model.action) { if (success) { glow.color = 'lightgreen'; } else { - palette.button = 'lightcoral'; glow.color = 'lightcoral'; } glow.visible = true; From 45cd04e083eb0c78e2233518289f0fcc36ade5a9 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 14 Apr 2020 15:07:45 +0300 Subject: [PATCH 11/20] many QML fixes and improvements --- TODO.md | 14 +++++-- stm32pio/lib.py | 4 +- stm32pio_gui/main.qml | 92 ++++++++++++++++++++----------------------- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/TODO.md b/TODO.md index 516ad88..9c2b503 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,8 @@ - [ ] GUI. Tests (research approaches and patterns) - [x] GUI. Reduce number of calls to 'state' (many IO operations) - [x] GUI. Drag and drop the new folder into the app window + - [ ] GUI. Multiple projects addition + - [ ] GUI. Divide on multiple modules (both Python and QML) - [ ] GUI. Implement other methods for Qt abstract models - [x] GUI. Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) - [x] GUI. On 'Clean' clean the log too @@ -15,10 +17,16 @@ - [x] GUI. Maybe use QML State for action buttons appearance - [x] GUI. Projects are not destructed until quit (something preserving the link probably...) - [x] GUI. Fix settings (window doesn't match real) - - [x] GUI. TypeError: Cannot read property 'actionRunning' of null + - [ ] GUI. TypeError: Cannot read property 'actionRunning' of null (deconstruction order) - [ ] GUI. QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - - [ ] GUI. Fix high CPU usage (probably some thread consuming) - - [ ] GUI. Bug in log box scrolling behavior + - [ ] GUI. Fix high CPU usage (most likely some thread consuming) + - [ ] GUI. Bug in log box scrolling behavior (autoscroll sometimes turns off, should re-enable when starting any action) + - [x] GUI. Fix loader when action running + - [ ] GUI. Mark list item when action is done and it is not a current item (i.e. notify a user) + - [ ] GUI. Highlight actions that were picked for continuous run (with some border, for example) + - [ ] GUI. Mark last error'ed action + - [x] GUI. Action buttons widget state machine diagram + - [x] GUI. Fix messed up performance when the list index changes! - [ ] GUI. Relative resource paths: ``` diff --git a/stm32pio/lib.py b/stm32pio/lib.py index c9bc615..690bd90 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -281,9 +281,9 @@ def _find_ioc_file(self, explicit_file: pathlib.Path = None) -> pathlib.Path: self.logger.warning(f"there are multiple .ioc files, {candidates[0].name} is selected") result_file = candidates[0] - # Check file correctness + # Check for the file correctness try: - content = result_file.read_text() + content = result_file.read_text() # should be a text file assert len(content) > 0 return result_file except Exception as e: diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 2c14223..8cc5a2e 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -14,7 +14,7 @@ import Settings 1.0 ApplicationWindow { id: mainWindow visible: true - minimumWidth: 980 // comfortable initial size + minimumWidth: 980 // comfortable initial size for all platforms (as the same style is used for any of them) minimumHeight: 300 height: 530 title: 'stm32pio' @@ -113,7 +113,10 @@ ApplicationWindow { verticalAlignment: TextEdit.AlignVCenter text: `2018 - 2020 © ussserrr
GitHub` - onLinkActivated: Qt.openUrlExternally(link) + onLinkActivated: { + Qt.openUrlExternally(link); + aboutDialog.close(); + } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton // we don't want to eat clicks on the Text @@ -146,24 +149,9 @@ ApplicationWindow { } } - // TODO: fix (jumps skipping next) - function moveToNextAndRemove() { - // Select and go to some adjacent index before deleting the current project. -1 is a correct - // QML index (note that for Python it can jump to the end of the list, ensure a consistency!) + function moveToPrevAndRemove() { const indexToRemove = projectsListView.currentIndex; - let indexToMoveTo; - if (indexToRemove === (projectsListView.count - 1)) { - indexToMoveTo = indexToRemove - 1; - } else { - indexToMoveTo = indexToRemove + 1; - } - - projectsListView.currentIndex = indexToMoveTo; - projectsWorkspaceView.currentIndex = indexToMoveTo; - - // There is some strange bug when the workspace view (highest level StackLayout) disappears after - // the project deletion (even when the removal is performed in a separated Timer after some delay - // and the current index is definitely has already changed for both widgets) + projectsListView.decrementCurrentIndex(); projectsModel.removeProject(indexToRemove); } @@ -173,7 +161,7 @@ ApplicationWindow { Action { text: '&Settings'; onTriggered: settingsDialog.open() } Action { text: '&About'; onTriggered: aboutDialog.open() } MenuSeparator { } - // Use this instead of Qt.qiut() to prevent segfaults (messed up shutdown order) + // Use mainWindow.close() instead of Qt.quit() to prevent segfaults (messed up shutdown order) Action { text: '&Quit'; onTriggered: mainWindow.close() } } } @@ -244,6 +232,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true clip: true // crawls under the Add/Remove buttons otherwise + keyNavigationWraps: true highlight: Rectangle { color: 'darkseagreen' } highlightMoveDuration: 0 // turn off animations @@ -255,6 +244,7 @@ ApplicationWindow { (See setInitInfo docs) One of the two main widgets representing the project. Use Loader component as it can give us the relible time of all its children loading completion (unlike Component.onCompleted) */ + id: listViewDelegate Loader { onLoaded: setInitInfo(index) sourceComponent: RowLayout { @@ -271,9 +261,9 @@ ApplicationWindow { if (state['INIT_ERROR']) { projectName.color = 'indianred'; projectCurrentStage.color = 'indianred'; - } else if (!project.fromStartup) { - // Do not touch those projects that have been loaded on startup (from the QSettings), - // only new ones added during this session + } else if (!project.fromStartup && projectsModel.rowCount() > 1) { + // Do not touch those projects that have been loaded on startup (from the QSettings), only the new ones + // added during this session. Also, do not highlight if there is only a single element in the list projectName.color = 'seagreen'; projectCurrentStage.color = 'seagreen'; } @@ -294,9 +284,9 @@ ApplicationWindow { Text { id: projectName leftPadding: 5 - rightPadding: busy.running ? 0 : leftPadding + rightPadding: busy.visible ? 0 : leftPadding Layout.alignment: Qt.AlignBottom - Layout.preferredWidth: busy.running ? + Layout.preferredWidth: busy.visible ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width elide: Text.ElideMiddle @@ -306,9 +296,9 @@ ApplicationWindow { Text { id: projectCurrentStage leftPadding: 5 - rightPadding: busy.running ? 0 : leftPadding + rightPadding: busy.visible ? 0 : leftPadding Layout.alignment: Qt.AlignTop - Layout.preferredWidth: busy.running ? + Layout.preferredWidth: busy.visible ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width elide: Text.ElideRight @@ -322,7 +312,8 @@ ApplicationWindow { Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height Layout.preferredHeight: parent.height - running: parent.initLoading || (project && project.actionRunning) + // It is important to use 'visible' instead of 'running' for stable visual appearance + visible: project.actionRunning || parent.initLoading // TODO TypeError: Cannot read property 'actionRunning' of null } MouseArea { @@ -331,10 +322,7 @@ ApplicationWindow { width: parent.width height: parent.height enabled: !parent.initLoading - onClicked: { - projectsListView.currentIndex = index; - projectsWorkspaceView.currentIndex = index; - } + onClicked: projectsListView.currentIndex = index } } } @@ -346,16 +334,13 @@ ApplicationWindow { currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] onAccepted: projectsModel.addProjectByPath([folder]) } - RowLayout { + RowLayout { // TODO: move to ListView's footer Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter Layout.fillWidth: true Connections { target: projectsModel - onDuplicateFound: { - projectsListView.currentIndex = duplicateIndex; - projectsWorkspaceView.currentIndex = duplicateIndex; - } + onDuplicateFound: projectsListView.currentIndex = duplicateIndex } Button { text: 'Add' @@ -363,13 +348,16 @@ ApplicationWindow { display: AbstractButton.TextBesideIcon icon.source: 'icons/add.svg' onClicked: addProjectFolderDialog.open() + ToolTip.visible: projectsListView.count === 0 && !loadingOverlay.visible // show when there is no items in the list + ToolTip.text: "Hint: add your project using this button or drag'n'drop it into the window" } Button { text: 'Remove' + visible: projectsListView.currentIndex !== -1 // show only if any item is selected Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter display: AbstractButton.TextBesideIcon icon.source: 'icons/remove.svg' - onClicked: moveToNextAndRemove() + onClicked: moveToPrevAndRemove() } } } @@ -387,6 +375,10 @@ ApplicationWindow { Layout.rightMargin: 10 Layout.topMargin: 10 + Connections { + target: projectsListView + onCurrentIndexChanged: projectsWorkspaceView.currentIndex = projectsListView.currentIndex + } Repeater { // Use similar to ListView pattern (same projects model, Loader component) model: projectsModel @@ -436,18 +428,18 @@ ApplicationWindow { /* Detect changes of a project outside of the app */ + property bool projectIncorrectDialogIsOpen: false QtDialogs.MessageDialog { id: projectIncorrectDialog text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
The project will be removed from the app. It will not affect any real content` icon: QtDialogs.StandardIcon.Critical onAccepted: { - moveToNextAndRemove(); + moveToPrevAndRemove(); mainOrInitScreen.projectIncorrectDialogIsOpen = false; } } signal handleState() - property bool projectIncorrectDialogIsOpen: false property var stateCached: ({}) onHandleState: { if (mainWindow.active && (projectIndex === projectsWorkspaceView.currentIndex) && !projectIncorrectDialogIsOpen && !project.actionRunning) { @@ -667,7 +659,7 @@ ApplicationWindow { id: actionButton text: model.name Layout.rightMargin: model.margin - property bool highlight: false + property bool shouldBeHighlighted: false property int buttonIndex: -1 Component.onCompleted: { buttonIndex = index; @@ -708,8 +700,8 @@ ApplicationWindow { } DSM.SignalTransition { targetState: highlighted - signal: actionButton.highlightChanged - guard: actionButton.highlight + signal: actionButton.shouldBeHighlightedChanged + guard: actionButton.shouldBeHighlighted } onEntered: { actionButton.enabled = true; @@ -757,8 +749,8 @@ ApplicationWindow { id: highlighted DSM.SignalTransition { targetState: mainHistory - signal: actionButton.highlightChanged - guard: !actionButton.highlight + signal: actionButton.shouldBeHighlightedChanged + guard: !actionButton.shouldBeHighlighted } onEntered: { actionButton.palette.button = Qt.lighter('lightgreen', 1.2); @@ -780,13 +772,13 @@ ApplicationWindow { property bool shiftPressedLastState: false function shiftHandler() { for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { - projActionsRow.children[i].highlight = shiftPressed; + projActionsRow.children[i].shouldBeHighlighted = shiftPressed; } } onClicked: { if (shiftPressed && buttonIndex >= buttonsModel.statefulActionsStartIndex) { for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { - projActionsRow.children[i].highlight = false; + projActionsRow.children[i].shouldBeHighlighted = false; } for (let i = buttonsModel.statefulActionsStartIndex; i < buttonIndex; ++i) { project.run(buttonsModel.get(i).action, []); @@ -804,15 +796,15 @@ ApplicationWindow { } shiftPressed = mouse.modifiers & Qt.ShiftModifier; // bitwise AND - if (shiftPressedLastState !== shiftPressed) { + if (shiftPressedLastState !== shiftPressed) { // reduce number of unnecessary shiftHandler() calls shiftPressedLastState = shiftPressed; shiftHandler(); } } onEntered: { if (model.action !== 'start_editor') { - let preparedText = - `Ctrl-click to open the editor specified in the Settings after the operation`; + let preparedText = `Ctrl-click to open the editor specified in the Settings + after the operation`; if (buttonIndex >= buttonsModel.statefulActionsStartIndex) { preparedText += `, Shift-click to perform all actions prior this one (including). From 5859863ae7a0e80059f7e5dbac50473c8f4b2cb4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 14 Apr 2020 22:35:50 +0300 Subject: [PATCH 12/20] TODO.md divide into sections --- TODO.md | 72 ++++++++++++++++++++++++------------------- stm32pio_gui/main.qml | 4 +-- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/TODO.md b/TODO.md index 9c2b503..da4099d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,33 +1,38 @@ # TODOs +## Business logic, business features - [ ] Middleware support (FreeRTOS, etc.) - [ ] Arduino framework support (needs research to check if it is possible) - - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - - [ ] GUI. Tests (research approaches and patterns) - - [x] GUI. Reduce number of calls to 'state' (many IO operations) - - [x] GUI. Drag and drop the new folder into the app window - - [ ] GUI. Multiple projects addition - - [ ] GUI. Divide on multiple modules (both Python and QML) - - [ ] GUI. Implement other methods for Qt abstract models - - [x] GUI. Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) - - [x] GUI. On 'Clean' clean the log too - - [x] GUI. Stop the chain of commands if someone drops -1 or an exception - - [ ] GUI. 2 types of logging formatters for 2 verbosity levels - - [x] GUI. Check for projects duplication - - [x] GUI. Maybe use QML State for action buttons appearance - - [x] GUI. Projects are not destructed until quit (something preserving the link probably...) - - [x] GUI. Fix settings (window doesn't match real) - - [ ] GUI. TypeError: Cannot read property 'actionRunning' of null (deconstruction order) - - [ ] GUI. QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - - [ ] GUI. Fix high CPU usage (most likely some thread consuming) - - [ ] GUI. Bug in log box scrolling behavior (autoscroll sometimes turns off, should re-enable when starting any action) - - [x] GUI. Fix loader when action running - - [ ] GUI. Mark list item when action is done and it is not a current item (i.e. notify a user) - - [ ] GUI. Highlight actions that were picked for continuous run (with some border, for example) - - [ ] GUI. Mark last error'ed action - - [x] GUI. Action buttons widget state machine diagram - - [x] GUI. Fix messed up performance when the list index changes! - - [ ] GUI. Relative resource paths: + - [ ] Create VSCode plugin + +## GUI version + - [ ] Tests (research approaches and patterns) + - [ ] Test performance with a large number of projects in the model + - [x] Reduce number of calls to 'state' (many IO operations) + - [x] Drag and drop the new folder into the app window + - [ ] Multiple projects addition + - [ ] Divide on multiple modules (both Python and QML) + - [ ] Implement other methods for Qt abstract models + - [x] Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) + - [x] On 'Clean' clean the log too + - [x] Stop the chain of commands if someone drops -1 or an exception + - [ ] 2 types of logging formatters for 2 verbosity levels + - [x] Check for projects duplication + - [x] Maybe use QML State for action buttons appearance + - [x] Projects are not destructed until quit (something preserving the link probably...) + - [x] Fix settings (window doesn't match real) + - [ ] `TypeError: Cannot read property 'actionRunning' of null (deconstruction order)` + - [ ] QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on + - [ ] Fix high CPU usage (most likely some thread consuming) + - [ ] Bug in log box scrolling behavior (autoscroll sometimes turns off, should re-enable when starting any action) + - [x] Fix loader when action running + - [ ] Start with folder open in it was provided on CLI (for example, `stm32pio_gui .`) + - [ ] Mark list item when action is done and it is not a current item (i.e. notify a user) + - [ ] Highlight actions that were picked for continuous run (with some border, for example) + - [ ] Mark last error'ed action + - [x] Action buttons widget state machine diagram + - [x] Fix messed up performance when the list index changes! + - [ ] Relative resource paths: ``` ⌘ python3 Documents/GitHub/stm32pio/stm32pio_gui/app.py @@ -43,26 +48,29 @@ main_window = engine.rootObjects()[0] IndexError: list index out of range ``` - - [ ] Create VSCode plugin - - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments) + +## Core library + - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... + - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments while old ones aren't) - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG') - - [ ] Store a folder initial content in .ini config and ignore it on clean-up process. Allow the user to modify such list (i.e. list of exclusion) + - [ ] Store a folder initial content in .ini config and ignore it on clean-up process. Allow the user to modify such list (i.e. list of exclusion) in the config file. Mb some integration with `.gitignore` - [ ] at some point check for all tools (CubeMX, ...) to be present in the system (both CLI and GUI) (global `--check` command (as `--version`), also before execution of the full cycle (no sense to start if some tool doesn't exist)) - [ ] generate code docs (help user to understand an internal mechanics, e.g. for embedding). Can be uploaded to the GitHub Wiki - - [ ] colored logs, maybe... + - [ ] colored logs, maybe (brakes zero-dependency principle) - [ ] check logging work when embed stm32pio lib in a third-party stuff (no logging setup at all) - [ ] merge subprocess pipes to one where suitable (i.e. `stdout` and `stderr`) - [ ] redirect subprocess pipes to `DEVNULL` where suitable to suppress output (tests) - [ ] Two words about a synchronous nature of the lib and user's responsibility of async wrapping (if needed). Also, maybe migrate to async/await approach in the future - [ ] `__init__`' `parameters` dict argument schema (Python 3.8 feature). - - [ ] See https://docs.python.org/3/howto/logging-cookbook.html#context-info to maybe remade current logging schema (current is, perhaps, a cause of the strange error while testing (in the logging thread)) + - [ ] See https://docs.python.org/3/howto/logging-cookbook.html#context-info to maybe remade current logging schema (current is, perhaps, a cause of the strange error while testing (in the logging thread), also modifies global settings (log message factory)) - [ ] UML diagrams (core, GUI back- and front-ends, thread flows, events, etc.) - [ ] CI is possible (Arch's AUR has the STM32CubeMX package, also there is a direct link). Deploy Docker one in Azure Pipelines, basic at Travis CI - [ ] Test preserving user files and folders on regeneration and mb other operations - [ ] Move special formatters inside the library. It is an implementation detail actually that we use subprocesses and so on - [ ] Mb store the last occurred exception traceback in .ini file and show on some CLI command (so we don't necessarily need to turn on the verbose mode). And, in general, we should show the error reason right off - [ ] 'verbose' and 'non-verbose' tests as `subTest` (also `should_log_error_...`) - - [ ] the lib sometimes raising, sometimes returning the code and it is not consistent. While the reasons behind such behaviour are clear, would be great to always return a result code and raise the exceptions in the outer scope, if there is need to + - [ ] the lib sometimes raising, sometimes returning the code and it is not consistent. While the reasons behind such behavior are clear, would be great to always return a result code and raise the exceptions in the outer scope, if there is need to - [ ] check board (no sense to go further on 'new' if the board in config.ini is not correct) - [ ] check if `platformio.ini` config will be successfully parsed when there are interpolation and/or empty parameters - [x] check if `.ioc` file is a text file on project initialization. Let `_find_ioc_file()` method to use explicitly provided file (useful for GUI). Maybe let user specify it via CLI + - [ ] mb add CLI command for starting the GUI version (for example, `stm32pio --gui`) diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 8cc5a2e..7d463d1 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -861,9 +861,7 @@ ApplicationWindow { SequentialAnimation { id: glowAnimation loops: 3 - onStopped: { - glow.visible = false; - } + onStopped: glow.visible = false OpacityAnimator { target: glow from: 0 From ec7a72510f59f5eec9fd7ac8e971d12dfb41d0fc Mon Sep 17 00:00:00 2001 From: usserr Date: Wed, 15 Apr 2020 00:49:09 +0300 Subject: [PATCH 13/20] implement visual notification about done actions in the projects list --- TODO.md | 11 ++++---- stm32pio_gui/main.qml | 59 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/TODO.md b/TODO.md index da4099d..4d4a8a2 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,7 @@ - [ ] Create VSCode plugin ## GUI version + - [x] When the list item is not active after the action the "current stage" line is not correct anymore. Consider updating or (better) gray out - [ ] Tests (research approaches and patterns) - [ ] Test performance with a large number of projects in the model - [x] Reduce number of calls to 'state' (many IO operations) @@ -21,15 +22,15 @@ - [x] Maybe use QML State for action buttons appearance - [x] Projects are not destructed until quit (something preserving the link probably...) - [x] Fix settings (window doesn't match real) - - [ ] `TypeError: Cannot read property 'actionRunning' of null (deconstruction order)` + - [ ] `TypeError: Cannot read property 'actionRunning' of null (deconstruction order)` (on project deletion only) - [ ] QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - - [ ] Fix high CPU usage (most likely some thread consuming) + - [x] Fix high CPU usage (most likely some thread consuming) - [ ] Bug in log box scrolling behavior (autoscroll sometimes turns off, should re-enable when starting any action) - [x] Fix loader when action running - - [ ] Start with folder open in it was provided on CLI (for example, `stm32pio_gui .`) - - [ ] Mark list item when action is done and it is not a current item (i.e. notify a user) + - [ ] Start with a folder opened if it was provided on CLI (for example, `stm32pio_gui .`) + - [x] Mark list item when action is done and it is not a current item (i.e. notify a user) - [ ] Highlight actions that were picked for continuous run (with some border, for example) - - [ ] Mark last error'ed action + - [x] Mark last error'ed action - [x] Action buttons widget state machine diagram - [x] Fix messed up performance when the list index changes! - [ ] Relative resource paths: diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 7d463d1..abd9351 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -268,13 +268,33 @@ ApplicationWindow { projectCurrentStage.color = 'seagreen'; } } + onActionStarted: { + runningOrDone.currentIndex = 0; + runningOrDone.visible = true; + } + onActionDone: { + if (index !== projectsListView.currentIndex) { + projectCurrentStage.color = 'darkgray'; + runningOrDone.currentIndex = 1; + recentlyDoneIndicator.color = success ? 'lightgreen' : 'lightcoral'; + runningOrDone.visible = true; + } else { + runningOrDone.visible = false; + } + } } Connections { target: projectsListView onCurrentIndexChanged: { - if (projectsListView.currentIndex === index && Qt.colorEqual(projectName.color, 'seagreen')) { - projectName.color = 'black'; - projectCurrentStage.color = 'black'; + if (projectsListView.currentIndex === index) { + if (Qt.colorEqual(projectName.color, 'seagreen')) { + projectName.color = 'black'; + projectCurrentStage.color = 'black'; + } + if (Qt.colorEqual(projectCurrentStage.color, 'darkgray')) { + projectCurrentStage.color = 'black'; + runningOrDone.visible = false; + } } } } @@ -284,9 +304,9 @@ ApplicationWindow { Text { id: projectName leftPadding: 5 - rightPadding: busy.visible ? 0 : leftPadding + rightPadding: runningOrDone.visible ? 0 : leftPadding Layout.alignment: Qt.AlignBottom - Layout.preferredWidth: busy.visible ? + Layout.preferredWidth: runningOrDone.visible ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width elide: Text.ElideMiddle @@ -296,9 +316,9 @@ ApplicationWindow { Text { id: projectCurrentStage leftPadding: 5 - rightPadding: busy.visible ? 0 : leftPadding + rightPadding: runningOrDone.visible ? 0 : leftPadding Layout.alignment: Qt.AlignTop - Layout.preferredWidth: busy.visible ? + Layout.preferredWidth: runningOrDone.visible ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width elide: Text.ElideRight @@ -307,13 +327,30 @@ ApplicationWindow { } } - BusyIndicator { - id: busy + StackLayout { + // TODO: probably can use DSM.StateMachine (or maybe regular State) for it, too + id: runningOrDone Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height Layout.preferredHeight: parent.height - // It is important to use 'visible' instead of 'running' for stable visual appearance - visible: project.actionRunning || parent.initLoading // TODO TypeError: Cannot read property 'actionRunning' of null + visible: parent.initLoading // initial binding + + BusyIndicator { + // Important note: if you toggle visibility frequently better use 'visible' instead of 'running' for stable visual appearance + Layout.fillWidth: true + Layout.fillHeight: true + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { + id: recentlyDoneIndicator + anchors.centerIn: parent + width: 10 + height: width + radius: width * 0.5 + } + } } MouseArea { From e3def69aa28afdca812025370d869eaf6a8e45c3 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 15 Apr 2020 16:33:33 +0300 Subject: [PATCH 14/20] highlight continuous operations, add notifications, support multiple projects addition --- TODO.md | 5 ++-- stm32pio_gui/app.py | 27 ++++++++++---------- stm32pio_gui/main.qml | 59 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index 4d4a8a2..53fe868 100644 --- a/TODO.md +++ b/TODO.md @@ -6,12 +6,13 @@ - [ ] Create VSCode plugin ## GUI version + - [x] Tray icon notification - [x] When the list item is not active after the action the "current stage" line is not correct anymore. Consider updating or (better) gray out - [ ] Tests (research approaches and patterns) - [ ] Test performance with a large number of projects in the model - [x] Reduce number of calls to 'state' (many IO operations) - [x] Drag and drop the new folder into the app window - - [ ] Multiple projects addition + - [x] Multiple projects addition - [ ] Divide on multiple modules (both Python and QML) - [ ] Implement other methods for Qt abstract models - [x] Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) @@ -29,7 +30,7 @@ - [x] Fix loader when action running - [ ] Start with a folder opened if it was provided on CLI (for example, `stm32pio_gui .`) - [x] Mark list item when action is done and it is not a current item (i.e. notify a user) - - [ ] Highlight actions that were picked for continuous run (with some border, for example) + - [x] Highlight actions that were picked for continuous run (with some border, for example) - [x] Mark last error'ed action - [x] Action buttons widget state machine diagram - [x] Fix messed up performance when the list index changes! diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index ce372ff..c08a11c 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# from __future__ import annotations - import collections import logging import pathlib @@ -12,6 +10,7 @@ import time import traceback import weakref +from typing import List try: from PySide2.QtCore import QUrl, Property, QAbstractListModel, QModelIndex, QObject, Qt, Slot, Signal, QThread,\ @@ -240,7 +239,6 @@ def actionRunning(self): @Slot(str) def actionStartedSlot(self, action: str): self.actionStarted.emit(action) - # print('actionRunning TRUE') self._is_action_running = True self.actionRunningChanged.emit() @@ -248,7 +246,6 @@ def actionStartedSlot(self, action: str): def actionDoneSlot(self, action: str, success: bool): if not success: self.workers_pool.clear() # clear the queue - stop further execution - # print('actionRunning FALSE') self._is_action_running = False self.actionRunningChanged.emit() self.actionDone.emit(action, success) @@ -380,7 +377,7 @@ def addProjectByPath(self, str_list: list): the QML GUI). """ - paths_list = [] + paths_list: List[str] = [] for path_str in str_list: path_qurl = QUrl(path_str) if path_qurl.isLocalFile(): @@ -388,8 +385,12 @@ def addProjectByPath(self, str_list: list): elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix paths_list.append(path_str) # just use source string - if len(paths_list): - path = paths_list[0] # for now just respond on one item even if a list was provided + if len(paths_list) == 1: + path = paths_list[0] + elif len(paths_list) > 1: + for path in paths_list: # TODO: not so elegant... + self.addProjectByPath([path]) + return else: module_logger.warning("No path were given") return @@ -475,16 +476,18 @@ class Settings(QSettings): QML side. Also, retrieve settings on creation. """ - DEFAULT_SETTINGS = { + DEFAULTS = { 'editor': '', - 'verbose': False + 'verbose': False, + 'notifications': True } - def __init__(self, prefix: str, defaults: dict, qs_args: list = None, qs_kwargs: dict = None, + def __init__(self, prefix: str, defaults: dict = None, qs_args: list = None, qs_kwargs: dict = None, external_triggers: dict = None): qs_args = qs_args if qs_args is not None else [] qs_kwargs = qs_kwargs if qs_kwargs is not None else {} + defaults = defaults if defaults is not None else self.DEFAULTS super().__init__(*qs_args, **qs_kwargs) @@ -558,10 +561,6 @@ def verbose_setter(value): project.logger.setLevel(logging.DEBUG if value else logging.INFO) settings = Settings(prefix='app/settings/', - defaults={ - 'editor': '', - 'verbose': False - }, qs_kwargs={ 'parent': app }, diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index abd9351..dc968dd 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -68,13 +68,28 @@ ApplicationWindow { leftPadding: -3 } + Label { + text: 'Notifications' + Layout.preferredWidth: 140 + } + CheckBox { + id: notifications + leftPadding: -3 + } + Item { Layout.preferredWidth: 140 } + Text { + Layout.preferredWidth: 250 + wrapMode: Text.Wrap + text: "Get messages about completed project actions when the app is in background" + } + Text { Layout.columnSpan: 2 Layout.maximumWidth: parent.width topPadding: 30 bottomPadding: 30 wrapMode: Text.Wrap - text: "To clear ALL app settings including the list of added projects click \"Reset\" then restart the app" + text: 'To clear ALL app settings including the list of added projects click "Reset" then restart the app' } } // Set UI values there so they are always reflect actual parameters @@ -82,11 +97,16 @@ ApplicationWindow { if (visible) { editor.text = settings.get('editor'); verbose.checked = settings.get('verbose'); + notifications.checked = settings.get('notifications'); } } onAccepted: { settings.set('editor', editor.text); settings.set('verbose', verbose.checked); + if (settings.get('notifications') !== notifications.checked) { + settings.set('notifications', notifications.checked); + sysTrayIcon.visible = notifications.checked; + } } onReset: { settings.clear(); @@ -112,7 +132,8 @@ ApplicationWindow { horizontalAlignment: TextEdit.AlignHCenter verticalAlignment: TextEdit.AlignVCenter text: `2018 - 2020 © ussserrr
- GitHub` + GitHub
+ Powered by Python3, PlatformIO, Qt for Python` onLinkActivated: { Qt.openUrlExternally(link); aboutDialog.close(); @@ -166,6 +187,12 @@ ApplicationWindow { } } + QtLabs.SystemTrayIcon { + id: sysTrayIcon + icon.source: 'icons/icon.svg' + visible: true + } + DropArea { id: dropArea anchors.fill: parent @@ -191,7 +218,7 @@ ApplicationWindow { } Text { // anchors.topMargin: 20 - text: "Drop project folder to add..." + text: "Drop projects folders to add..." font.pointSize: 24 // different on different platforms, Qt's bug font.weight: Font.Black // heaviest } @@ -697,9 +724,11 @@ ApplicationWindow { text: model.name Layout.rightMargin: model.margin property bool shouldBeHighlighted: false + property bool shouldBeHighlightedWhileRunning: false property int buttonIndex: -1 Component.onCompleted: { buttonIndex = index; + background.border.color = 'dimgray'; } ToolTip { visible: parent.hovered @@ -816,6 +845,7 @@ ApplicationWindow { if (shiftPressed && buttonIndex >= buttonsModel.statefulActionsStartIndex) { for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { projActionsRow.children[i].shouldBeHighlighted = false; + projActionsRow.children[i].shouldBeHighlightedWhileRunning = true; } for (let i = buttonsModel.statefulActionsStartIndex; i < buttonIndex; ++i) { project.run(buttonsModel.get(i).action, []); @@ -870,6 +900,9 @@ ApplicationWindow { palette.button = 'gold'; } glow.visible = false; + if (shouldBeHighlightedWhileRunning) { + background.border.width = 2; + } } onActionDone: { if (action === model.action) { @@ -879,6 +912,26 @@ ApplicationWindow { glow.color = 'lightcoral'; } glow.visible = true; + + if (settings.get('notifications') && !mainWindow.active) { + sysTrayIcon.showMessage( + success ? 'Success' : 'Error', + `${project.name} - ${model.name}`, + success ? QtLabs.SystemTrayIcon.Information : QtLabs.SystemTrayIcon.Warning, + 5000 + ); + } + + if (shouldBeHighlightedWhileRunning && + ((buttonIndex === (buttonsModel.count - 1)) || + (projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) + ) + ) { + for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; + projActionsRow.children[i].background.border.width = 0; + } + } } } } From d3e3ca69d603444e65b00bfb94089fc984ebac4c Mon Sep 17 00:00:00 2001 From: usserr Date: Wed, 15 Apr 2020 21:51:09 +0300 Subject: [PATCH 15/20] fix multiple project creation, move Add and Remove buttons into the ListView footer --- TODO.md | 2 + stm32pio_gui/app.py | 20 ++++---- stm32pio_gui/main.qml | 117 ++++++++++++++++++++++-------------------- 3 files changed, 74 insertions(+), 65 deletions(-) diff --git a/TODO.md b/TODO.md index 53fe868..607f0fa 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,7 @@ - [x] When the list item is not active after the action the "current stage" line is not correct anymore. Consider updating or (better) gray out - [ ] Tests (research approaches and patterns) - [ ] Test performance with a large number of projects in the model + - [ ] Test with different timings - [x] Reduce number of calls to 'state' (many IO operations) - [x] Drag and drop the new folder into the app window - [x] Multiple projects addition @@ -34,6 +35,7 @@ - [x] Mark last error'ed action - [x] Action buttons widget state machine diagram - [x] Fix messed up performance when the list index changes! + - [x] Fix tooltip on Linux ('Add' works OK actually) - [ ] Relative resource paths: ``` diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index c08a11c..1ff6c7b 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -377,6 +377,14 @@ def addProjectByPath(self, str_list: list): the QML GUI). """ + if len(str_list) > 1: + for path_str in str_list: + self.addProjectByPath([path_str]) + return + elif len(str_list) == 0: + module_logger.warning("No path were given") + return + paths_list: List[str] = [] for path_str in str_list: path_qurl = QUrl(path_str) @@ -385,17 +393,9 @@ def addProjectByPath(self, str_list: list): elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix paths_list.append(path_str) # just use source string - if len(paths_list) == 1: - path = paths_list[0] - elif len(paths_list) > 1: - for path in paths_list: # TODO: not so elegant... - self.addProjectByPath([path]) - return - else: - module_logger.warning("No path were given") - return + path = paths_list[0] - duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if + duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if list_item.project is not None and list_item.project.path.samefile(pathlib.Path(path))), -1) if duplicate_index >= 0: module_logger.warning(f"This project is already in the list: {path}") diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index dc968dd..435c4d6 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -2,10 +2,10 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtGraphicalEffects 1.12 -import QtQuick.Dialogs 1.3 as QtDialogs +import QtQuick.Dialogs 1.3 as Dialogs import QtQml.StateMachine 1.12 as DSM -import Qt.labs.platform 1.1 as QtLabs +import Qt.labs.platform 1.1 as Labs import ProjectListItem 1.0 import Settings 1.0 @@ -44,10 +44,10 @@ ApplicationWindow { Slightly customized QSettings */ property Settings settings: appSettings - QtDialogs.Dialog { + Dialogs.Dialog { id: settingsDialog title: 'Settings' - standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel | QtDialogs.StandardButton.Reset + standardButtons: Dialogs.StandardButton.Save | Dialogs.StandardButton.Cancel | Dialogs.StandardButton.Reset GridLayout { columns: 2 @@ -78,16 +78,17 @@ ApplicationWindow { } Item { Layout.preferredWidth: 140 } Text { - Layout.preferredWidth: 250 + Layout.preferredWidth: 250 // Detected recursive rearrange. Aborting after two iterations (on Windows) wrapMode: Text.Wrap + color: 'dimgray' text: "Get messages about completed project actions when the app is in background" } Text { Layout.columnSpan: 2 - Layout.maximumWidth: parent.width + Layout.maximumWidth: 250 topPadding: 30 - bottomPadding: 30 + bottomPadding: 10 wrapMode: Text.Wrap text: 'To clear ALL app settings including the list of added projects click "Reset" then restart the app' } @@ -114,10 +115,10 @@ ApplicationWindow { } } - QtDialogs.Dialog { + Dialogs.Dialog { id: aboutDialog title: 'About' - standardButtons: QtDialogs.StandardButton.Close + standardButtons: Dialogs.StandardButton.Close ColumnLayout { Rectangle { width: 250 @@ -132,8 +133,8 @@ ApplicationWindow { horizontalAlignment: TextEdit.AlignHCenter verticalAlignment: TextEdit.AlignVCenter text: `2018 - 2020 © ussserrr
- GitHub
- Powered by Python3, PlatformIO, Qt for Python` + GitHub

+ Powered by Python3, PlatformIO, Qt for Python, FlatIcons and other awesome technologies` onLinkActivated: { Qt.openUrlExternally(link); aboutDialog.close(); @@ -170,9 +171,9 @@ ApplicationWindow { } } - function moveToPrevAndRemove() { + function removeCurrentProject() { const indexToRemove = projectsListView.currentIndex; - projectsListView.decrementCurrentIndex(); + indexToRemove === 0 ? projectsListView.incrementCurrentIndex() : projectsListView.decrementCurrentIndex(); projectsModel.removeProject(indexToRemove); } @@ -187,10 +188,10 @@ ApplicationWindow { } } - QtLabs.SystemTrayIcon { + Labs.SystemTrayIcon { id: sysTrayIcon icon.source: 'icons/icon.svg' - visible: true + visible: settings.get('notifications') } DropArea { @@ -288,9 +289,10 @@ ApplicationWindow { if (state['INIT_ERROR']) { projectName.color = 'indianred'; projectCurrentStage.color = 'indianred'; - } else if (!project.fromStartup && projectsModel.rowCount() > 1) { + } else if (!project.fromStartup && projectsModel.rowCount() > 1 && index !== projectsListView.currentIndex) { // Do not touch those projects that have been loaded on startup (from the QSettings), only the new ones - // added during this session. Also, do not highlight if there is only a single element in the list + // added during this session. Also, do not highlight if there is only a single element in the list or + // the list is already located to this item projectName.color = 'seagreen'; projectCurrentStage.color = 'seagreen'; } @@ -363,7 +365,8 @@ ApplicationWindow { visible: parent.initLoading // initial binding BusyIndicator { - // Important note: if you toggle visibility frequently better use 'visible' instead of 'running' for stable visual appearance + // Important note: if you toggle visibility frequently better use 'visible' + // instead of 'running' for stable visual appearance Layout.fillWidth: true Layout.fillHeight: true } @@ -391,37 +394,43 @@ ApplicationWindow { } } } - } - - QtLabs.FolderDialog { - id: addProjectFolderDialog - currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] - onAccepted: projectsModel.addProjectByPath([folder]) - } - RowLayout { // TODO: move to ListView's footer - Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter - Layout.fillWidth: true - Connections { - target: projectsModel - onDuplicateFound: projectsListView.currentIndex = duplicateIndex + Labs.FolderDialog { + id: addProjectFolderDialog + currentFolder: Labs.StandardPaths.standardLocations(Labs.StandardPaths.HomeLocation)[0] + onAccepted: projectsModel.addProjectByPath([folder]) } - Button { - text: 'Add' - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - display: AbstractButton.TextBesideIcon - icon.source: 'icons/add.svg' - onClicked: addProjectFolderDialog.open() - ToolTip.visible: projectsListView.count === 0 && !loadingOverlay.visible // show when there is no items in the list - ToolTip.text: "Hint: add your project using this button or drag'n'drop it into the window" - } - Button { - text: 'Remove' - visible: projectsListView.currentIndex !== -1 // show only if any item is selected - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - display: AbstractButton.TextBesideIcon - icon.source: 'icons/remove.svg' - onClicked: moveToPrevAndRemove() + footerPositioning: ListView.OverlayFooter + footer: Rectangle { + z: 2 + width: projectsListView.width + implicitHeight: listFooter.implicitHeight + color: mainWindow.color + RowLayout { + id: listFooter + anchors.centerIn: parent + Connections { + target: projectsModel + onDuplicateFound: projectsListView.currentIndex = duplicateIndex + } + Button { + text: 'Add' + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + display: AbstractButton.TextBesideIcon + icon.source: 'icons/add.svg' + onClicked: addProjectFolderDialog.open() + ToolTip.visible: projectsListView.count === 0 && !loadingOverlay.visible // show when there is no items in the list + ToolTip.text: "Hint: add your project using this button or drag'n'drop it into the window" + } + Button { + text: 'Remove' + visible: projectsListView.currentIndex !== -1 // show only if any item is selected + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + display: AbstractButton.TextBesideIcon + icon.source: 'icons/remove.svg' + onClicked: removeCurrentProject() + } + } } } } @@ -493,13 +502,13 @@ ApplicationWindow { Detect changes of a project outside of the app */ property bool projectIncorrectDialogIsOpen: false - QtDialogs.MessageDialog { + Dialogs.MessageDialog { id: projectIncorrectDialog text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
The project will be removed from the app. It will not affect any real content` - icon: QtDialogs.StandardIcon.Critical + icon: Dialogs.StandardIcon.Critical onAccepted: { - moveToPrevAndRemove(); + removeCurrentProject(); mainOrInitScreen.projectIncorrectDialogIsOpen = false; } } @@ -917,15 +926,14 @@ ApplicationWindow { sysTrayIcon.showMessage( success ? 'Success' : 'Error', `${project.name} - ${model.name}`, - success ? QtLabs.SystemTrayIcon.Information : QtLabs.SystemTrayIcon.Warning, + success ? Labs.SystemTrayIcon.Information : Labs.SystemTrayIcon.Warning, 5000 ); } if (shouldBeHighlightedWhileRunning && - ((buttonIndex === (buttonsModel.count - 1)) || - (projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) - ) + (buttonIndex === (buttonsModel.count - 1) || + projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) ) { for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; @@ -981,7 +989,6 @@ ApplicationWindow { readOnly: true selectByMouse: true wrapMode: Text.WordWrap - font.family: 'Courier' font.pointSize: 10 // different on different platforms, Qt's bug textFormat: TextEdit.RichText } From 6e45ed34d49d525d652e477f1d184c0e67d905c2 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 16 Apr 2020 17:45:49 +0300 Subject: [PATCH 16/20] spruce up QML --- TODO.md | 13 +- stm32pio/lib.py | 1 + stm32pio_gui/app.py | 2 +- stm32pio_gui/main.qml | 279 +++++++++++++++++++++++------------------- 4 files changed, 163 insertions(+), 132 deletions(-) diff --git a/TODO.md b/TODO.md index 607f0fa..b61ebf0 100644 --- a/TODO.md +++ b/TODO.md @@ -6,10 +6,19 @@ - [ ] Create VSCode plugin ## GUI version - - [x] Tray icon notification + - [ ] Some visual flaws when the window have got resized (e.g. 'Add' button goes off-screen, 'Log' area crawls onto the status bar) + - [x] Tray icon notifications - [x] When the list item is not active after the action the "current stage" line is not correct anymore. Consider updating or (better) gray out - [ ] Tests (research approaches and patterns) - - [ ] Test performance with a large number of projects in the model + - [ ] Test performance with a large number of projects in the model. First test was made: + + 1. Some projects occasionally change `initLoading` by itself (probably Loader unloads the content) (hence cannot click on them, busy indicator appearing) + + Note: Delegates are instantiated as needed and may be destroyed at any time. They are parented to ListView's contentItem, not to the view itself. State should never be stored in a delegate. + + Or do not use ListView at all (replace be Repeater, for example) as it can reset our "notifications" + 2. Some projects show OK even after its deletion (only the app restart helps) + - [ ] Test with different timings - [x] Reduce number of calls to 'state' (many IO operations) - [x] Drag and drop the new folder into the app window diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 690bd90..0caaa55 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -207,6 +207,7 @@ def state(self) -> ProjectState: Constructing and returning the current state of the project (tweaked dict, see ProjectState docs) """ + print('wants state') # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") pio_is_initialized = False diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index 1ff6c7b..2d2c419 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -189,9 +189,9 @@ def init_project(self, *args, **kwargs) -> None: self._finalizer = weakref.finalize(self, self.at_exit, self.workers_pool, self.logging_worker, self.name if self.project is None else str(self.project)) self.qml_ready.wait() # wait for the GUI to initialize - self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending self.stageChanged.emit() self.stateChanged.emit() + self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending @Property(bool) def fromStartup(self): diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 435c4d6..5f5c613 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -21,7 +21,7 @@ ApplicationWindow { color: 'whitesmoke' /* - Notify the front-end about the end of an initial loading + Notify the front about the end of an initial loading */ signal backendLoaded() onBackendLoaded: loadingOverlay.close() @@ -43,7 +43,7 @@ ApplicationWindow { /* Slightly customized QSettings */ - property Settings settings: appSettings + readonly property Settings settings: appSettings Dialogs.Dialog { id: settingsDialog title: 'Settings' @@ -52,16 +52,17 @@ ApplicationWindow { columns: 2 Label { - text: 'Editor' Layout.preferredWidth: 140 + text: 'Editor' } TextField { id: editor + placeholderText: "e.g. atom" } Label { - text: 'Verbose output' Layout.preferredWidth: 140 + text: 'Verbose output' } CheckBox { id: verbose @@ -69,16 +70,16 @@ ApplicationWindow { } Label { - text: 'Notifications' Layout.preferredWidth: 140 + text: 'Notifications' } CheckBox { id: notifications leftPadding: -3 } - Item { Layout.preferredWidth: 140 } + Item { Layout.preferredWidth: 140 } // spacer Text { - Layout.preferredWidth: 250 // Detected recursive rearrange. Aborting after two iterations (on Windows) + Layout.preferredWidth: 250 // Detected recursive rearrange. Aborting after two iterations wrapMode: Text.Wrap color: 'dimgray' text: "Get messages about completed project actions when the app is in background" @@ -93,7 +94,7 @@ ApplicationWindow { text: 'To clear ALL app settings including the list of added projects click "Reset" then restart the app' } } - // Set UI values there so they are always reflect actual parameters + // Set UI values there so they are always reflect the actual parameters onVisibleChanged: { if (visible) { editor.text = settings.get('editor'); @@ -157,7 +158,7 @@ ApplicationWindow { a number of widgets currently loaded for each project in model and informs the Qt-side right after all necessary components become ready. */ - property var initInfo: ({}) + readonly property var initInfo: ({}) function setInitInfo(projectIndex) { if (projectIndex in initInfo) { initInfo[projectIndex]++; @@ -202,12 +203,8 @@ ApplicationWindow { parent: Overlay.overlay anchors.centerIn: Overlay.overlay modal: true - background: Rectangle { - opacity: 0.0 - } - Overlay.modal: Rectangle { - color: "#aaffffff" - } + background: Rectangle { opacity: 0.0 } + Overlay.modal: Rectangle { color: "#aaffffff" } contentItem: Column { spacing: 20 Image { @@ -218,7 +215,6 @@ ApplicationWindow { sourceSize.width: 64 } Text { - // anchors.topMargin: 20 text: "Drop projects folders to add..." font.pointSize: 24 // different on different platforms, Qt's bug font.weight: Font.Black // heaviest @@ -270,16 +266,16 @@ ApplicationWindow { delegate: Component { /* (See setInitInfo docs) One of the two main widgets representing the project. Use Loader component - as it can give us the relible time of all its children loading completion (unlike Component.onCompleted) + as it can give us the relible timestamp of all its children loading completion (unlike Component.onCompleted) */ id: listViewDelegate Loader { onLoaded: setInitInfo(index) sourceComponent: RowLayout { - property bool initLoading: true // initial waiting for the backend-side - property ProjectListItem project: projectsModel.getProject(index) + property bool initLoading: true // initial waiting for the backend-side TODO: do not store state in the delegate! + readonly property ProjectListItem project: projectsModel.getProject(index) Connections { - target: project // (newbie hint) sender which signals we want to catch below + target: project // Currently, this event is equivalent to the complete initialization of the backend side of the project onNameChanged: { initLoading = false; @@ -303,8 +299,8 @@ ApplicationWindow { } onActionDone: { if (index !== projectsListView.currentIndex) { - projectCurrentStage.color = 'darkgray'; - runningOrDone.currentIndex = 1; + projectCurrentStage.color = 'darkgray'; // show that the stage has changed from the last visit + runningOrDone.currentIndex = 1; // show "notification" about the finished action recentlyDoneIndicator.color = success ? 'lightgreen' : 'lightcoral'; runningOrDone.visible = true; } else { @@ -315,6 +311,7 @@ ApplicationWindow { Connections { target: projectsListView onCurrentIndexChanged: { + // "Read" all "notifications" after navigating to the list element if (projectsListView.currentIndex === index) { if (Qt.colorEqual(projectName.color, 'seagreen')) { projectName.color = 'black'; @@ -356,8 +353,9 @@ ApplicationWindow { } } + // Show whether a busy indicator or a finished action notification StackLayout { - // TODO: probably can use DSM.StateMachine (or maybe regular State) for it, too + // TODO: probably can use DSM.StateMachine (or maybe regular State) for this, too id: runningOrDone Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height @@ -373,7 +371,7 @@ ApplicationWindow { Item { Layout.fillWidth: true Layout.fillHeight: true - Rectangle { + Rectangle { // Circle :) id: recentlyDoneIndicator anchors.centerIn: parent width: 10 @@ -401,7 +399,7 @@ ApplicationWindow { onAccepted: projectsModel.addProjectByPath([folder]) } footerPositioning: ListView.OverlayFooter - footer: Rectangle { + footer: Rectangle { // Probably should use Pane but need to override default window color then z: 2 width: projectsListView.width implicitHeight: listFooter.implicitHeight @@ -411,6 +409,7 @@ ApplicationWindow { anchors.centerIn: parent Connections { target: projectsModel + // Just added project is already in the list so abort the addition and jump to the existing one onDuplicateFound: projectsListView.currentIndex = duplicateIndex } Button { @@ -466,31 +465,60 @@ ApplicationWindow { Use another one StackLayout to separate Project initialization "screen" and Main one */ sourceComponent: StackLayout { - id: mainOrInitScreen // for clarity + id: mainOrInitScreen currentIndex: -1 // at widget creation we do not show main nor init screen Layout.fillWidth: true Layout.fillHeight: true - property ProjectListItem project: projectsModel.getProject(index) + readonly property ProjectListItem project: projectsModel.getProject(index) - Connections { - target: project // sender - onLogAdded: { - if (level === Logging.WARNING) { - log.append('
' + message + '
'); - } else if (level >= Logging.ERROR) { - log.append('
' + message + '
'); - } else { - log.append('
' + message + '
'); + /* + State retrieving procedure is relatively expensive (many IO operations) so we optimize it by getting the state + only in certain situations (see Component.onCompleted below) and caching a value in the local varible. Then, all + widgets can pick up this value as many times as they want while not abusing the real property getter. Such a subscription + can be established by the creation of a local reference to the cache and listening to the change event like this: + + property var stateCachedNotifier: stateCached + onStateCachedNotifierChanged: { + // use stateCached there } + */ + signal handleState() + property var stateCached: ({}) + onHandleState: { + if (mainWindow.active && // the app got foreground + projectIndex === projectsWorkspaceView.currentIndex && // only for the current list item + !projectIncorrectDialog.visible && + !project.actionRunning + ) { + const state = project.state; + stateCached = state; + + project.stageChanged(); // side-effect: update the stage at the same time + + // if (!state['INIT_ERROR'] && !state['EMPTY']) { // i.e. no .ioc file but the project was able to initialize itself + // // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + // projectIncorrectDialogIsOpen = true; + // projectIncorrectDialog.open(); + // } } + } + Component.onCompleted: { + // Several events lead to a single handler + project.stateChanged.connect(handleState); // the model has notified about the change + projectsWorkspaceView.currentIndexChanged.connect(handleState); // the project was selected in the list + mainWindow.activeChanged.connect(handleState); // the app window has got (or lost, filter in the handler) the focus + } + + Connections { + target: project // Currently, this event is equivalent to the complete initialization of the backend side of the project onNameChanged: { - const state = project.state; - const completedStages = Object.keys(state).filter(stateName => state[stateName]); + // const state = project.state; + const completedStages = Object.keys(stateCached).filter(stateName => stateCached[stateName]); if (completedStages.length === 1 && completedStages[0] === 'EMPTY') { - initScreenLoader.active = true; + setupScreenLoader.active = true; mainOrInitScreen.currentIndex = 0; // show init dialog } else { mainOrInitScreen.currentIndex = 1; // show main view @@ -498,43 +526,18 @@ ApplicationWindow { } } - /* - Detect changes of a project outside of the app - */ - property bool projectIncorrectDialogIsOpen: false + // property bool projectIncorrectDialogIsOpen: false Dialogs.MessageDialog { id: projectIncorrectDialog + visible: Object.keys(stateCached).length && !stateCached['INIT_ERROR'] && !stateCached['EMPTY'] text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
The project will be removed from the app. It will not affect any real content` icon: Dialogs.StandardIcon.Critical onAccepted: { removeCurrentProject(); - mainOrInitScreen.projectIncorrectDialogIsOpen = false; - } - } - signal handleState() - property var stateCached: ({}) - onHandleState: { - if (mainWindow.active && (projectIndex === projectsWorkspaceView.currentIndex) && !projectIncorrectDialogIsOpen && !project.actionRunning) { - const state = project.state; - stateCached = state; - - project.stageChanged(); // side-effect: update the stage at the same time - - if (!state['INIT_ERROR'] && !state['EMPTY']) { // i.e. no .ioc file but the project was able to initialize itself - // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialogIsOpen = true; - projectIncorrectDialog.open(); - } + // mainOrInitScreen.projectIncorrectDialogIsOpen = false; } } - Component.onCompleted: { - // Several events lead to a single handler - project.stateChanged.connect(handleState); - projectsWorkspaceView.currentIndexChanged.connect(handleState); // the project was selected in the list - mainWindow.activeChanged.connect(handleState); // the app window has got the focus - } - /* Index: 0. Project initialization "screen" @@ -542,7 +545,7 @@ ApplicationWindow { Prompt a user to perform initial setup */ Loader { - id: initScreenLoader + id: setupScreenLoader active: false sourceComponent: Column { Text { @@ -558,19 +561,15 @@ ApplicationWindow { editable: true model: boardsModel // backend-side (simple string model) textRole: 'display' - onAccepted: { - focus = false; - } - onActivated: { - focus = false; - } + onAccepted: focus = false + onActivated: focus = false onFocusChanged: { - if (!focus) { + if (focus) { + selectAll(); + } else { if (find(editText) === -1) { editText = textAt(0); // should be 'None' at index 0 } - } else { - selectAll(); } } } @@ -584,9 +583,10 @@ ApplicationWindow { ToolTip { visible: runCheckBox.hovered Component.onCompleted: { + // Form the tool tip text using action names const actions = []; - for (let i = buttonsModel.statefulActionsStartIndex; i < buttonsModel.count; ++i) { - actions.push(`${buttonsModel.get(i).name}`); + for (let i = projActionsModel.statefulActionsStartIndex; i < projActionsModel.count; ++i) { + actions.push(`${projActionsModel.get(i).name}`); } text = `Do: ${actions.join(' → ')}`; } @@ -621,21 +621,21 @@ ApplicationWindow { topPadding: 20 leftPadding: 18 onClicked: { - // All 'run' operations will be queued + // All 'run' operations will be queued by the backend project.run('save_config', [{ 'project': { 'board': board.editText === board.textAt(0) ? '' : board.editText } }]); if (board.editText === board.textAt(0)) { - project.logAdded("WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO \ - project creation. You can set it in 'stm32pio.ini' file in the project directory", + project.logAdded('WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO \ + project creation. You can set it in "stm32pio.ini" file in the project directory', Logging.WARNING); } if (runCheckBox.checked) { - for (let i = buttonsModel.statefulActionsStartIndex + 1; i < buttonsModel.count; ++i) { - project.run(buttonsModel.get(i).action, []); + for (let i = projActionsModel.statefulActionsStartIndex + 1; i < projActionsModel.count; ++i) { + project.run(projActionsModel.get(i).action, []); } } @@ -644,7 +644,7 @@ ApplicationWindow { } mainOrInitScreen.currentIndex = 1; // go to main screen - initScreenLoader.sourceComponent = undefined; // destroy init screen + setupScreenLoader.sourceComponent = undefined; // destroy init screen } } } @@ -657,50 +657,52 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true - property var stateCachedNotifier: stateCached - onStateCachedNotifierChanged: { - if (stateCached['INIT_ERROR']) { - projActionsRow.visible = false; - initErrorMessage.visible = true; - } - } + // property var stateCachedNotifier: stateCached + // onStateCachedNotifierChanged: { + // if (stateCached['INIT_ERROR']) { + // projActionsRow.visible = false; + // initErrorMessage.visible = true; + // } + // } /* Show this or action buttons */ Text { id: initErrorMessage - visible: false + visible: stateCached['INIT_ERROR'] ? true : false // explicitly convert to boolean padding: 10 - text: "The project cannot be initialized" - color: 'red' + text: "The project cannot be initialized" + color: 'indianred' } /* The core widget - a group of buttons mapping all main actions that can be performed on the given project. They also serve the project state displaying - each button indicates a stage associated with it: - - green: done + - green (and green glow): done - yellow: in progress right now - - red: an error has occured during the last execution + - red glow: an error has occured during the last execution */ RowLayout { id: projActionsRow + visible: stateCached['INIT_ERROR'] ? false : true Layout.fillWidth: true Layout.bottomMargin: 7 z: 1 // for the glowing animation Repeater { model: ListModel { - id: buttonsModel + id: projActionsModel readonly property int statefulActionsStartIndex: 2 ListElement { name: 'Clean' action: 'clean' - tooltip: "WARNING: this will delete ALL content of the project folder except the current .ioc file and clear all logs" + tooltip: "WARNING: this will delete ALL content of the project folder \ + except the current .ioc file and clear all logs" } ListElement { name: 'Open editor' action: 'start_editor' - margin: 15 // margin to visually separate first 2 actions as they doesn't represent any stage + margin: 15 // margin to visually separate first 2 actions as they don't represent any stage } ListElement { name: 'Initialize' @@ -732,8 +734,8 @@ ApplicationWindow { id: actionButton text: model.name Layout.rightMargin: model.margin - property bool shouldBeHighlighted: false - property bool shouldBeHighlightedWhileRunning: false + property bool shouldBeHighlighted: false // highlight on mouse over + property bool shouldBeHighlightedWhileRunning: false // distinguish actions picked out for the batch run property int buttonIndex: -1 Component.onCompleted: { buttonIndex = index; @@ -750,7 +752,8 @@ ApplicationWindow { } } onClicked: { - const args = []; // JS array cannot be attached to a ListElement (at least in a non-hacky manner) + // JS array cannot be attached to a ListElement (at least in a non-hacky manner) so we fill arguments here + const args = []; switch (model.action) { case 'start_editor': args.push(settings.get('editor')); @@ -763,9 +766,14 @@ ApplicationWindow { } project.run(model.action, args); } + /* + As the button reflects relatively complex logic it's easier to maintain using the state machine technique. + We define states and allowed transitions between them, all other stuff is managed by the DSM framework. + You can find the graphical diagram somewhere in the docs + */ DSM.StateMachine { - initialState: main - running: true + initialState: main // start position + running: true // run immediately DSM.State { id: main initialState: normal @@ -776,7 +784,7 @@ ApplicationWindow { DSM.SignalTransition { targetState: highlighted signal: actionButton.shouldBeHighlightedChanged - guard: actionButton.shouldBeHighlighted + guard: actionButton.shouldBeHighlighted // go only if... } onEntered: { actionButton.enabled = true; @@ -798,7 +806,7 @@ ApplicationWindow { DSM.SignalTransition { targetState: normal signal: stateCachedChanged - guard: stateCached[model.stageRepresented] ? false : true // explicitly convert to boolean + guard: stateCached[model.stageRepresented] ? false : true } onEntered: { actionButton.palette.button = 'lightgreen'; @@ -834,9 +842,9 @@ ApplicationWindow { } } /* - Detect modifier keys: - - Ctrl (Cmd): start the editor after an operation(s) - - Shift: continuous actions run + Detect modifier keys using overlayed MouseArea: + - Ctrl (Cmd): start the editor after the action(s) + - Shift: batch actions run */ MouseArea { anchors.fill: parent @@ -846,21 +854,22 @@ ApplicationWindow { property bool shiftPressed: false property bool shiftPressedLastState: false function shiftHandler() { - for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + // manage the appearance of all [stateful] buttons prior this one + for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { projActionsRow.children[i].shouldBeHighlighted = shiftPressed; } } onClicked: { - if (shiftPressed && buttonIndex >= buttonsModel.statefulActionsStartIndex) { - for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + if (shiftPressed && buttonIndex >= projActionsModel.statefulActionsStartIndex) { + for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { projActionsRow.children[i].shouldBeHighlighted = false; projActionsRow.children[i].shouldBeHighlightedWhileRunning = true; } - for (let i = buttonsModel.statefulActionsStartIndex; i < buttonIndex; ++i) { - project.run(buttonsModel.get(i).action, []); + for (let i = projActionsModel.statefulActionsStartIndex; i < buttonIndex; ++i) { + project.run(projActionsModel.get(i).action, []); } } - parent.clicked(); + parent.clicked(); // pass the event to the underlying button though all work can be done in-place if (ctrlPressed && model.action !== 'start_editor') { project.run('start_editor', [settings.get('editor')]); } @@ -872,7 +881,7 @@ ApplicationWindow { } shiftPressed = mouse.modifiers & Qt.ShiftModifier; // bitwise AND - if (shiftPressedLastState !== shiftPressed) { // reduce number of unnecessary shiftHandler() calls + if (shiftPressedLastState !== shiftPressed) { // reduce a number of unnecessary shiftHandler() calls shiftPressedLastState = shiftPressed; shiftHandler(); } @@ -881,7 +890,7 @@ ApplicationWindow { if (model.action !== 'start_editor') { let preparedText = `Ctrl-click to open the editor specified in the Settings after the operation`; - if (buttonIndex >= buttonsModel.statefulActionsStartIndex) { + if (buttonIndex >= projActionsModel.statefulActionsStartIndex) { preparedText += `, Shift-click to perform all actions prior this one (including). Ctrl-Shift-click for both`; @@ -906,6 +915,7 @@ ApplicationWindow { target: project onActionStarted: { if (action === model.action) { + // Some properties like this are still managed outside of the DSM but this is, probably, OK palette.button = 'gold'; } glow.visible = false; @@ -924,18 +934,19 @@ ApplicationWindow { if (settings.get('notifications') && !mainWindow.active) { sysTrayIcon.showMessage( - success ? 'Success' : 'Error', - `${project.name} - ${model.name}`, - success ? Labs.SystemTrayIcon.Information : Labs.SystemTrayIcon.Warning, - 5000 + success ? 'Success' : 'Error', // title + `${project.name} - ${model.name}`, // text + success ? Labs.SystemTrayIcon.Information : Labs.SystemTrayIcon.Warning, // icon + 5000 // ms ); } + // Erase highlighting if this action is last in the series or at all if (shouldBeHighlightedWhileRunning && - (buttonIndex === (buttonsModel.count - 1) || + (buttonIndex === (projActionsModel.count - 1) || projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) ) { - for (let i = buttonsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; projActionsRow.children[i].background.border.width = 0; } @@ -953,9 +964,7 @@ ApplicationWindow { cornerRadius: 25 glowRadius: 20 spread: 0.25 - onVisibleChanged: { - visible ? glowAnimation.start() : glowAnimation.complete(); - } + onVisibleChanged: visible ? glowAnimation.start() : glowAnimation.complete() SequentialAnimation { id: glowAnimation loops: 3 @@ -991,6 +1000,18 @@ ApplicationWindow { wrapMode: Text.WordWrap font.pointSize: 10 // different on different platforms, Qt's bug textFormat: TextEdit.RichText + Connections { + target: project + onLogAdded: { + if (level === Logging.WARNING) { + log.append('
' + message + '
'); + } else if (level >= Logging.ERROR) { + log.append('
' + message + '
'); + } else { + log.append('
' + message + '
'); + } + } + } } } } @@ -1003,8 +1024,8 @@ ApplicationWindow { } /* - Simple text line. Currently, doesn't support smart intrinsic properties as a fully-fledged status bar, - but is used only for a single feature so not a big deal + Improvised status bar - simple text line. Currently, doesn't support smart intrinsic properties + as a fully-fledged status bar, but is used only for a single feature so not a big deal right now */ footer: Text { id: statusBar From 860bd22708c6eaf211dfbe75a25cb3466839547a Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 17 Apr 2020 01:52:00 +0300 Subject: [PATCH 17/20] spruce up GUI app.py --- TODO.md | 21 +-- stm32pio_gui/app.py | 288 +++++++++++++++++++++++++----------------- stm32pio_gui/main.qml | 128 +++++++++++-------- 3 files changed, 258 insertions(+), 179 deletions(-) diff --git a/TODO.md b/TODO.md index b61ebf0..0057858 100644 --- a/TODO.md +++ b/TODO.md @@ -6,19 +6,20 @@ - [ ] Create VSCode plugin ## GUI version + - [ ] Notify the user that the 'board' parameter is empty + - [ ] Mac: sometimes auto turned off shift highlighting after action (hide-restore helps) + - [x] in `ProjectListItem` set-up `currentAction` instead of `actionRunning` - [ ] Some visual flaws when the window have got resized (e.g. 'Add' button goes off-screen, 'Log' area crawls onto the status bar) - [x] Tray icon notifications - [x] When the list item is not active after the action the "current stage" line is not correct anymore. Consider updating or (better) gray out - [ ] Tests (research approaches and patterns) - [ ] Test performance with a large number of projects in the model. First test was made: - - 1. Some projects occasionally change `initLoading` by itself (probably Loader unloads the content) (hence cannot click on them, busy indicator appearing) - - Note: Delegates are instantiated as needed and may be destroyed at any time. They are parented to ListView's contentItem, not to the view itself. State should never be stored in a delegate. - - Or do not use ListView at all (replace be Repeater, for example) as it can reset our "notifications" - 2. Some projects show OK even after its deletion (only the app restart helps) - + 1. Some projects occasionally change `initLoading` by itself (probably Loader unloads the content) (hence cannot click on them, busy indicator appearing) + + Note: Delegates are instantiated as needed and may be destroyed at any time. They are parented to ListView's contentItem, not to the view itself. State should never be stored in a delegate. + + Use `id()` in `setInitInfo()`. Or do not use ListView at all (replace be Repeater, for example) as it can reset our "notifications" + 2. Some projects show OK even after its deletion (only the app restart helps) - [ ] Test with different timings - [x] Reduce number of calls to 'state' (many IO operations) - [x] Drag and drop the new folder into the app window @@ -44,7 +45,9 @@ - [x] Mark last error'ed action - [x] Action buttons widget state machine diagram - [x] Fix messed up performance when the list index changes! - - [x] Fix tooltip on Linux ('Add' works OK actually) + - [ ] Linux: + - Tooltip for 'Clean' not showing ('Add' works OK) + - Not a monospace font in the log area - [ ] Relative resource paths: ``` diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index 2d2c419..cfad898 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -10,23 +10,25 @@ import time import traceback import weakref -from typing import List +from typing import List, Callable, Optional, Dict, Any try: from PySide2.QtCore import QUrl, Property, QAbstractListModel, QModelIndex, QObject, Qt, Slot, Signal, QThread,\ qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, QThreadPool, QRunnable,\ QStringListModel, QSettings if platform.system() == 'Linux': - # Most UNIX systems does not provide QtDialogs implementation... + # Most UNIX systems does not provide QtDialogs implementation so the program should be 'linked' against + # the QApplication... from PySide2.QtWidgets import QApplication else: from PySide2.QtGui import QGuiApplication from PySide2.QtGui import QIcon from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine, QJSValue -except IndexError as e: +except ImportError as e: print(e) print("\nGUI version requires PySide2 to be installed. You can re-install stm32pio as 'pip install stm32pio[GUI]' " "or manually install its dependencies by yourself") + sys.exit(-1) try: import stm32pio.settings @@ -44,6 +46,7 @@ class BufferedLoggingHandler(logging.Handler): """ Simple logging.Handler subclass putting all incoming records into the given buffer """ + def __init__(self, buffer: collections.deque): super().__init__() self.buffer = buffer @@ -54,15 +57,15 @@ def emit(self, record: logging.LogRecord) -> None: class LoggingWorker(QObject): """ - QObject living in a separate QThread, logging everything it receiving. Intended to be an attached Stm32pio project - class property. Stringifies log records using DispatchingFormatter and passes them via Signal interface so they can - be conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can interpret - them differently. + QObject living in a separate QThread, logging everything it receiving. Intended to be an attached ProjectListItem + property. Stringifies log records using DispatchingFormatter and passes them via Signal interface so they can be + conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can interpret them + differently. Can be controlled by two threading.Event's: - stopped - on activation, leads to thread termination - can_flush_log - use this to temporarily save the logs in an internal buffer while waiting for some event to occurs - (for example GUI widgets to load), and then flush them when time has come + stopped - on activation, leads to thread termination + can_flush_log - use this to temporarily save the logs in an internal buffer while waiting for some event to + occurs (for example GUI widgets to load), and then flush them when the time has come """ sendLog = Signal(str, int) @@ -101,7 +104,7 @@ def routine(self) -> None: class ProjectListItem(QObject): """ - The core functionality class - GUI representation of the Stm32pio project + The core functionality class - wrapper around Stm32pio class suitable for the project GUI representation """ nameChanged = Signal() # properties notifiers @@ -111,11 +114,20 @@ class ProjectListItem(QObject): logAdded = Signal(str, int, arguments=['message', 'level']) # send the log message to the front-end actionStarted = Signal(str, arguments=['action']) - actionDone = Signal(str, bool, arguments=['action', 'success']) # emit when the action has executed - actionRunningChanged = Signal() + actionFinished = Signal(str, bool, arguments=['action', 'success']) + + def __init__(self, project_args: list = None, project_kwargs: dict = None, from_startup: bool = False, + parent: QObject = None): + """ + Args: + project_args: list of positional arguments that will be passed to the Stm32pio constructor + project_kwargs: dictionary of keyword arguments that will be passed to the Stm32pio constructor + from_startup: mark that this project comes from the beginning of the app life (e.g. from the NV-storage) so + it can be treated differently on the GUI side + parent: Qt parent + """ - def __init__(self, project_args: list = None, project_kwargs: dict = None, from_startup: bool = False, parent: QObject = None): super().__init__(parent=parent) if project_args is None: @@ -134,22 +146,21 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, from_ self.workers_pool = QThreadPool(parent=self) self.workers_pool.setMaxThreadCount(1) self.workers_pool.setExpiryTimeout(-1) # tasks forever wait for the available spot - self._is_action_running = False + self._current_action = '' # These values are valid till the Stm32pio project does not initialize itself (or failed to) self.project = None self._name = 'Loading...' - self._state = { 'LOADING': True } + self._state = { 'LOADING': True } # pseudo-stage (isn't present in ProjectStage enum) self._current_stage = 'Loading...' self.qml_ready = threading.Event() # the front and the back both should know when each other is initialized - self._finalizer = None # register some kind of the deconstruction handler + # Register some kind of the deconstruction handler (later, after the project initialization) + self._finalizer = None if 'instance_options' not in project_kwargs: - project_kwargs['instance_options'] = { - 'logger': self.logger - } + project_kwargs['instance_options'] = { 'logger': self.logger } elif 'logger' not in project_kwargs['instance_options']: project_kwargs['instance_options']['logger'] = self.logger @@ -169,7 +180,7 @@ def init_project(self, *args, **kwargs) -> None: """ try: self.project = stm32pio.lib.Stm32pio(*args, **kwargs) - except Exception as e: + except Exception: # Error during the initialization. Print format is: "ExceptionName: message" self.logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1], exc_info=self.logger.isEnabledFor(logging.DEBUG)) @@ -177,7 +188,7 @@ def init_project(self, *args, **kwargs) -> None: self._name = args[0] # use a project path string (as it should be a first argument) as a name else: self._name = 'Undefined' - self._state = { 'INIT_ERROR': True } + self._state = { 'INIT_ERROR': True } # pseudo-stage (isn't present in ProjectStage enum) self._current_stage = 'Initializing error' else: # Successful initialization. These values should not be used anymore but we "reset" them anyway @@ -188,31 +199,44 @@ def init_project(self, *args, **kwargs) -> None: # Register some kind of the deconstruction handler self._finalizer = weakref.finalize(self, self.at_exit, self.workers_pool, self.logging_worker, self.name if self.project is None else str(self.project)) - self.qml_ready.wait() # wait for the GUI to initialize + self.qml_ready.wait() # wait for the GUI to initialize (which one is earlier, actually, back or front) + self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending self.stageChanged.emit() self.stateChanged.emit() - self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending - @Property(bool) - def fromStartup(self): - return self._from_startup @staticmethod def at_exit(workers_pool: QThreadPool, logging_worker: LoggingWorker, name: str): + """ + Instance deconstruction handler meant to be used with weakref.finalize() conforming with the requirement to have + no reference to the target object (so it is decorated as 'staticmethod') + """ module_logger.info(f"destroy {name}") - workers_pool.waitForDone(msecs=-1) # wait for all jobs to complete. Currently, we cannot abort them gracefully + # Wait forever for all the jobs to complete. Currently, we cannot abort them gracefully + workers_pool.waitForDone(msecs=-1) logging_worker.stopped.set() # post the event in the logging worker to inform it... - logging_worker.thread.wait() # ...and wait for it to exit + logging_worker.thread.wait() # ...and wait for it to exit, too + + + @Property(bool) + def fromStartup(self) -> bool: + """Is this project is here from the beginning of the app life?""" + return self._from_startup @Property(str, notify=nameChanged) - def name(self): + def name(self) -> str: + """Human-readable name of the project. Will evaluate to the absolute path if it cannot be instantiated""" if self.project is not None: return self.project.path.name else: return self._name @Property('QVariant', notify=stateChanged) - def state(self): + def state(self) -> dict: + """ + Get the current project state in the appropriate Qt form. Update the cached 'current stage' value as a side + effect + """ if self.project is not None: state = self.project.state @@ -221,40 +245,51 @@ def state(self): # to necessarily keeps them separated self._current_stage = str(state.current_stage) - # Convert to normal dict (JavaScript object) and exclude UNDEFINED key - return { stage.name: value for stage, value in state.items() - if stage != stm32pio.lib.ProjectStage.UNDEFINED } - + state.pop(stm32pio.lib.ProjectStage.UNDEFINED) # exclude UNDEFINED key + # Convert to {string: boolean} dict (will be translated into the JavaScript object) + return { stage.name: value for stage, value in state.items() } else: return self._state @Property(str, notify=stageChanged) - def current_stage(self): + def currentStage(self) -> str: + """ + Get the current stage the project resides in. + Note: this returns a cached value. Cache updates every time the state property got requested + """ return self._current_stage - @Property(bool, notify=actionRunningChanged) - def actionRunning(self): - return self._is_action_running + @Property(str) + def currentAction(self) -> str: + """ + Stm32pio action (i.e. function name) that is currently executing or an empty string if there is none. It is set + on actionStarted signal and reset on actionFinished + """ + return self._current_action @Slot(str) def actionStartedSlot(self, action: str): + """Pass the corresponding signal from the worker, perform related tasks""" + # Currently, this property should be set BEFORE emitting the 'actionStarted' signal (because QML will query it + # when the signal will be handled in StateMachine) (probably, should be resolved later as it is bad to be bound + # to such a specific logic) + self._current_action = action self.actionStarted.emit(action) - self._is_action_running = True - self.actionRunningChanged.emit() @Slot(str, bool) - def actionDoneSlot(self, action: str, success: bool): + def actionFinishedSlot(self, action: str, success: bool): + """Pass the corresponding signal from the worker, perform related tasks""" if not success: self.workers_pool.clear() # clear the queue - stop further execution - self._is_action_running = False - self.actionRunningChanged.emit() - self.actionDone.emit(action, success) + self.actionFinished.emit(action, success) + # Currently, this property should be reset AFTER emitting the 'actionFinished' signal (because QML will query it + # when the signal will be handled in StateMachine) (probably, should be resolved later as it is bad to be bound + # to such a specific logic) + self._current_action = '' @Slot() def qmlLoaded(self): - """ - Event signaling the complete loading of needed frontend components. - """ + """Event signaling the complete loading of the needed frontend components""" self.qml_ready.set() self.logging_worker.can_flush_log.set() @@ -266,47 +301,53 @@ def run(self, action: str, args: list): Args: action: method name of the corresponding Stm32pio action - args: list of positional arguments for the action + args: list of positional arguments for this action """ - worker = ProjectActionWorker(getattr(self.project, action), args, self.logger) - worker.actionStarted.connect(self.actionStartedSlot) - worker.actionDone.connect(self.actionDoneSlot) - worker.actionDone.connect(self.stateChanged) - worker.actionDone.connect(self.stageChanged) + worker = Worker(getattr(self.project, action), args, self.logger) + worker.started.connect(self.actionStartedSlot) + worker.finished.connect(self.actionFinishedSlot) + worker.finished.connect(self.stateChanged) + worker.finished.connect(self.stageChanged) self.workers_pool.start(worker) # will automatically place to the queue -class ProjectActionWorker(QObject, QRunnable): +class Worker(QObject, QRunnable): """ - Generic worker for asynchronous processes. QObject + QRunnable combination. First allows to attach Qt signals, + Generic worker for asynchronous processes: QObject + QRunnable combination. First allows to attach Qt signals, second is compatible with QThreadPool. """ - actionStarted = Signal(str, arguments=['action']) - actionDone = Signal(str, bool, arguments=['action', 'success']) + started = Signal(str, arguments=['action']) + finished = Signal(str, bool, arguments=['action', 'success']) + - def __init__(self, func, args: list = None, logger: logging.Logger = None, parent: QObject = None): + def __init__(self, func: Callable[[list], Optional[int]], args: list = None, logger: logging.Logger = None, + parent: QObject = None): + """ + Args: + func: function to run. It should return 0 or None to call to be considered successful + args: the list of positional arguments. They will be unpacked and passed to the function + logger: optional logger to report about the occurred exception + parent: Qt object + """ QObject.__init__(self, parent=parent) QRunnable.__init__(self) - self.logger = logger self.func = func - if args is None: - self.args = [] - else: - self.args = args + self.args = args if args is not None else [] + self.logger = logger self.name = func.__name__ def run(self): - self.actionStarted.emit(self.name) # notify the caller + self.started.emit(self.name) # notify the caller try: result = self.func(*self.args) - except Exception as e: + except Exception: if self.logger is not None: # Print format is: "ExceptionName: message" self.logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1], @@ -318,7 +359,7 @@ def run(self): else: success = False - self.actionDone.emit(self.name, success) # notify the caller + self.finished.emit(self.name, success) # notify the caller if not success: # Pause the thread and, therefore, the parent QThreadPool queue so the caller can decide whether we should @@ -337,7 +378,7 @@ class ProjectsList(QAbstractListModel): duplicateFound = Signal(int, arguments=['duplicateIndex']) - def __init__(self, projects: list = None, parent: QObject = None): + def __init__(self, projects: List[ProjectListItem] = None, parent: QObject = None): """ Args: projects: initial list of projects @@ -347,7 +388,7 @@ def __init__(self, projects: list = None, parent: QObject = None): self.projects = projects if projects is not None else [] @Slot(int, result=ProjectListItem) - def getProject(self, index: int): + def get(self, index: int): """ Expose the ProjectListItem to the GUI QML side. You should firstly register the returning type using qmlRegisterType or similar. @@ -373,8 +414,8 @@ def addProject(self, project: ProjectListItem): @Slot('QStringList') def addProjectByPath(self, str_list: list): """ - Create, append and save in QSettings a new ProjectListItem instance with a given QUrl path (typically sent from - the QML GUI). + Create, append to the end and save in QSettings a new ProjectListItem instance with a given QUrl path (typically + is sent from the QML GUI). """ if len(str_list) > 1: @@ -385,21 +426,22 @@ def addProjectByPath(self, str_list: list): module_logger.warning("No path were given") return - paths_list: List[str] = [] - for path_str in str_list: - path_qurl = QUrl(path_str) - if path_qurl.isLocalFile(): - paths_list.append(path_qurl.toLocalFile()) - elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix - paths_list.append(path_str) # just use source string - - path = paths_list[0] + path_qurl = QUrl(str_list[0]) + if path_qurl.isLocalFile(): + path = path_qurl.toLocalFile() + elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix + path = str_list[0] # just use a source string + else: + module_logger.error(f"Incorrect path: {str_list[0]}") + return + # When we add a bunch of projects (or in the general case, too) recently added ones can be not instantiated yet + # so we need to check duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if list_item.project is not None and list_item.project.path.samefile(pathlib.Path(path))), -1) - if duplicate_index >= 0: + if duplicate_index > -1: module_logger.warning(f"This project is already in the list: {path}") - self.duplicateFound.emit(duplicate_index) + self.duplicateFound.emit(duplicate_index) # notify the GUI return self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) @@ -407,6 +449,8 @@ def addProjectByPath(self, str_list: list): project = ProjectListItem(project_args=[path], parent=self) self.projects.append(project) + self.endInsertRows() + settings.beginGroup('app') settings.beginWriteArray('projects') settings.setArrayIndex(len(self.projects) - 1) @@ -414,8 +458,6 @@ def addProjectByPath(self, str_list: list): settings.endArray() settings.endGroup() - self.endInsertRows() - @Slot(int) def removeProject(self, index: int): """ @@ -425,8 +467,11 @@ def removeProject(self, index: int): self.beginRemoveRows(QModelIndex(), index, index) project = self.projects.pop(index) + # It allows the project to be deconstructed (i.e. GC'ed) very soon, not at the app shutdown time project.deleteLater() + self.endRemoveRows() + settings.beginGroup('app') # Get current settings ... @@ -440,7 +485,7 @@ def removeProject(self, index: int): settings_projects_list.pop(index) # ... and overwrite the list. We don't use self.projects[i].project.path as there is a chance that 'path' - # doesn't exist (e.g. not initialized for some reason project) + # doesn't exist (e.g. not initialized for some reason project) but reuse the current values settings.remove('projects') settings.beginWriteArray('projects') for idx, path in enumerate(settings_projects_list): @@ -450,30 +495,13 @@ def removeProject(self, index: int): settings.endGroup() - self.endRemoveRows() - - - - -def qt_message_handler(mode, context, message): - if mode == QtInfoMsg: - mode = logging.INFO - elif mode == QtWarningMsg: - mode = logging.WARNING - elif mode == QtCriticalMsg: - mode = logging.ERROR - elif mode == QtFatalMsg: - mode = logging.CRITICAL - else: - mode = logging.DEBUG - qml_logger.log(mode, message) class Settings(QSettings): """ Extend the class by useful get/set methods allowing to avoid redundant code lines and also are callable from the - QML side. Also, retrieve settings on creation. + QML side """ DEFAULTS = { @@ -483,15 +511,29 @@ class Settings(QSettings): } def __init__(self, prefix: str, defaults: dict = None, qs_args: list = None, qs_kwargs: dict = None, - external_triggers: dict = None): + external_triggers: Dict[str, Callable[[str], Any]] = None): + """ + Args: + prefix: this prefix will always be added when get/set methods will be called so use it to group some most + needed preferences under a single name. For example, prefix='app/params' while the list of users is + located in 'app/users' + defaults: dictionary of fallback values (under the prefix mentioned above) that will be used if there is no + matching key in the storage + qs_args: positional arguments that will be passed to the QSettings constructor + qs_kwargs: keyword arguments that will be passed to the QSettings constructor + external_triggers: dictionary where the keys are parameters names (under the prefix) and the values are + functions that will be called with the corresponding parameter value as the argument when the parameter + is going to be set. Itis useful for a setup of the additional actions needed to be performed right after + a certain parameter gets an update + """ qs_args = qs_args if qs_args is not None else [] qs_kwargs = qs_kwargs if qs_kwargs is not None else {} - defaults = defaults if defaults is not None else self.DEFAULTS super().__init__(*qs_args, **qs_kwargs) self.prefix = prefix + defaults = defaults if defaults is not None else self.DEFAULTS self.external_triggers = external_triggers if external_triggers is not None else {} for key, value in defaults.items(): @@ -506,6 +548,7 @@ def clear(self): @Slot(str, result='QVariant') def get(self, key): value = self.value(self.prefix + key) + # Windows registry storage is case insensitive so 'False' is saved as 'false' and we need to handle this if value == 'false': value = False elif value == 'true': @@ -529,9 +572,26 @@ def main(): module_logger.setLevel(logging.INFO) module_logger.info('Starting stm32pio_gui...') + def qt_message_handler(mode, context, message): + """ + Register this logging handler for the Qt stuff if your plarform doesn't provide the built-in one or if you want to + customize it + """ + if mode == QtInfoMsg: + mode = logging.INFO + elif mode == QtWarningMsg: + mode = logging.WARNING + elif mode == QtCriticalMsg: + mode = logging.ERROR + elif mode == QtFatalMsg: + mode = logging.CRITICAL + else: + mode = logging.DEBUG + qml_logger.log(mode, message) + # Apparently Windows version of PySide2 doesn't have QML logging feature turn on so we fill this gap # TODO: set up for other platforms too (separate console.debug, console.warn, etc.) - global qml_logger + qml_logger = logging.getLogger('qml') if platform.system() == 'Windows': qml_log_handler = logging.StreamHandler() qml_log_handler.setFormatter(logging.Formatter("[QML] %(levelname)s %(message)s")) @@ -541,7 +601,6 @@ def main(): # Most Linux distros should be linked with the QWidgets' QApplication instead of the QGuiApplication to enable # QtDialogs - global app if platform.system() == 'Linux': app = QApplication(sys.argv) else: @@ -615,7 +674,7 @@ def verbose_setter(value): # Getting PlatformIO boards can take long time when the PlatformIO cache is outdated but it is important to have # them before the projects list restoring, so we start a dedicated loading thread. We actually can add other - # start-up operations here if there will be need to. Use the same ProjectActionWorker to spawn the thread at pool. + # start-up operations here if there will be need to. Use the same Worker to spawn the thread at pool. def loading(): nonlocal boards @@ -629,8 +688,8 @@ def loaded(_, success): projects_model.addProject(p) main_window.backendLoaded.emit() # inform the GUI - loader = ProjectActionWorker(loading, logger=module_logger) - loader.actionDone.connect(loaded) + loader = Worker(loading, logger=module_logger) + loader.finished.connect(loaded) QThreadPool.globalInstance().start(loader) @@ -639,12 +698,9 @@ def loaded(_, success): # Globals - -module_logger = logging.getLogger(__name__) # use it as a console logger for whatever you want to -qml_logger = logging.getLogger('qml') - -app = None -settings = QSettings() +module_logger = logging.getLogger(__name__) # use it as a console logger for whatever you want to, typically not + # related to the concrete project +settings = QSettings() # placeholder, will be replaced in main() diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 5f5c613..2f1359f 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -168,7 +168,7 @@ ApplicationWindow { if (initInfo[projectIndex] === 2) { delete initInfo[projectIndex]; // index can be reused - projectsModel.getProject(projectIndex).qmlLoaded(); + projectsModel.get(projectIndex).qmlLoaded(); } } @@ -273,7 +273,7 @@ ApplicationWindow { onLoaded: setInitInfo(index) sourceComponent: RowLayout { property bool initLoading: true // initial waiting for the backend-side TODO: do not store state in the delegate! - readonly property ProjectListItem project: projectsModel.getProject(index) + readonly property ProjectListItem project: projectsModel.get(index) Connections { target: project // Currently, this event is equivalent to the complete initialization of the backend side of the project @@ -294,17 +294,17 @@ ApplicationWindow { } } onActionStarted: { - runningOrDone.currentIndex = 0; - runningOrDone.visible = true; + runningOrFinished.currentIndex = 0; + runningOrFinished.visible = true; } - onActionDone: { + onActionFinished: { if (index !== projectsListView.currentIndex) { projectCurrentStage.color = 'darkgray'; // show that the stage has changed from the last visit - runningOrDone.currentIndex = 1; // show "notification" about the finished action - recentlyDoneIndicator.color = success ? 'lightgreen' : 'lightcoral'; - runningOrDone.visible = true; + runningOrFinished.currentIndex = 1; // show "notification" about the finished action + recentlyFinishedIndicator.color = success ? 'lightgreen' : 'lightcoral'; + runningOrFinished.visible = true; } else { - runningOrDone.visible = false; + runningOrFinished.visible = false; } } } @@ -319,7 +319,7 @@ ApplicationWindow { } if (Qt.colorEqual(projectCurrentStage.color, 'darkgray')) { projectCurrentStage.color = 'black'; - runningOrDone.visible = false; + runningOrFinished.visible = false; } } } @@ -330,9 +330,9 @@ ApplicationWindow { Text { id: projectName leftPadding: 5 - rightPadding: runningOrDone.visible ? 0 : leftPadding + rightPadding: runningOrFinished.visible ? 0 : leftPadding Layout.alignment: Qt.AlignBottom - Layout.preferredWidth: runningOrDone.visible ? + Layout.preferredWidth: runningOrFinished.visible ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width elide: Text.ElideMiddle @@ -342,21 +342,21 @@ ApplicationWindow { Text { id: projectCurrentStage leftPadding: 5 - rightPadding: runningOrDone.visible ? 0 : leftPadding + rightPadding: runningOrFinished.visible ? 0 : leftPadding Layout.alignment: Qt.AlignTop - Layout.preferredWidth: runningOrDone.visible ? + Layout.preferredWidth: runningOrFinished.visible ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width elide: Text.ElideRight maximumLineCount: 1 - text: display.current_stage + text: display.currentStage } } // Show whether a busy indicator or a finished action notification StackLayout { // TODO: probably can use DSM.StateMachine (or maybe regular State) for this, too - id: runningOrDone + id: runningOrFinished Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height Layout.preferredHeight: parent.height @@ -372,7 +372,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true Rectangle { // Circle :) - id: recentlyDoneIndicator + id: recentlyFinishedIndicator anchors.centerIn: parent width: 10 height: width @@ -471,7 +471,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true - readonly property ProjectListItem project: projectsModel.getProject(index) + readonly property ProjectListItem project: projectsModel.get(index) /* State retrieving procedure is relatively expensive (many IO operations) so we optimize it by getting the state @@ -490,7 +490,7 @@ ApplicationWindow { if (mainWindow.active && // the app got foreground projectIndex === projectsWorkspaceView.currentIndex && // only for the current list item !projectIncorrectDialog.visible && - !project.actionRunning + project.currentAction === '' ) { const state = project.state; stateCached = state; @@ -515,8 +515,8 @@ ApplicationWindow { target: project // Currently, this event is equivalent to the complete initialization of the backend side of the project onNameChanged: { - // const state = project.state; - const completedStages = Object.keys(stateCached).filter(stateName => stateCached[stateName]); + const state = project.state; + const completedStages = Object.keys(state).filter(stateName => state[stateName]); if (completedStages.length === 1 && completedStages[0] === 'EMPTY') { setupScreenLoader.active = true; mainOrInitScreen.currentIndex = 0; // show init dialog @@ -628,8 +628,8 @@ ApplicationWindow { } }]); if (board.editText === board.textAt(0)) { - project.logAdded('WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO \ - project creation. You can set it in "stm32pio.ini" file in the project directory', + project.logAdded('WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO ' + + 'project creation. You can set it in "stm32pio.ini" file in the project directory', Logging.WARNING); } @@ -731,7 +731,6 @@ ApplicationWindow { } } delegate: Button { - id: actionButton text: model.name Layout.rightMargin: model.margin property bool shouldBeHighlighted: false // highlight on mouse over @@ -783,12 +782,12 @@ ApplicationWindow { } DSM.SignalTransition { targetState: highlighted - signal: actionButton.shouldBeHighlightedChanged - guard: actionButton.shouldBeHighlighted // go only if... + signal: shouldBeHighlightedChanged + guard: shouldBeHighlighted // go only if... } onEntered: { - actionButton.enabled = true; - actionButton.palette.buttonText = 'black'; + enabled = true; + palette.buttonText = 'black'; } DSM.State { id: normal @@ -798,7 +797,7 @@ ApplicationWindow { guard: stateCached[model.stageRepresented] ? true : false // explicitly convert to boolean } onEntered: { - actionButton.palette.button = 'lightgray'; + palette.button = 'lightgray'; } } DSM.State { @@ -809,7 +808,7 @@ ApplicationWindow { guard: stateCached[model.stageRepresented] ? false : true } onEntered: { - actionButton.palette.button = 'lightgreen'; + palette.button = 'lightgreen'; } } DSM.HistoryState { @@ -818,25 +817,46 @@ ApplicationWindow { } } DSM.State { + // Activates/deactivates additional properties (such as color or border) on some conditions + // (e.g. some action is currently running), see onEntered, onExited id: disabled DSM.SignalTransition { targetState: mainHistory - signal: project.actionDone + signal: project.actionFinished } onEntered: { - actionButton.enabled = false; - actionButton.palette.buttonText = 'darkgray'; + enabled = false; + palette.buttonText = 'darkgray'; + if (project.currentAction === model.action) { + palette.button = 'gold'; + } + if (shouldBeHighlightedWhileRunning) { + background.border.width = 2; + } + } + onExited: { + // Erase highlighting if this action is last in the series or at all + if (project.currentAction === model.action && + shouldBeHighlightedWhileRunning && + (buttonIndex === (projActionsModel.count - 1) || + projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) + ) { + for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; + projActionsRow.children[i].background.border.width = 0; + } + } } } DSM.State { id: highlighted DSM.SignalTransition { targetState: mainHistory - signal: actionButton.shouldBeHighlightedChanged - guard: !actionButton.shouldBeHighlighted + signal: shouldBeHighlightedChanged + guard: !shouldBeHighlighted } onEntered: { - actionButton.palette.button = Qt.lighter('lightgreen', 1.2); + palette.button = Qt.lighter('lightgreen', 1.2); palette.buttonText = 'dimgray'; } } @@ -914,16 +934,16 @@ ApplicationWindow { Connections { target: project onActionStarted: { - if (action === model.action) { - // Some properties like this are still managed outside of the DSM but this is, probably, OK - palette.button = 'gold'; - } + // if (action === model.action) { + // // Some properties like this are still managed outside of the DSM but this is, probably, OK + // palette.button = 'gold'; + // } glow.visible = false; - if (shouldBeHighlightedWhileRunning) { - background.border.width = 2; - } + // if (shouldBeHighlightedWhileRunning) { + // background.border.width = 2; + // } } - onActionDone: { + onActionFinished: { if (action === model.action) { if (success) { glow.color = 'lightgreen'; @@ -941,16 +961,16 @@ ApplicationWindow { ); } - // Erase highlighting if this action is last in the series or at all - if (shouldBeHighlightedWhileRunning && - (buttonIndex === (projActionsModel.count - 1) || - projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) - ) { - for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { - projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; - projActionsRow.children[i].background.border.width = 0; - } - } + // // Erase highlighting if this action is last in the series or at all + // if (shouldBeHighlightedWhileRunning && + // (buttonIndex === (projActionsModel.count - 1) || + // projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) + // ) { + // for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { + // projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; + // projActionsRow.children[i].background.border.width = 0; + // } + // } } } } From 52f9596c8d901587a9b722f3530a552139ec645a Mon Sep 17 00:00:00 2001 From: usserr Date: Fri, 17 Apr 2020 14:37:26 +0300 Subject: [PATCH 18/20] test on Win7, fix some bugs --- TODO.md | 3 ++- stm32pio/lib.py | 3 +-- stm32pio_gui/app.py | 1 + stm32pio_gui/main.qml | 15 +++++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/TODO.md b/TODO.md index 0057858..cd64666 100644 --- a/TODO.md +++ b/TODO.md @@ -37,7 +37,8 @@ - [ ] `TypeError: Cannot read property 'actionRunning' of null (deconstruction order)` (on project deletion only) - [ ] QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - [x] Fix high CPU usage (most likely some thread consuming) - - [ ] Bug in log box scrolling behavior (autoscroll sometimes turns off, should re-enable when starting any action) + - [ ] Lost log box autoscroll when manually scrolling between the actions + - [ ] Crash on shutdown in Win and Linux (errors such as `[QML] CRITICAL QThread: Destroyed while thread is still running Process finished with exit code 1073741845`) - [x] Fix loader when action running - [ ] Start with a folder opened if it was provided on CLI (for example, `stm32pio_gui .`) - [x] Mark list item when action is done and it is not a current item (i.e. notify a user) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 0caaa55..bef900a 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -207,7 +207,6 @@ def state(self) -> ProjectState: Constructing and returning the current state of the project (tweaked dict, see ProjectState docs) """ - print('wants state') # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") pio_is_initialized = False @@ -583,7 +582,7 @@ def start_editor(self, editor_command: str) -> int: sanitized_input = shlex.quote(editor_command) - self.logger.info(f"starting an editor {sanitized_input}...") + self.logger.info(f'starting an editor "{sanitized_input}"...') try: # Works unstable on some Windows 7 systems, but correct on Win10... # result = subprocess.run([editor_command, str(self.path)], check=True) diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index cfad898..e1fc9c3 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -237,6 +237,7 @@ def state(self) -> dict: Get the current project state in the appropriate Qt form. Update the cached 'current stage' value as a side effect """ + module_logger.info(f"{self.name} {time.time()}") if self.project is not None: state = self.project.state diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 2f1359f..5e52b39 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -157,6 +157,8 @@ ApplicationWindow { QML-side implementation details to the backend we define this helper function that counts and stores a number of widgets currently loaded for each project in model and informs the Qt-side right after all necessary components become ready. + + TODO: should be remade to use Python id() as a unique identifier, see TODO.md */ readonly property var initInfo: ({}) function setInitInfo(projectIndex) { @@ -449,18 +451,18 @@ ApplicationWindow { Connections { target: projectsListView - onCurrentIndexChanged: projectsWorkspaceView.currentIndex = projectsListView.currentIndex + onCurrentIndexChanged: { + // console.log('currentIndex', projectsListView.currentIndex, projectsWorkspaceView.currentIndex); + projectsWorkspaceView.currentIndex = projectsListView.currentIndex; + } } Repeater { // Use similar to ListView pattern (same projects model, Loader component) model: projectsModel delegate: Component { Loader { - property int projectIndex: -1 - onLoaded: { - projectIndex = index; - setInitInfo(projectIndex) - } + property int projectIndex: index // binding so will be automatically updated on change + onLoaded: setInitInfo(index) /* Use another one StackLayout to separate Project initialization "screen" and Main one */ @@ -1019,6 +1021,7 @@ ApplicationWindow { selectByMouse: true wrapMode: Text.WordWrap font.pointSize: 10 // different on different platforms, Qt's bug + font.weight: Font.DemiBold textFormat: TextEdit.RichText Connections { target: project From 0aac9852f2b6c64ca3d8599b0b74c9213725a3a4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 18 Apr 2020 18:34:21 +0300 Subject: [PATCH 19/20] preparing the v1.20 release --- MANIFEST.in | 1 - TODO.md | 24 ++++---- stm32pio/app.py | 2 +- stm32pio/lib.py | 12 ++-- stm32pio_gui/README.md | 54 ++++++++++++++++-- .../docs/action_button_state_machine.drawio | 1 + .../docs/action_button_state_machine.png | Bin 0 -> 159390 bytes stm32pio_gui/main.qml | 54 ++++-------------- stm32pio_gui/screenshots/group.png | Bin 0 -> 6996 bytes stm32pio_gui/screenshots/highlighting.png | Bin 0 -> 29732 bytes stm32pio_gui/screenshots/init_screen.png | Bin 0 -> 8762 bytes stm32pio_gui/screenshots/main.png | Bin 0 -> 68379 bytes 12 files changed, 80 insertions(+), 68 deletions(-) create mode 100644 stm32pio_gui/docs/action_button_state_machine.drawio create mode 100644 stm32pio_gui/docs/action_button_state_machine.png create mode 100644 stm32pio_gui/screenshots/group.png create mode 100644 stm32pio_gui/screenshots/highlighting.png create mode 100644 stm32pio_gui/screenshots/init_screen.png create mode 100644 stm32pio_gui/screenshots/main.png diff --git a/MANIFEST.in b/MANIFEST.in index c43da9a..c0434a4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,6 @@ include LICENSE include MANIFEST.in include README.md include TODO.md -recursive-include screenshots * recursive-include stm32pio-test-project * include stm32pio-gui/main.qml include stm32pio-gui/README.md diff --git a/TODO.md b/TODO.md index cd64666..6278cd1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,24 +1,27 @@ # TODOs ## Business logic, business features + - [ ] Issues guide for the GitHub (OS, content of the config, project tree, enable verbose) + - [ ] GitHub CHANGELOG - separate New, Fixed, Changed into paragraphs - [ ] Middleware support (FreeRTOS, etc.) - [ ] Arduino framework support (needs research to check if it is possible) - [ ] Create VSCode plugin ## GUI version + - [ ] Can probably detect Ctrl and Shift clicks without moving the mouse first - [ ] Notify the user that the 'board' parameter is empty - [ ] Mac: sometimes auto turned off shift highlighting after action (hide-restore helps) - - [x] in `ProjectListItem` set-up `currentAction` instead of `actionRunning` - - [ ] Some visual flaws when the window have got resized (e.g. 'Add' button goes off-screen, 'Log' area crawls onto the status bar) + - [x] In `ProjectListItem` set-up `currentAction` instead of `actionRunning` + - [ ] Some visual flaws when the window have got resized (e.g. 'Add' button position doesn't change until the list gets focus, 'Log' area crawls onto the status bar) - [x] Tray icon notifications - - [x] When the list item is not active after the action the "current stage" line is not correct anymore. Consider updating or (better) gray out + - [ ] Gray out "stage" line in all projects except current - [ ] Tests (research approaches and patterns) - [ ] Test performance with a large number of projects in the model. First test was made: 1. Some projects occasionally change `initLoading` by itself (probably Loader unloads the content) (hence cannot click on them, busy indicator appearing) - + Note: Delegates are instantiated as needed and may be destroyed at any time. They are parented to ListView's contentItem, not to the view itself. State should never be stored in a delegate. - - Use `id()` in `setInitInfo()`. Or do not use ListView at all (replace be Repeater, for example) as it can reset our "notifications" + + Use `id()` in `setInitInfo()`. Or do not use ListView at all (replace by Repeater, for example) as it can reset our "notifications" 2. Some projects show OK even after its deletion (only the app restart helps) - [ ] Test with different timings - [x] Reduce number of calls to 'state' (many IO operations) @@ -26,7 +29,7 @@ - [x] Multiple projects addition - [ ] Divide on multiple modules (both Python and QML) - [ ] Implement other methods for Qt abstract models - - [x] Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) + - [ ] Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) - [x] On 'Clean' clean the log too - [x] Stop the chain of commands if someone drops -1 or an exception - [ ] 2 types of logging formatters for 2 verbosity levels @@ -34,7 +37,7 @@ - [x] Maybe use QML State for action buttons appearance - [x] Projects are not destructed until quit (something preserving the link probably...) - [x] Fix settings (window doesn't match real) - - [ ] `TypeError: Cannot read property 'actionRunning' of null (deconstruction order)` (on project deletion only) + - [ ] `TypeError: Cannot read property 'actionRunning' of null` (deconstruction order) (on project deletion only) - [ ] QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - [x] Fix high CPU usage (most likely some thread consuming) - [ ] Lost log box autoscroll when manually scrolling between the actions @@ -47,12 +50,11 @@ - [x] Action buttons widget state machine diagram - [x] Fix messed up performance when the list index changes! - [ ] Linux: - - Tooltip for 'Clean' not showing ('Add' works OK) - Not a monospace font in the log area - [ ] Relative resource paths: - + ``` - ⌘ python3 Documents/GitHub/stm32pio/stm32pio_gui/app.py + ⌘ python3 Documents/GitHub/stm32pio/stm32pio_gui/app.py INFO main Starting stm32pio_gui... qt.svg: Cannot open file '/Users/chufyrev/stm32pio_gui/icons/icon.svg', because: No such file or directory qt.svg: Cannot open file '/Users/chufyrev/stm32pio_gui/icons/icon.svg', because: No such file or directory diff --git a/stm32pio/app.py b/stm32pio/app.py index 584ea19..55b1fe8 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__version__ = '1.10' +__version__ = '1.20' import argparse import logging diff --git a/stm32pio/lib.py b/stm32pio/lib.py index bef900a..8ca62ae 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -165,7 +165,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, instance_options: d else: self.logger = logging.getLogger(f"{__name__}.{id(self)}") # use id() as uniqueness guarantee - # The path is a primary identifier of the project so we process it first and foremost. Handle 'path/to/proj', + # The path is a primary entity of the project so we process it first and foremost. Handle 'path/to/proj', # 'path/to/proj/', '.', '../proj', etc., make the path absolute and check for existence. Also, the .ioc file can # be specified instead of the directory. In this case it is assumed that the parent path is an actual project # path and the provided .ioc file is used on a priority basis @@ -246,8 +246,8 @@ def state(self) -> ProjectState: def _find_ioc_file(self, explicit_file: pathlib.Path = None) -> pathlib.Path: """ - Find and return an .ioc file. If there are more than one return first. If no .ioc file is present raise - FileNotFoundError exception + Find, check (that this is a non-empty text file) and return an .ioc file. If there are more than one return + first. If no .ioc file is present raise FileNotFoundError exception. Use explicit_file if it was provided Returns: absolute path to the .ioc file @@ -356,9 +356,9 @@ def save_config(self, parameters: dict = None) -> int: Invokes base _save_config function. Preliminarily, updates the config with the given 'parameters' dictionary. It should has the following format: { - 'section1_name': { - 'key1': 'value1', - 'key2': 'value2' + 'project': { + 'board': 'nucleo_f031k6', + 'ioc_file': 'fan_controller.ioc' }, ... } diff --git a/stm32pio_gui/README.md b/stm32pio_gui/README.md index d8a1ac4..12311e1 100644 --- a/stm32pio_gui/README.md +++ b/stm32pio_gui/README.md @@ -1,15 +1,59 @@ -# stm32pio-gui +# stm32pio GUI -The cross-platform GUI version of the application. It wraps the core library functionality in the Qt-QML skin using PySide2 (aka "Qt for Python" project) adding projects management feature so you can store and manipulate multiple stm32pio projects in one place. +![Main](screenshots/main.png) -Currently, it is in a beta stage though all implemented features work, with more or less (mostly visual and architectural) flaws. +The cross-platform GUI version of the stm32pio. It wraps the core library functionality into the Qt-QML skin using the PySide2 (aka "Qt for Python" project) and adding the projects management feature allowing you to store and manipulate multiple stm32pio projects at one place. ## Installation -The app requires PySide2 5.12+ package (Qt 5.12 respectively). It is available in all major package managers including pip, apt, brew and so on. More convenient installation process is coming in next releases. +The app requires PySide2 5.12+ package (Qt 5.12 respectively). It is available in all major package managers including pip, apt, brew and so on. + +The convenient way to install is via `pip` specifying `extras` option: +```shell script +$ pip install stm32pio[GUI] +``` + +Then it can be started as +```shell script +$ stm32pio_gui +``` +from anywhere. If you have already installed the latest basic CLI version this script and sources are already on your machine so you can reinstall using the command above or to supplement the setup installing the PySide2 manually. + +If you rather want to launch completely from the sources, currently it's possible only from the repository root point: +```shell script +stm32pio-repo/ $ python stm32pio_gui/app.py +``` ## Usage -Enter `python3 app.py` to start the app. Projects list (not the projects themself) and settings are stored by QSettings so refer to its docs if you bother about the actual location. +Add a folder with the `.ioc` file to begin with. You can also drag-and-drop it to the main window, in this case you can add multiple projects simultaneously. If the project is empty the initialization screen will be shown to help in setup: + +![Init](screenshots/init_screen.png) + +You can skip it or enter one of the available PlatformIO STM32 boards. Select "Run" to apply all actions to the project (analog of the `new` CLI command). + +In the main screen the buttons row allows you to run specific actions while represents the state of the project at the same time. Green color means that this stage is fulfilled. The active project is refreshing automatically while all the others only when you click on them so the "stage" line at the projects list item can be outdated. + +Let's assume you've worked on the project for some time and need to re-generate and rebuild the configuration. To schedule all the necessary actions to run one after another navigate to the last desired action pressing the Shift key. All the projects prior this one should be colored light-green now: + +![Highlighting](screenshots/highlighting.png) + +Shift-click on it to execute the series. The picked actions will be framed with border around each of them: + +![Group](screenshots/group.png) + +Add Ctrl to the mouse click to start the editor specified in the settings after the action. It can be combined with Shift. **Hint:** specify a `start` as an "Editor" command to open the folder in the new Explorer window under the Windows, `open` for the Finder on the macOS. + + +## Architecture notes + +Projects list (not the projects themself) and settings are stored by `QSettings` so refer to its docs if you bother about the actual location. + +See `docs` directory to see state machine diagram of the project action button. + + +## Known issues + +The number of added projects that can be correctly represented is currently limited to about 5 due to some architectural mistakes. It's planned to be fixed in the near future. diff --git a/stm32pio_gui/docs/action_button_state_machine.drawio b/stm32pio_gui/docs/action_button_state_machine.drawio new file mode 100644 index 0000000..1010a13 --- /dev/null +++ b/stm32pio_gui/docs/action_button_state_machine.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/stm32pio_gui/docs/action_button_state_machine.png b/stm32pio_gui/docs/action_button_state_machine.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e53eebf5ff30f1f3c0f7dde2cd51cdb9fd5fdd GIT binary patch literal 159390 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 BN-zKb literal 0 HcmV?d00001 diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 5e52b39..3197b91 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -451,10 +451,7 @@ ApplicationWindow { Connections { target: projectsListView - onCurrentIndexChanged: { - // console.log('currentIndex', projectsListView.currentIndex, projectsWorkspaceView.currentIndex); - projectsWorkspaceView.currentIndex = projectsListView.currentIndex; - } + onCurrentIndexChanged: projectsWorkspaceView.currentIndex = projectsListView.currentIndex } Repeater { // Use similar to ListView pattern (same projects model, Loader component) @@ -491,19 +488,16 @@ ApplicationWindow { onHandleState: { if (mainWindow.active && // the app got foreground projectIndex === projectsWorkspaceView.currentIndex && // only for the current list item - !projectIncorrectDialog.visible && + !projectIncorrectDialog.visible && // on macOS, there is an animation effect so this property isn't updated + // immediately and the state can be retrieved several times and some flaws + // may appear. Workaround - is to have a dedicated flag and update it + // manually but this isn't very elegant solution project.currentAction === '' ) { const state = project.state; stateCached = state; project.stageChanged(); // side-effect: update the stage at the same time - - // if (!state['INIT_ERROR'] && !state['EMPTY']) { // i.e. no .ioc file but the project was able to initialize itself - // // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - // projectIncorrectDialogIsOpen = true; - // projectIncorrectDialog.open(); - // } } } Component.onCompleted: { @@ -535,10 +529,7 @@ ApplicationWindow { text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
The project will be removed from the app. It will not affect any real content` icon: Dialogs.StandardIcon.Critical - onAccepted: { - removeCurrentProject(); - // mainOrInitScreen.projectIncorrectDialogIsOpen = false; - } + onAccepted: removeCurrentProject() } /* @@ -583,7 +574,7 @@ ApplicationWindow { text: 'Run' enabled: false ToolTip { - visible: runCheckBox.hovered + visible: runCheckBox.hovered // not working on Linux (Manjaro LXQt) Component.onCompleted: { // Form the tool tip text using action names const actions = []; @@ -612,7 +603,7 @@ ApplicationWindow { text: 'Open editor' ToolTip { text: "Start the editor specified in the Settings after the completion" - visible: openEditor.hovered + visible: openEditor.hovered // not working on Linux (Manjaro LXQt) } } } @@ -659,14 +650,6 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true - // property var stateCachedNotifier: stateCached - // onStateCachedNotifierChanged: { - // if (stateCached['INIT_ERROR']) { - // projActionsRow.visible = false; - // initErrorMessage.visible = true; - // } - // } - /* Show this or action buttons */ @@ -743,7 +726,7 @@ ApplicationWindow { background.border.color = 'dimgray'; } ToolTip { - visible: parent.hovered + visible: mouseArea.containsMouse Component.onCompleted: { if (model.tooltip) { text = model.tooltip; @@ -869,6 +852,7 @@ ApplicationWindow { - Shift: batch actions run */ MouseArea { + id: mouseArea anchors.fill: parent hoverEnabled: true property bool ctrlPressed: false @@ -936,14 +920,7 @@ ApplicationWindow { Connections { target: project onActionStarted: { - // if (action === model.action) { - // // Some properties like this are still managed outside of the DSM but this is, probably, OK - // palette.button = 'gold'; - // } glow.visible = false; - // if (shouldBeHighlightedWhileRunning) { - // background.border.width = 2; - // } } onActionFinished: { if (action === model.action) { @@ -962,17 +939,6 @@ ApplicationWindow { 5000 // ms ); } - - // // Erase highlighting if this action is last in the series or at all - // if (shouldBeHighlightedWhileRunning && - // (buttonIndex === (projActionsModel.count - 1) || - // projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false) - // ) { - // for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) { - // projActionsRow.children[i].shouldBeHighlightedWhileRunning = false; - // projActionsRow.children[i].background.border.width = 0; - // } - // } } } } diff --git a/stm32pio_gui/screenshots/group.png b/stm32pio_gui/screenshots/group.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e09c8bcb1528bf50ceb7f4fa07689de67abf5a GIT binary patch literal 6996 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 z%0DdKccu2Kamo#w%5K;RTAgffT~(I}9yMazpw{;(WWT{cXN4qnmXvSIyPWMk&(j^- zTI*jlt$)u>d+|zd0x&l`=S^+9TSb?im4wHVO)Q=u@%z2DHw*=OaSQl_WvX~GW>WC~ zB^BZX{l!ocYT>%4x6Ir54Ca}Nu;JQX1_SYW+LY8=&Kg=&WlSyhXT+1qoYMi99vt8E zgNP$2=TbAhI|C#Rg!Y(w`!%sNbCq$ju4hhV2!WBcyJt0UWWk?b^JdANHIibwz9c3{ zm5<2Wwg6{KGzN>i&$e75LFgY1D`@pZE29e~eVZabr* zph)wMqG#w8SX*l8P|P3yirNDKt|4diRXjb;IwGiA%=7|T*;EB@o^&FBdHBj~1g(5- zy_%-Om-2m@bTq)>ti{P?{LSn~RPj4Cr{7MWA_HfgS3?7dYB)~OR<<3o!HV?`avlT1`3O)yYJ-7@Tq zmnCYv{qqllV$`qg&!Q%R&=kbah$O$$bR1q>#62UFl(hF*`|2cMV^9s8px?q^4tomDZ3 zqBVBdn0vNmtE`>^-^YQn=pem%1waZ6zSq_Qb> zV#V{*}?g;?y-gLhp`heYjlg4p+0u0VQKSlel^B)aKP6-z{?95Z$fCqTfEG9?8G~= zi$sawPEyfPnuf!($V&pn%ts_v>VSF)nxKIw2Eh;&(sv=xiOfVHBV-V5(SzOc?G)sX zRY>8C=M<-|yS_F(jkyU*4nyt<>zhkA+?2BS+4%#gP#u5T2+BApd3v}QByuu3)EwuT zdj97547GdN7U$}-a5G2e+oHm77gU8aFZkwP3^xAmfa zqnEGxw5ZjSqfmaousTX5b}`q+o1xJ;p(UqRs;X-LK(r@&(N@`3So#uPgimc{92oW) zF6gR-0?jU~5;L8L=uK3y@n1V% zcFL?N-`(wQ7PWiTaFkTLAXCZCOS6y9VT^Hci%QNPU)%E8zaZYi`S!crmjT;*tyL3! zNQ$(rt?s#HY#TQ%D#hD_;X7W$dPDK`BWlkH^oE;vFPh{kuUkG^TJ~jAPzGSrd&KJB zOS}fh6AxhE&ogkdJ=x4kl$>Hv1nW;4h|BNJH3kYP&{X@!CK!J$2NyXUTzMEKoh^iCs7fNL2Ux? zN+*5pXHW3Q?=8yT4R>~qxDRPvPaQ6tMgzws5)rH>=$^9OGx9%(Cr5cC%2ey;W6;sz z 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 bhbR0^>X4t6Fme6A2f<4Pb@>W8i_rf7)(9$> literal 0 HcmV?d00001 diff --git a/stm32pio_gui/screenshots/main.png b/stm32pio_gui/screenshots/main.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebcd101cb8a51fa35f2e2e64c8e2508373a3daa GIT binary patch literal 68379 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%0DL9*V&w<) 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 Date: Sun, 19 Apr 2020 00:12:06 +0300 Subject: [PATCH 20/20] v1.20 release preparation :wrench: --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ MANIFEST.in | 8 ++++---- README.md | 2 ++ TODO.md | 21 +++------------------ stm32pio_gui/README.md | 11 +++++++++-- stm32pio_gui/app.py | 2 -- stm32pio_gui/main.qml | 4 ++-- 7 files changed, 53 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fdc70..af4fced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,3 +195,36 @@ - Changed: separate `Stm32pio` arguments onto 2 categories: project parameters and instance options and use dictionaries for them. First one has now the same form as the project config `configparser.ConfigParser` and merging into the default and file settings on the project creation. Instance options are more related to the programmatic instance itself and contains currently 2 options - `logger` and `save_on_destruction` - Changed: use `append()` instead of `insert()` to modify `sys.path` - Changed: when raising the exceptions use more elegant expressions (e.g. `raise FileNotFoundError(file)` instead of `raise FileNotFoundError("file FILE was not found")`). Use `pathlib.Path().resolve(strict=True)` where appropriate to shorten the code + +## ver. 1.20 (18.04.20) + - New: GUI. System tray notifications when the main window is not in the foreground (can be turned off in the settings) + - New: GUI. Drag-and-drop folder(s) to add + - New: GUI. README, screenshots, diagrams + - New: GUI. Catch projects duplication on appending + - New: GUI. Mark the list item when the action is done and it is not a current item + - New: GUI. Highlight the actions that were picked for the series + - New: GUI. setuptools `extras` option to install the GUI version via pip + - New: GUI. Wrap imports into `try...catch` + - New: GUI. Reset settings feature + - New: GUI. New `ProjectListItem` members: `_from_startup` flag, `_current_action` string with corresponding properties and signals + - New: GUI. More extensive use of the `typing` annotations + - New: GUI. Allow to pass extra setter functions to the `Settings` which will be called on the value change + - New: allow to specify the `.ioc` file instead of the directory. Check that `.ioc` is a non-empty text file + - Fixed: GUI. Projects are not destructed until the app shutdown + - Fixed: GUI. Settings dialog doesn't correctly represents the parameters + - Fixed: GUI. Settings on Windows (case-sensitive vs insensitive situation) + - Fixed: GUI. List item loader + - Fixed: GUI. Flaws when index changes + - Changed: GUI. Clean the logs too when invoking the 'Clean' action + - Changed: GUI. Stop the chain of commands if someone drops -1 or an exception + - Changed: GUI. Use Qt StateMachine to control the visual appearance of the project action button + - Changed: GUI. Rename the module `stm32pio-gui` -> `stm32pio_gui` + - Changed: GUI. Better printing of exception messages + - Changed: GUI. Revised finalizer (similar to core version) + - Changed: GUI. Cache `state` and `state.current_stage` both for back- and frontend to reduce IO operations + - Changed: GUI. rename `ProjectActionWorker` -> `Worker` (as it is used for the variety of tasks) and some of its internals + - Changed: GUI. Pass `Settings` prefix as an argument for the constructor + - Changed: GUI. Move more stuff inside the `main()` function, less global variables + - Changed: exclude screenshots from the setuptools bundle + - Changed: restructure TODO.md into sections + - Changed: remove `from __future__ import annotations` statements diff --git a/MANIFEST.in b/MANIFEST.in index c0434a4..2274f3e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,10 @@ include .gitignore -include CHANGELOG +include CHANGELOG.md include LICENSE include MANIFEST.in include README.md include TODO.md recursive-include stm32pio-test-project * -include stm32pio-gui/main.qml -include stm32pio-gui/README.md -recursive-include stm32pio-gui/icons * +include stm32pio_gui/main.qml +include stm32pio_gui/README.md +recursive-include stm32pio_gui/icons * diff --git a/README.md b/README.md index cc50033..2caa647 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Small cross-platform Python app that can create and update [PlatformIO](https:// It uses STM32CubeMX to generate a HAL-framework-based code and alongside creates PlatformIO project with compatible parameters to stick them both together. +The [GUI version](/stm32pio_gui) is available, too. + ![Logo](/screenshots/logo.png) diff --git a/TODO.md b/TODO.md index 6278cd1..26797ab 100644 --- a/TODO.md +++ b/TODO.md @@ -8,12 +8,12 @@ - [ ] Create VSCode plugin ## GUI version + - [ ] Handle the initialization error (when boards are receiving) + - [ ] Maybe `data()` `QAbstractListModel` method can be used instead of custom `get()` - [ ] Can probably detect Ctrl and Shift clicks without moving the mouse first - [ ] Notify the user that the 'board' parameter is empty - [ ] Mac: sometimes auto turned off shift highlighting after action (hide-restore helps) - - [x] In `ProjectListItem` set-up `currentAction` instead of `actionRunning` - [ ] Some visual flaws when the window have got resized (e.g. 'Add' button position doesn't change until the list gets focus, 'Log' area crawls onto the status bar) - - [x] Tray icon notifications - [ ] Gray out "stage" line in all projects except current - [ ] Tests (research approaches and patterns) - [ ] Test performance with a large number of projects in the model. First test was made: @@ -24,31 +24,15 @@ Use `id()` in `setInitInfo()`. Or do not use ListView at all (replace by Repeater, for example) as it can reset our "notifications" 2. Some projects show OK even after its deletion (only the app restart helps) - [ ] Test with different timings - - [x] Reduce number of calls to 'state' (many IO operations) - - [x] Drag and drop the new folder into the app window - - [x] Multiple projects addition - [ ] Divide on multiple modules (both Python and QML) - [ ] Implement other methods for Qt abstract models - [ ] Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter)) - - [x] On 'Clean' clean the log too - - [x] Stop the chain of commands if someone drops -1 or an exception - [ ] 2 types of logging formatters for 2 verbosity levels - - [x] Check for projects duplication - - [x] Maybe use QML State for action buttons appearance - - [x] Projects are not destructed until quit (something preserving the link probably...) - - [x] Fix settings (window doesn't match real) - [ ] `TypeError: Cannot read property 'actionRunning' of null` (deconstruction order) (on project deletion only) - [ ] QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on - - [x] Fix high CPU usage (most likely some thread consuming) - [ ] Lost log box autoscroll when manually scrolling between the actions - [ ] Crash on shutdown in Win and Linux (errors such as `[QML] CRITICAL QThread: Destroyed while thread is still running Process finished with exit code 1073741845`) - - [x] Fix loader when action running - [ ] Start with a folder opened if it was provided on CLI (for example, `stm32pio_gui .`) - - [x] Mark list item when action is done and it is not a current item (i.e. notify a user) - - [x] Highlight actions that were picked for continuous run (with some border, for example) - - [x] Mark last error'ed action - - [x] Action buttons widget state machine diagram - - [x] Fix messed up performance when the list index changes! - [ ] Linux: - Not a monospace font in the log area - [ ] Relative resource paths: @@ -93,3 +77,4 @@ - [ ] check if `platformio.ini` config will be successfully parsed when there are interpolation and/or empty parameters - [x] check if `.ioc` file is a text file on project initialization. Let `_find_ioc_file()` method to use explicitly provided file (useful for GUI). Maybe let user specify it via CLI - [ ] mb add CLI command for starting the GUI version (for example, `stm32pio --gui`) + - [ ] test using virtualenv diff --git a/stm32pio_gui/README.md b/stm32pio_gui/README.md index 12311e1..dc2a15e 100644 --- a/stm32pio_gui/README.md +++ b/stm32pio_gui/README.md @@ -24,17 +24,22 @@ If you rather want to launch completely from the sources, currently it's possibl ```shell script stm32pio-repo/ $ python stm32pio_gui/app.py ``` +or +```shell script +stm32pio-repo/ $ python -m stm32pio_gui +``` + ## Usage -Add a folder with the `.ioc` file to begin with. You can also drag-and-drop it to the main window, in this case you can add multiple projects simultaneously. If the project is empty the initialization screen will be shown to help in setup: +Add a folder with the `.ioc` file to begin with. You can also drag-and-drop it into the main window, in this case you can add multiple projects simultaneously. If the project is empty the initialization screen will be shown to help in setup: ![Init](screenshots/init_screen.png) You can skip it or enter one of the available PlatformIO STM32 boards. Select "Run" to apply all actions to the project (analog of the `new` CLI command). -In the main screen the buttons row allows you to run specific actions while represents the state of the project at the same time. Green color means that this stage is fulfilled. The active project is refreshing automatically while all the others only when you click on them so the "stage" line at the projects list item can be outdated. +In the main screen the buttons row allows you to run specific actions while represents the state of the project at the same time. Green color means that this stage is fulfilled. The active project is monitored automatically while all the others refresh only when you click on them so the "stage" line at the projects list item can be outdated. Let's assume you've worked on the project for some time and need to re-generate and rebuild the configuration. To schedule all the necessary actions to run one after another navigate to the last desired action pressing the Shift key. All the projects prior this one should be colored light-green now: @@ -57,3 +62,5 @@ See `docs` directory to see state machine diagram of the project action button. ## Known issues The number of added projects that can be correctly represented is currently limited to about 5 due to some architectural mistakes. It's planned to be fixed in the near future. + +Right after the removing of the project from the list there are several errors on the terminal appears. It is most likely caused by the non proper destruction order of components and isn't something to be worried about. By a similar reasons the app itself sometimes crushes during the shutdown process (doesn't observed on the macOS, though). diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py index e1fc9c3..54868f0 100644 --- a/stm32pio_gui/app.py +++ b/stm32pio_gui/app.py @@ -237,7 +237,6 @@ def state(self) -> dict: Get the current project state in the appropriate Qt form. Update the cached 'current stage' value as a side effect """ - module_logger.info(f"{self.name} {time.time()}") if self.project is not None: state = self.project.state @@ -682,7 +681,6 @@ def loading(): boards = ['None'] + stm32pio.util.get_platformio_boards('platformio') def loaded(_, success): - # TODO: somehow handle an initialization error boards_model.setStringList(boards) projects = [ProjectListItem(project_args=[path], from_startup=True, parent=projects_model) for path in projects_paths] for p in projects: diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml index 3197b91..3cacbde 100644 --- a/stm32pio_gui/main.qml +++ b/stm32pio_gui/main.qml @@ -82,7 +82,7 @@ ApplicationWindow { Layout.preferredWidth: 250 // Detected recursive rearrange. Aborting after two iterations wrapMode: Text.Wrap color: 'dimgray' - text: "Get messages about completed project actions when the app is in background" + text: "Get messages about completed project actions when the app is in the background" } Text { @@ -542,7 +542,7 @@ ApplicationWindow { active: false sourceComponent: Column { Text { - text: "To complete initialization you can provide PlatformIO name of the board" + text: "To complete initialization you can provide the PlatformIO name of the board" padding: 10 } Row {