From 853c0f4391c4cd185b3ee7178a1303b11eda3ef6 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sun, 21 Feb 2021 13:16:13 +0000 Subject: [PATCH 1/5] status bar messages to indicate rendering and viewing progress --- cq_editor/main_window.py | 47 +++++++++++++++++++++++---- cq_editor/widgets/debugger.py | 2 ++ cq_editor/widgets/object_tree.py | 20 +++++++----- cq_editor/widgets/traceback_viewer.py | 3 +- cq_editor/widgets/viewer.py | 37 +++++++++------------ 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index d66890ec..259e7574 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,6 +1,9 @@ import sys -from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) +from typing import Optional +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction, QApplication) import cadquery as cq @@ -55,6 +58,8 @@ def __init__(self,parent=None): self.restoreWindow() self.restoreComponentState() + self.on_idle() + def closeEvent(self,event): self.saveWindow() @@ -192,7 +197,7 @@ def prepare_toolbar(self): self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar') for c in self.components.values(): - add_actions(self.toolbar,c.toolbarActions()) + add_actions(self.toolbar, c.toolbarActions()) self.addToolBar(self.toolbar) @@ -203,18 +208,25 @@ def prepare_statusbar(self): def prepare_actions(self): + self.components['debugger'].sigRenderStarted \ + .connect(self.on_render_start) self.components['debugger'].sigRendered\ .connect(self.components['object_tree'].addObjects) self.components['debugger'].sigTraceback\ .connect(self.components['traceback_viewer'].addTraceback) + self.components['debugger'].sigRendered \ + .connect(lambda _: self.on_idle()) + self.components['debugger'].sigTraceback \ + .connect(lambda _: self.on_idle()) + self.components['debugger'].sigLocals\ .connect(self.components['variables_viewer'].update_frame) self.components['debugger'].sigLocals\ .connect(self.components['console'].push_vars) - self.components['object_tree'].sigObjectsAdded[list]\ - .connect(self.components['viewer'].display_many) - self.components['object_tree'].sigObjectsAdded[list,bool]\ + self.components['object_tree'].sigObjectsAdded[list, list]\ + .connect(lambda objects, names: self.components['viewer'].display_many(objects, None, names)) + self.components['object_tree'].sigObjectsAdded[list, bool, list]\ .connect(self.components['viewer'].display_many) self.components['object_tree'].sigItemChanged.\ connect(self.components['viewer'].update_item) @@ -229,6 +241,8 @@ def prepare_actions(self): self.components['viewer'].sigObjectSelected\ .connect(self.components['object_tree'].handleGraphicalSelection) + self.components['viewer'].sigDisplayProgress \ + .connect(self.on_display_progress) self.components['traceback_viewer'].sigHighlightLine\ .connect(self.components['editor'].go_to_line) @@ -332,6 +346,25 @@ def handle_filename_change(self, fname): new_title = fname if fname else "*" self.setWindowTitle(f"{self.name}: {new_title}") -if __name__ == "__main__": + def on_idle(self): + self.set_status_message('Idle', '#000000') - pass + @pyqtSlot() + def on_render_start(self): + self.set_status_message('Rendering...', '#ff0000') + + @pyqtSlot(int, int, str) + def on_display_progress(self, current: int, total: int, name: Optional[str]): + if current == total: + self.on_idle() + else: + message = f'Displaying Shape {current + 1} / {total}' + if name: + message += f' ({name})' + self.set_status_message(message, '#0000ff') + + def set_status_message(self, message: str, color: str): + self.statusBar().showMessage(message) + self.statusBar().setStyleSheet(f'color: {color}') + # required because rendering is currently done on the main thread + QApplication.processEvents() diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index b6810d0f..1a1d570d 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -110,6 +110,7 @@ class Debugger(QObject,ComponentMixin): {'name': 'Change working dir to script dir','type': 'bool', 'value': True}]) + sigRenderStarted = pyqtSignal() sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) sigTraceback = pyqtSignal(object,str) @@ -221,6 +222,7 @@ def _cleanup_locals(self,module,injected_names): @pyqtSlot(bool) def render(self): + self.sigRenderStarted.emit() if self.preferences['Reload CQ']: reload_cq() diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index c0ea1dcf..3775b5da 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -91,7 +91,7 @@ class ObjectTree(QWidget,ComponentMixin): {'name': 'Clear all before each run', 'type': 'bool', 'value': True}, {'name': 'STL precision','type': 'float', 'value': .1}]) - sigObjectsAdded = pyqtSignal([list],[list,bool]) + sigObjectsAdded = pyqtSignal([list, list],[list, bool, list]) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigAISObjectsSelected = pyqtSignal(list) @@ -195,6 +195,7 @@ def addLines(self): origin = (0,0,0) ais_list = [] + names = [] for name,color,direction in zip(('X','Y','Z'), ('red','lawngreen','blue'), @@ -208,8 +209,9 @@ def addLines(self): ais=line)) ais_list.append(line) + names.append(name) - self.sigObjectsAdded.emit(ais_list) + self.sigObjectsAdded.emit(ais_list, names) def _current_properties(self): @@ -242,6 +244,7 @@ def addObjects(self,objects,clean=False,root=None): self.removeObjects() ais_list = [] + names = [] #remove empty objects objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)} @@ -260,20 +263,21 @@ def addObjects(self,objects,clean=False,root=None): if child.properties['Visible']: ais_list.append(ais) - + names.append(name) + root.addChild(child) if request_fit_view: - self.sigObjectsAdded[list,bool].emit(ais_list,True) + self.sigObjectsAdded[list, bool, list].emit(ais_list, True, names) else: - self.sigObjectsAdded[list].emit(ais_list) + self.sigObjectsAdded[list, list].emit(ais_list, names) @pyqtSlot(object,str,object) def addObject(self,obj,name='',options={}): root = self.CQ - ais,shape_display = make_AIS(obj, options) + ais, shape_display = make_AIS(obj, options) root.addChild(ObjectTreeItem(name, shape=obj, @@ -281,7 +285,7 @@ def addObject(self,obj,name='',options={}): ais=ais, sig=self.sigObjectPropertiesChanged)) - self.sigObjectsAdded.emit([ais]) + self.sigObjectsAdded.emit([ais], name) @pyqtSlot(list) @pyqtSlot() @@ -305,7 +309,7 @@ def stashObjects(self,action : bool): self.removeObjects() self.CQ.addChildren(self._stash) ais_list = [el.ais for el in self._stash] - self.sigObjectsAdded.emit(ais_list) + self.sigObjectsAdded.emit(ais_list, [''] * len(ais_list)) @pyqtSlot() def removeSelected(self): diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index d5e0baa6..875429b1 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -35,8 +35,7 @@ def __init__(self,parent): self.tree = TracebackTree(self) self.current_exception = QLabel(self) - self.current_exception.setStyleSheet(\ - "QLabel {color : red; }"); + self.current_exception.setStyleSheet("QLabel {color : red; }"); layout(self, (self.current_exception, diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index a3d4c361..c58416f8 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from typing import List, Optional from PyQt5.QtWidgets import (QWidget, QPushButton, QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QFileDialog, QHBoxLayout, QFrame, QLabel, QApplication, @@ -45,6 +46,7 @@ class OCCViewer(QWidget,ComponentMixin): IMAGE_EXTENSIONS = 'png' sigObjectSelected = pyqtSignal(list) + sigDisplayProgress = pyqtSignal(int, int, str) def __init__(self,parent=None): @@ -140,31 +142,23 @@ def clear(self): context.PurgeDisplay() context.RemoveAll(True) - def _display(self,shape): - - ais = make_AIS(shape) - self.canvas.context.Display(shape,True) - - self.displayed_shapes.append(shape) - self.displayed_ais.append(ais) - - #self.canvas._display.Repaint() - @pyqtSlot(object) - def display(self,ais): - - context = self._get_context() - context.Display(ais,True) - - if self.preferences['Fit automatically']: self.fit() + def display(self, ais): + self.display_many([ais]) @pyqtSlot(list) - @pyqtSlot(list,bool) - def display_many(self,ais_list,fit=None): + @pyqtSlot(list, bool, list) + def display_many(self, ais_list, fit: Optional[bool] = None, names: Optional[List] = None): + if names is None: + names = [None] * len(ais_list) + assert len(ais_list) == len(names) context = self._get_context() - for ais in ais_list: - context.Display(ais,True) + num_objects = len(ais_list) + for i, (ais, name) in enumerate(zip(ais_list, names)): + self.sigDisplayProgress.emit(i, num_objects, name) + context.Display(ais, True) + self.sigDisplayProgress.emit(num_objects, num_objects, None) if self.preferences['Fit automatically'] and fit is None: self.fit() @@ -184,7 +178,8 @@ def update_item(self,item,col): def remove_items(self,ais_items): ctx = self._get_context() - for ais in ais_items: ctx.Erase(ais,True) + for ais in ais_items: + ctx.Erase(ais,True) @pyqtSlot() def redraw(self): From 7c16e99f1bc3501cb629f435eaf4010477349563 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Fri, 28 May 2021 22:48:43 +0100 Subject: [PATCH 2/5] set render button checked during rendering --- cq_editor/main_window.py | 2 ++ cq_editor/widgets/debugger.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 259e7574..9292abc8 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -347,10 +347,12 @@ def handle_filename_change(self, fname): self.setWindowTitle(f"{self.name}: {new_title}") def on_idle(self): + self.components['debugger'].set_rendering_state(False) self.set_status_message('Idle', '#000000') @pyqtSlot() def on_render_start(self): + self.components['debugger'].set_rendering_state(True) self.set_status_message('Rendering...', '#ff0000') @pyqtSlot(int, int, str) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 1a1d570d..4712f909 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -251,7 +251,12 @@ def render(self): exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] self.sigTraceback.emit(exc_info, cq_script) - + + def set_rendering_state(self, rendering): + render_action = self._actions['Run'][0] + render_action.setCheckable(rendering) + render_action.setChecked(rendering) + @property def breakpoints(self): return [ el[0] for el in self.get_breakpoints()] From 43caa13dc43dde861b8ea779dc97b15f4bd1e4d8 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Fri, 31 Jan 2025 23:52:08 +0000 Subject: [PATCH 3/5] fix failing tests --- cq_editor/widgets/object_tree.py | 2 +- tests/test_app.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 0884e9e6..de2ffb2b 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -293,7 +293,7 @@ def addObject(self,obj,name='',options=None): ais=ais, sig=self.sigObjectPropertiesChanged)) - self.sigObjectsAdded.emit([ais], name) + self.sigObjectsAdded.emit([ais], [name]) @pyqtSlot(list) @pyqtSlot() diff --git a/tests/test_app.py b/tests/test_app.py index 2ec4baa7..d2007d1a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -747,9 +747,10 @@ def test_console(main): assert(len(a) == 1) # test print_text - pos_orig = console._prompt_pos - console.print_text('a') - assert(console._prompt_pos == pos_orig + len('a')) + text_before = console._control.document().toPlainText() + console.print_text('foo') + text_after = console._control.document().toPlainText() + assert text_after == text_before + 'foo' def test_viewer(main): From 91caa79de28bb155bed0e70e4a0fe81229e59d34 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sat, 1 Feb 2025 21:48:08 +0000 Subject: [PATCH 4/5] resolve deprecation warning --- tests/test_app.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index d2007d1a..71507cac 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,8 @@ from path import Path import os, sys, asyncio +from pytestqt.qtbot import QtBot + if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -124,7 +126,7 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha @pytest.fixture -def main(qtbot,mocker): +def main(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) @@ -142,15 +144,14 @@ def main(qtbot,mocker): return qtbot, win @pytest.fixture -def main_clean(qtbot,mocker): +def main_clean(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) win = MainWindow() - win.show() - qtbot.addWidget(win) - qtbot.waitForWindowShown(win) + with qtbot.waitExposed(win): + win.show() editor = win.components['editor'] editor.set_text(code) @@ -158,15 +159,14 @@ def main_clean(qtbot,mocker): return qtbot, win @pytest.fixture -def main_clean_do_not_close(qtbot,mocker): +def main_clean_do_not_close(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) win = MainWindow() - win.show() - qtbot.addWidget(win) - qtbot.waitForWindowShown(win) + with qtbot.waitExposed(win): + win.show() editor = win.components['editor'] editor.set_text(code) @@ -174,16 +174,15 @@ def main_clean_do_not_close(qtbot,mocker): return qtbot, win @pytest.fixture -def main_multi(qtbot,mocker): +def main_multi(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) win = MainWindow() - win.show() - qtbot.addWidget(win) - qtbot.waitForWindowShown(win) + with qtbot.waitExposed(win): + win.show() editor = win.components['editor'] editor.set_text(code_multi) @@ -571,7 +570,7 @@ def test_traceback(main): assert(traceback_view.tree.root.childCount() == 3) # 1 in user code + 2 in CQ code @pytest.fixture -def editor(qtbot): +def editor(qtbot: QtBot): win = Editor() win.show() From 8454b5e6475fba3974e3cce4e6223daccfd75c18 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sat, 1 Feb 2025 21:50:13 +0000 Subject: [PATCH 5/5] increase timeout --- tests/test_app.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 71507cac..a5ad8490 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,6 +2,7 @@ import os, sys, asyncio from pytestqt.qtbot import QtBot +import pytestqt.exceptions if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -93,6 +94,8 @@ sk = cq.Sketch().rect(1,1) """ +TIMEOUT = 10_000 + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -150,7 +153,7 @@ def main_clean(qtbot: QtBot, mocker): win = MainWindow() qtbot.addWidget(win) - with qtbot.waitExposed(win): + with qtbot.waitExposed(win, timeout=TIMEOUT): win.show() editor = win.components['editor'] @@ -165,7 +168,7 @@ def main_clean_do_not_close(qtbot: QtBot, mocker): win = MainWindow() qtbot.addWidget(win) - with qtbot.waitExposed(win): + with qtbot.waitExposed(win, timeout=TIMEOUT): win.show() editor = win.components['editor'] @@ -181,7 +184,7 @@ def main_multi(qtbot: QtBot, mocker): win = MainWindow() qtbot.addWidget(win) - with qtbot.waitExposed(win): + with qtbot.waitExposed(win, timeout=TIMEOUT): win.show() editor = win.components['editor'] @@ -662,8 +665,6 @@ def test_editor_autoreload(monkeypatch,editor): qtbot, editor = editor - TIMEOUT = 500 - # start out with autoreload enabled editor.autoreload(True) @@ -712,7 +713,6 @@ def test_autoreload_nested(editor): qtbot, editor = editor - TIMEOUT = 500 editor.autoreload(True) editor.preferences['Autoreload: watch imported modules'] = True @@ -1440,7 +1440,6 @@ def makebox(z): def test_reload_import_handle_error(tmp_path, main): - TIMEOUT = 500 qtbot, win = main editor = win.components["editor"] debugger = win.components["debugger"] @@ -1481,7 +1480,6 @@ def test_reload_import_handle_error(tmp_path, main): def test_modulefinder(tmp_path, main): - TIMEOUT = 500 qtbot, win = main editor = win.components["editor"] debugger = win.components["debugger"]