From edf99266ef1766a0ce759353256c2198f383cac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Thu, 23 May 2024 00:01:25 +0200 Subject: [PATCH] ref: migrate from QTableView to QTableWidget (issues with Windows 11) --- CHANGELOG | 2 + chipstream/gui/main_window.py | 32 ++++--- chipstream/gui/main_window.ui | 41 +++++++-- chipstream/gui/table_progress.py | 149 ++++++++++++++----------------- tests/test_gui.py | 8 +- 5 files changed, 124 insertions(+), 108 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3719a03..1bdb659 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +0.3.1 + - ref: migrate from QTableView to QTableWidget (issues with Windows 11) 0.3.0 - BREAKING CHANGE: major changes in dcnum postprocessing - feat: allow to select whether volume should be computed diff --git a/chipstream/gui/main_window.py b/chipstream/gui/main_window.py index 006a1d5..26ff3cd 100644 --- a/chipstream/gui/main_window.py +++ b/chipstream/gui/main_window.py @@ -16,6 +16,7 @@ from ..path_cache import PathCache from .._version import version +from .manager import ChipStreamJobManager from . import splash @@ -29,13 +30,15 @@ def __init__(self, *arguments): application will print the version after initialization and exit. """ + self.job_manager = ChipStreamJobManager() QtWidgets.QMainWindow.__init__(self) ref_ui = resources.files("chipstream.gui") / "main_window.ui" with resources.as_file(ref_ui) as path_ui: uic.loadUi(path_ui, self) + self.tableWidget_input.set_job_manager(self.job_manager) + self.logger = logging.getLogger(__name__) - self.manager = self.tableView_input.model.manager # Populate segmenter combobox self.comboBox_segmenter.blockSignals(True) @@ -92,7 +95,7 @@ def __init__(self, *arguments): # Signals self.run_completed.connect(self.on_run_completed) - self.tableView_input.row_selected.connect(self.on_select_job) + self.tableWidget_input.row_selected.connect(self.on_select_job) # if "--version" was specified, print the version and exit if "--version" in arguments: @@ -103,7 +106,7 @@ def __init__(self, *arguments): # Create a timer that continuously updates self.textBrowser self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.tableView_input.on_selection_changed) + self.timer.timeout.connect(self.tableWidget_input.on_selection_changed) self.timer.start(1000) splash.splash_close() @@ -115,9 +118,10 @@ def __init__(self, *arguments): def append_paths(self, path_list): """Add input paths to the table""" - if not self.manager.is_busy(): + if not self.job_manager.is_busy(): for pp in path_list: - self.tableView_input.add_input_path(pp) + self.job_manager.add_path(pp) + self.tableWidget_input.update_from_job_manager() @QtCore.pyqtSlot(QtCore.QEvent) def dragEnterEvent(self, e): @@ -187,7 +191,7 @@ def get_job_kwargs(self): return job_kwargs def is_running(self): - return self.manager.is_alive() + return self.job_manager.is_busy() @QtCore.pyqtSlot() def on_action_about(self) -> None: @@ -220,7 +224,8 @@ def on_action_add(self): @QtCore.pyqtSlot() def on_action_clear(self): """Clear the current table view""" - self.manager.clear() + self.job_manager.clear() + self.tableWidget_input.update_from_job_manager() @QtCore.pyqtSlot() def on_action_docs(self): @@ -258,7 +263,7 @@ def on_path_out(self): data = self.comboBox_output.currentData() if data == "input": # Store output data alongside input data - self.manager.set_output_path(None) + self.job_manager.set_output_path(None) elif data == "new": # New output path default = "." if len(self.path_cache) == 0 else self.path_cache[-1] @@ -277,16 +282,15 @@ def on_path_out(self): ) self.comboBox_output.setCurrentIndex(len(self.path_cache) + 1) self.path_cache.add_path(pathlib.Path(path)) - self.manager.set_output_path(path) + self.job_manager.set_output_path(path) else: # User pressed cancel self.comboBox_output.setCurrentIndex(0) - self.manager.set_output_path(None) + self.job_manager.set_output_path(None) self.comboBox_output.blockSignals(False) else: # Data is an integer index for `self.path_cache` - self.manager.set_output_path(self.path_cache[data]) - print(self.manager._path_out) + self.job_manager.set_output_path(self.path_cache[data]) @QtCore.pyqtSlot() def on_run(self): @@ -295,7 +299,7 @@ def on_run(self): # finished. The user can still add items to the list but not # change the pipeline. self.widget_options.setEnabled(False) - self.manager.run_all_in_thread( + self.job_manager.run_all_in_thread( job_kwargs=self.get_job_kwargs(), callback_when_done=self.run_completed.emit) @@ -309,7 +313,7 @@ def on_select_job(self, row): info = "No job selected." else: # Display some information in the lower text box. - info = self.manager.get_info(row) + info = self.job_manager.get_info(row) # Compare the text to the current text. old_text = self.textBrowser.toPlainText() if info != old_text: diff --git a/chipstream/gui/main_window.ui b/chipstream/gui/main_window.ui index 35f18cc..a336c8f 100644 --- a/chipstream/gui/main_window.ui +++ b/chipstream/gui/main_window.ui @@ -41,15 +41,18 @@ Qt::Vertical - + 1 3 + + QAbstractItemView::NoEditTriggers + - QAbstractItemView::InternalMove + QAbstractItemView::NoDragDrop true @@ -60,6 +63,12 @@ QAbstractItemView::SelectRows + + Qt::ElideMiddle + + + QAbstractItemView::ScrollPerPixel + true @@ -67,14 +76,32 @@ false - 50 + 100 - - true + + 100 - + false + + true + + + + Path + + + + + State + + + + + Progress + + @@ -428,7 +455,7 @@ ProgressTable - QTableView + QTableWidget
chipstream.gui.table_progress
diff --git a/chipstream/gui/table_progress.py b/chipstream/gui/table_progress.py index 00c7846..1104c86 100644 --- a/chipstream/gui/table_progress.py +++ b/chipstream/gui/table_progress.py @@ -1,100 +1,83 @@ from PyQt6 import QtCore, QtWidgets -from .manager import ChipStreamJobManager - -ItemProgressRole = QtCore.Qt.ItemDataRole.UserRole + 1001 - - -class ProgressDelegate(QtWidgets.QStyledItemDelegate): - def paint(self, painter, option, index): - progress = index.data(ItemProgressRole) - opt = QtWidgets.QStyleOptionProgressBar() - opt.rect = option.rect - opt.minimum = 0 - opt.maximum = 100 - opt.progress = int(progress * 100) - opt.text = f"{progress:.1%}" - opt.textVisible = True - QtWidgets.QApplication.style().drawControl( - QtWidgets.QStyle.ControlElement.CE_ProgressBar, opt, painter) - - -class ProgressModel(QtCore.QAbstractTableModel): - def __init__(self, *args, **kwargs): - super(ProgressModel, self).__init__(*args, **kwargs) - self.manager = ChipStreamJobManager() - self.map_columns = ["path", "state", "progress"] - self.headers = [m.capitalize() for m in self.map_columns] - self.monitor_timer = QtCore.QTimer(self) - self.monitor_timer.timeout.connect(self.monitor_current_job) - self.monitor_timer.start(300) - - def add_input_path(self, path): - self.manager.add_path(path) - self.layoutChanged.emit() - - def data(self, index, role): - status = self.manager[index.row()] - key = self.map_columns[index.column()] - if role in [ItemProgressRole, QtCore.Qt.ItemDataRole.DisplayRole]: - return status[key] - else: - return QtCore.QVariant() - - def columnCount(self, parent): - return len(self.headers) - - def headerData(self, section, orientation, role): - if role != QtCore.Qt.ItemDataRole.DisplayRole: - return QtCore.QVariant() - return self.headers[section] - - def rowCount(self, parent): - return len(self.manager) - - @QtCore.pyqtSlot() - def monitor_current_job(self): - current_index = self.manager.current_index - if current_index is not None: - self.update_index(current_index) - - @QtCore.pyqtSlot(int) - def update_index(self, index): - # update the row - index_1 = self.index(index, 0) - index_2 = self.index(index, len(self.headers)) - self.dataChanged.emit(index_1, - index_2, - [QtCore.Qt.ItemDataRole.DisplayRole, - QtCore.Qt.ItemDataRole.DisplayRole, - ItemProgressRole]) - - -class ProgressTable(QtWidgets.QTableView): +class ProgressTable(QtWidgets.QTableWidget): row_selected = QtCore.pyqtSignal(int) def __init__(self, *args, **kwargs): super(ProgressTable, self).__init__(*args, **kwargs) - pbar_delegate = ProgressDelegate(self) - self.setItemDelegateForColumn(2, pbar_delegate) - self.model = ProgressModel() - self.setModel(self.model) - self.setColumnWidth(0, 400) - self.setColumnWidth(1, 80) + self.job_manager = None # signals - self.selectionModel().selectionChanged.connect( + self.itemSelectionChanged.connect( self.on_selection_changed) - def add_input_path(self, path): - self.model.add_input_path(path) + # timer for updating table contents + self.monitor_timer = QtCore.QTimer(self) + self.monitor_timer.timeout.connect( + self.update_from_job_manager_progress) + self.monitor_timer.start(300) @QtCore.pyqtSlot() def on_selection_changed(self): """Emit a row-selected signal""" - row = self.selectionModel().currentIndex().row() + row = self.currentIndex().row() self.row_selected.emit(row) - def update(self): - self.model.dataChanged.emit() + def set_job_manager(self, job_manager): + if self.job_manager is None: + self.job_manager = job_manager + else: + raise ValueError("Job manager already set!") + + def set_item_label(self, row, col, label, align=None): + """Get/Create a Qlabel at the specified position + """ + label = f"{label}" + item = self.item(row, col) + if item is None: + item = QtWidgets.QTableWidgetItem(label) + self.setItem(row, col, item) + if align is not None: + item.setTextAlignment(align) + else: + if item.text() != label: + item.setText(label) + + def set_item_progress(self, row, col, progress): + """Get/Create a QProgressBar at the specified position + """ + pb = self.cellWidget(row, col) + if pb is None: + pb = QtWidgets.QProgressBar(self) + pb.setMaximum(1000) + self.setCellWidget(row, col, pb) + else: + if pb.value() != int(progress*1000): + pb.setValue(int(progress*1000)) + + @QtCore.pyqtSlot() + def update_from_job_manager(self): + if self.job_manager is None: + raise ValueError("Job manager not set!") + self.setRowCount(len(self.job_manager)) + # Check rows and populate new items + for ii in range(len(self.job_manager)): + status = self.job_manager[ii] + self.set_item_label(ii, 0, str(status["path"])) + self.set_item_label(ii, 1, str(status["state"]), + align=QtCore.Qt.AlignmentFlag.AlignCenter) + self.set_item_progress(ii, 2, status["progress"]) + # Set path column width to something large (does not work during init) + if self.columnWidth(0) == 100: + self.setColumnWidth(0, 500) + + @QtCore.pyqtSlot() + def update_from_job_manager_progress(self): + for ii in range(len(self.job_manager)): + st = self.item(ii, 1) + st.setText(self.job_manager[ii]["state"]) + pb = self.cellWidget(ii, 2) + progress = self.job_manager[ii]["progress"] + if pb is not None and pb.value() != int(progress*1000): + pb.setValue(int(progress*1000)) diff --git a/tests/test_gui.py b/tests/test_gui.py index 36f39d3..b72dbf4 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -37,7 +37,7 @@ def test_gui_basic(mw): # Just check some known properties in the UI. assert mw.spinBox_thresh.value() == -6 assert mw.checkBox_feat_bright.isChecked() - assert len(mw.manager) == 0 + assert len(mw.job_manager) == 0 @pytest.mark.parametrize("correct_offset", [True, False]) @@ -49,7 +49,7 @@ def test_gui_correct_offset(mw, correct_offset): mw.checkBox_bg_flickering.setChecked(correct_offset) mw.doubleSpinBox_pixel_size.setValue(0.666) mw.on_run() - while mw.manager.is_busy(): + while mw.job_manager.is_busy(): time.sleep(.1) out_path = path.with_name(path.stem + "_dcn.rtdc") assert out_path.exists() @@ -68,7 +68,7 @@ def test_gui_set_pixel_size(mw): mw.checkBox_pixel_size.setChecked(True) mw.doubleSpinBox_pixel_size.setValue(0.666) mw.on_run() - while mw.manager.is_busy(): + while mw.job_manager.is_busy(): time.sleep(.1) out_path = path.with_name(path.stem + "_dcn.rtdc") assert out_path.exists() @@ -88,7 +88,7 @@ def test_gui_use_volume(mw, use_volume): mw.checkBox_feat_volume.setChecked(use_volume) mw.doubleSpinBox_pixel_size.setValue(0.666) mw.on_run() - while mw.manager.is_busy(): + while mw.job_manager.is_busy(): time.sleep(.1) out_path = path.with_name(path.stem + "_dcn.rtdc") assert out_path.exists()