diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 4bbee280..b78ffb69 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,6 +1,8 @@ import sys -from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) +from typing import Optional +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import (QApplication, QLabel, QMainWindow, QToolBar, QDockWidget, QAction) from logbook import Logger import cadquery as cq @@ -65,6 +67,8 @@ def __init__(self,parent=None, filename=None): self.restoreComponentState() + self.on_idle() + def closeEvent(self,event): self.saveWindow() @@ -202,7 +206,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) @@ -213,18 +217,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) @@ -239,6 +250,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) @@ -344,6 +357,27 @@ 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.components['debugger'].set_rendering_state(False) + self.set_status_message('Idle', '#000000') - pass + @pyqtSlot() + def on_render_start(self): + self.components['debugger'].set_rendering_state(True) + 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 70d5795f..ddef98ad 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -111,6 +111,7 @@ class Debugger(QObject,ComponentMixin): ]) + sigRenderStarted = pyqtSignal() sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) sigTraceback = pyqtSignal(object,str) @@ -263,6 +264,7 @@ def _cleanup_locals(self,module,injected_names): @pyqtSlot(bool) def render(self): + self.sigRenderStarted.emit() seed(59798267586177) if self.preferences['Reload CQ']: @@ -294,6 +296,11 @@ def render(self): 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()] diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 1778bfd5..de2ffb2b 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -97,7 +97,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) @@ -201,6 +201,7 @@ def addLines(self): origin = (0,0,0) ais_list = [] + names = [] for name,color,direction in zip(('X','Y','Z'), ('red','lawngreen','blue'), @@ -214,8 +215,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): @@ -248,6 +250,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)} @@ -266,13 +269,14 @@ 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=None): @@ -281,7 +285,7 @@ def addObject(self,obj,name='',options=None): root = self.CQ - ais,shape_display = make_AIS(obj, options) + ais, shape_display = make_AIS(obj, options) root.addChild(ObjectTreeItem(name, shape=obj, @@ -289,7 +293,7 @@ def addObject(self,obj,name='',options=None): ais=ais, sig=self.sigObjectPropertiesChanged)) - self.sigObjectsAdded.emit([ais]) + self.sigObjectsAdded.emit([ais], [name]) @pyqtSlot(list) @pyqtSlot() @@ -313,7 +317,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 7d1051a0..aa7cd9a8 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -36,8 +36,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 9c5d620b..498f8438 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -1,5 +1,6 @@ from PyQt5.QtWidgets import QWidget, QDialog, QTreeWidgetItem, QApplication, QAction +from typing import Optional, List from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon @@ -47,6 +48,7 @@ class OCCViewer(QWidget,ComponentMixin): IMAGE_EXTENSIONS = 'png' sigObjectSelected = pyqtSignal(list) + sigDisplayProgress = pyqtSignal(int, int, str) def __init__(self,parent=None): @@ -179,31 +181,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() @@ -223,7 +217,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): diff --git a/tests/test_app.py b/tests/test_app.py index 2ec4baa7..a5ad8490 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,9 @@ from path import Path import os, sys, asyncio +from pytestqt.qtbot import QtBot +import pytestqt.exceptions + if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -91,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) @@ -124,7 +129,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 +147,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, timeout=TIMEOUT): + win.show() editor = win.components['editor'] editor.set_text(code) @@ -158,15 +162,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, timeout=TIMEOUT): + win.show() editor = win.components['editor'] editor.set_text(code) @@ -174,16 +177,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, timeout=TIMEOUT): + win.show() editor = win.components['editor'] editor.set_text(code_multi) @@ -571,7 +573,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() @@ -663,8 +665,6 @@ def test_editor_autoreload(monkeypatch,editor): qtbot, editor = editor - TIMEOUT = 500 - # start out with autoreload enabled editor.autoreload(True) @@ -713,7 +713,6 @@ def test_autoreload_nested(editor): qtbot, editor = editor - TIMEOUT = 500 editor.autoreload(True) editor.preferences['Autoreload: watch imported modules'] = True @@ -747,9 +746,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): @@ -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"]