From 9beae2bc2b370b0a9eca6e472dd1f6b9e331775a Mon Sep 17 00:00:00 2001 From: GuyTeichman <48219633+GuyTeichman@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:46:40 +0300 Subject: [PATCH] transitioned RNAlysis to Qt6 --- pytest.ini | 2 +- requirements.txt | 6 +- rnalysis/gui/gui.py | 216 ++++++++++---------- rnalysis/gui/gui_graphics.py | 4 +- rnalysis/gui/gui_quickstart.py | 24 +-- rnalysis/gui/gui_style.py | 2 +- rnalysis/gui/gui_widgets.py | 50 ++--- rnalysis/gui/gui_windows.py | 350 ++++++++------------------------- tests/test_gui.py | 52 ++--- tests/test_gui_graphics.py | 6 +- tests/test_gui_quickstart.py | 44 ++--- tests/test_gui_widgets.py | 10 +- tests/test_gui_windows.py | 71 +------ 13 files changed, 301 insertions(+), 536 deletions(-) diff --git a/pytest.ini b/pytest.ini index 608ad155f..ddae59950 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,4 +2,4 @@ testpaths = tests/ python_files = *.py python_functions = test_* -qt_api=pyqt5 +qt_api=pyqt6 diff --git a/requirements.txt b/requirements.txt index 76d2bdd33..5af48976d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,8 +15,8 @@ joblib>=1.4.2 tqdm>=4.65 appdirs>=1.4.0 typing_extensions>=4.5 -PyQt5>=5.15.9 -qdarkstyle +PyQt6>=6.7 +qdarkstyle>=3 defusedxml>=0.7.1 aiohttp>=3.8.4, <3.10 aiodns>=3.0.0 @@ -26,5 +26,5 @@ tenacity>=8.2.3 mslex>=1.1.0 nest-asyncio>=1.6.0 kmedoids>=0.5.1 -polars[async,numpy,pyarrow,pandas]>=1.5.0 +polars[async,numpy,pyarrow,pandas]>=1.6.0,<1.7 pandas[performance,parquet] diff --git a/rnalysis/gui/gui.py b/rnalysis/gui/gui.py index 62400629a..841d0f6f3 100644 --- a/rnalysis/gui/gui.py +++ b/rnalysis/gui/gui.py @@ -20,7 +20,7 @@ import numpy as np import polars as pl import yaml -from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt6 import QtCore, QtWidgets, QtGui from rnalysis import fastq, filtering, enrichment, __version__ from rnalysis.gui import gui_style, gui_widgets, gui_windows, gui_graphics, gui_quickstart @@ -512,7 +512,7 @@ def init_setups_ui(self): self.setups_grid.addWidget(self.stack, 1, 0) self.setups_widgets['list'] = gui_widgets.MultiChoiceListWithDelete(list(), parent=self.setups_group) self.setups_widgets['list'].itemDeleted.connect(self.remove_clustering_setup) - self.setups_grid.addWidget(QtWidgets.QLabel('Added setups'), 0, 1, QtCore.Qt.AlignCenter) + self.setups_grid.addWidget(QtWidgets.QLabel('Added setups'), 0, 1, QtCore.Qt.AlignmentFlag.AlignCenter) self.setups_grid.addWidget(self.setups_widgets['list'], 1, 1, 2, 1) self.setups_widgets['add_button'] = QtWidgets.QPushButton('Add setup') @@ -622,10 +622,10 @@ def init_basic_ui(self): self.plot_group.setVisible(False) self.stats_group.setVisible(False) - self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.scroll.setWidgetResizable(True) self.scroll.setWidget(self.scroll_widget) - self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinAndMaxSize) self.scroll_layout.addWidget(self.list_group) self.scroll_layout.addWidget(self.stats_group) @@ -994,7 +994,7 @@ def init_ui(self): self.setWindowTitle('Set Operations') self.setGeometry(600, 50, 1050, 800) self.setLayout(self.layout) - self.widgets['splitter'] = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + self.widgets['splitter'] = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) self.layout.addWidget(self.widgets['splitter']) self.widgets['splitter'].addWidget(self.list_group) self.widgets['splitter'].addWidget(self.operations_group) @@ -1223,7 +1223,7 @@ def init_ui(self): self.setWindowTitle('Gene Set Visualization') self.setGeometry(600, 50, 1050, 800) self.setLayout(self.layout) - self.widgets['splitter'] = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + self.widgets['splitter'] = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) self.layout.addWidget(self.widgets['splitter']) self.widgets['splitter'].addWidget(self.list_group) self.widgets['splitter'].addWidget(self.visualization_group) @@ -1398,18 +1398,25 @@ class TabPage(QtWidgets.QWidget): GENERAL_FUNCS = () THREADED_FUNCS = set() - def __init__(self, parent=None, undo_stack: QtWidgets.QUndoStack = None, tab_id: int = None): + def __init__(self, parent=None, undo_stack: QtGui.QUndoStack = None, tab_id: int = None): super().__init__(parent) self.tab_id = tab_id self.undo_stack = undo_stack self.sup_layout = QtWidgets.QVBoxLayout(self) - self.container = QtWidgets.QWidget(self) - self.layout = QtWidgets.QVBoxLayout(self.container) + + self.splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical) + self.sup_layout.addWidget(self.splitter) + + # initiate the splitter layout for the tab self.scroll = QtWidgets.QScrollArea() - self.scroll.setWidget(self.container) - self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.scroll.setWidgetResizable(True) + self.splitter.addWidget(self.scroll) + + self.container = QtWidgets.QWidget() + self.layout = QtWidgets.QVBoxLayout(self.container) + self.scroll.setWidget(self.container) self.name = None self.creation_time = time.time() @@ -1429,7 +1436,7 @@ def __init__(self, parent=None, undo_stack: QtWidgets.QUndoStack = None, tab_id: self.function_group = QtWidgets.QGroupBox('Apply functions') self.function_grid = QtWidgets.QGridLayout(self.function_group) self.function_widgets = {} - self.layout.insertWidget(2, self.function_group) + self.layout.addWidget(self.function_group) self.function_group.setVisible(False) self.stdout_group = QtWidgets.QGroupBox('Log') @@ -1439,13 +1446,9 @@ def __init__(self, parent=None, undo_stack: QtWidgets.QUndoStack = None, tab_id: # initiate apply button self.apply_button = QtWidgets.QPushButton('Apply') self.apply_button.clicked.connect(self.apply_function) - self.layout.insertWidget(3, self.apply_button) + self.layout.addWidget(self.apply_button) self.apply_button.setVisible(False) - # initiate the splitter layout for the tab - self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) - self.sup_layout.addWidget(self.splitter) - self.splitter.addWidget(self.scroll) self.splitter.addWidget(self.stdout_group) self.splitter.setStretchFactor(0, 1) @@ -1670,7 +1673,7 @@ class SetTabPage(TabPage): } def __init__(self, set_name: str, gene_set: typing.Union[set, enrichment.FeatureSet] = None, parent=None, - undo_stack: QtWidgets.QUndoStack = None, tab_id: int = None): + undo_stack: QtGui.QUndoStack = None, tab_id: int = None): super().__init__(parent, undo_stack, tab_id) if gene_set is None: gene_set = enrichment.FeatureSet(set(), set_name) @@ -1971,7 +1974,7 @@ class FilterTabPage(TabPage): startedClustering = QtCore.pyqtSignal(object, object, object) widthChanged = QtCore.pyqtSignal() - def __init__(self, parent=None, undo_stack: QtWidgets.QUndoStack = None, tab_id: int = None): + def __init__(self, parent=None, undo_stack: QtGui.QUndoStack = None, tab_id: int = None): super().__init__(parent, undo_stack, tab_id) self.filter_obj = None @@ -2051,8 +2054,8 @@ def init_overview_ui(self): self.overview_widgets['table_name_label'].setWordWrap(True) self.overview_widgets['preview'] = gui_widgets.ReactiveTableView() - self.overview_widgets['preview'].setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.overview_widgets['preview'].setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.overview_widgets['preview'].setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.overview_widgets['preview'].setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.overview_grid.addWidget(self.overview_widgets['table_name_label'], this_row, 0, 1, 4) this_row += 1 @@ -2158,7 +2161,7 @@ def init_basic_ui(self): self.basic_grid.setRowStretch(4, 1) self.basic_grid.setColumnStretch(4, 1) - def _check_for_special_functions(self, is_selected: bool): + def _check_for_special_functions(self, is_selected: bool=True): if not is_selected: return this_stack: FuncTypeStack = self.stack.currentWidget() @@ -2513,7 +2516,7 @@ def remove_last_function(self): err = QtWidgets.QMessageBox(self) err.setWindowTitle('Pipeline is already empty!') err.setText('Cannot remove functions from the Pipeline - it is already empty!') - err.setIcon(err.Warning) + err.setIcon(err.Icon.Warning) err.exec() def _get_pipeline_name(self): @@ -2536,9 +2539,10 @@ def closeEvent(self, event): # pragma: no cover "All unsaved progress will be lost" reply = QtWidgets.QMessageBox.question(self, "Close 'Create Pipeline' window?", - quit_msg, QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + quit_msg, QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes) - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: event.accept() else: event.ignore() @@ -2577,7 +2581,8 @@ def __init__(self, objs: List[Union[filtering.Filter, enrichment.FeatureSet]], j self.objs[key] = obj self.files = list(self.objs.keys()) - self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + self.button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.labels = dict() self.keep_marks = dict() self.names = dict() @@ -2596,7 +2601,7 @@ def init_ui(self): self.scroll.setWidgetResizable(True) self.scroll.setWidget(self.scroll_widget) self.scroll_widget.setLayout(self.scroll_layout) - self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinAndMaxSize) self.select_all.clicked.connect(self.change_all) self.button_box.accepted.connect(self.accept) @@ -2658,7 +2663,8 @@ class MultiOpenWindow(QtWidgets.QDialog): def __init__(self, files: List[str], parent=None): super().__init__(parent) self.files = files - self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + self.button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.all_types_combo = QtWidgets.QComboBox(self) self.paths = dict() self.table_types = dict() @@ -2679,7 +2685,7 @@ def init_ui(self): self.scroll.setWidgetResizable(True) self.scroll.setWidget(self.scroll_widget) self.scroll_widget.setLayout(self.scroll_layout) - self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinAndMaxSize) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) @@ -2770,9 +2776,9 @@ def __init__(self, parent=None): self.setElideMode(QtCore.Qt.TextElideMode.ElideMiddle) def mousePressEvent(self, event: QtGui.QMouseEvent): - if event.button() == QtCore.Qt.LeftButton: + if event.button() == QtCore.Qt.MouseButton.LeftButton: super().mousePressEvent(event) - elif event.button() == QtCore.Qt.RightButton: + elif event.button() == QtCore.Qt.MouseButton.RightButton: point = event.pos() if point.isNull(): return @@ -2810,7 +2816,7 @@ def widget(self, index: int) -> Union[FilterTabPage, SetTabPage]: return super().widget(index) -class RenameCommand(QtWidgets.QUndoCommand): +class RenameCommand(QtGui.QUndoCommand): __slots__ = {'prev_name': 'previous name of the tab', 'new_name': 'new name of the tab', 'prev_id': 'previous ID', @@ -2835,7 +2841,7 @@ def redo(self): self.tab._rename(self.new_name, self.job_id) -class CloseTabCommand(QtWidgets.QUndoCommand): +class CloseTabCommand(QtGui.QUndoCommand): __slots__ = {'tab_container': 'ReactiveTabWidget containing the tabs', 'tab_index': 'index of the tab to be closed', 'tab_icon': 'icon of the tab', @@ -2868,7 +2874,7 @@ def redo(self): self.tab_container.removeTab(self.tab_index) -class InplaceCommand(QtWidgets.QUndoCommand): +class InplaceCommand(QtGui.QUndoCommand): __slots__ = {'tab': 'tab widget', 'prev_job_id': 'previous job ID', 'new_job_id': 'new job ID', @@ -2965,7 +2971,7 @@ def redo(self): self.tab.itemSpawned.emit(f"'{source_name}'\noutput", new_spawn_id, self.new_job_id, self.tab.obj()) -class PipelineInplaceCommand(QtWidgets.QUndoCommand): +class PipelineInplaceCommand(QtGui.QUndoCommand): __slots__ = {'tab': 'tab object', 'pipeline': 'Pipeline to apply', 'pipeline_name': 'Pipeline name', @@ -3016,8 +3022,8 @@ def __init__(self, gather_stdout: bool = True): self.report = None self.tabs = ReactiveTabWidget(self) - self.closed_tabs_stack = QtWidgets.QUndoStack(self) - self.undo_group = QtWidgets.QUndoGroup(self) + self.closed_tabs_stack = QtGui.QUndoStack(self) + self.undo_group = QtGui.QUndoGroup(self) self.tabs.currentChanged.connect(self._change_undo_stack) self.undo_view = QtWidgets.QUndoView(self.undo_group) @@ -3150,12 +3156,12 @@ def init_ui(self): self.command_history_dock.setWidget(self.undo_view) self.command_history_dock.setFloating(False) - self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.command_history_dock) + self.addDockWidget(QtCore.Qt.DockWidgetArea.RightDockWidgetArea, self.command_history_dock) self.setStatusBar(self.status_bar) self.task_queue_window.cancelRequested.connect(self.cancel_job) - self.tabs.setCornerWidget(self.add_tab_button, QtCore.Qt.TopRightCorner) + self.tabs.setCornerWidget(self.add_tab_button, QtCore.Qt.Corner.TopRightCorner) self.setCentralWidget(self.tabs) @QtCore.pyqtSlot() @@ -3164,11 +3170,12 @@ def clear_history(self, confirm_action: bool = True): clear_msg = """Are you sure you want to clear all command history? This cannot be undone!""" reply = QtWidgets.QMessageBox.question(self, 'Clear history', - clear_msg, QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + clear_msg, QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes) else: - reply = QtWidgets.QMessageBox.Yes + reply = QtWidgets.QMessageBox.StandardButton.Yes - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: for stack in self.undo_group.stacks(): stack.clear() self.closed_tabs_stack.clear() @@ -3177,7 +3184,7 @@ def clear_history(self, confirm_action: bool = True): def init_tab_contextmenu(self, ind: int): self.tab_contextmenu = QtWidgets.QMenu(self) - new_to_right_action = QtWidgets.QAction("New tab to the right") + new_to_right_action = QtGui.QAction("New tab to the right") new_to_right_action.triggered.connect( functools.partial(self.add_new_tab_at, index=ind + 1, name=None, is_set=False)) self.tab_contextmenu.addAction(new_to_right_action) @@ -3186,36 +3193,36 @@ def init_tab_contextmenu(self, ind: int): color_menu = self.tab_contextmenu.addMenu("Change tab &color") actions = [] for color in gui_graphics.COLOR_ICONS: - this_action = QtWidgets.QAction(color.capitalize()) + this_action = QtGui.QAction(color.capitalize()) this_action.setIcon(gui_graphics.get_icon(color)) this_action.triggered.connect(functools.partial(self.set_tab_icon, ind, icon_name=color)) actions.append(this_action) color_menu.addAction(this_action) - reset_action = QtWidgets.QAction("Reset color") + reset_action = QtGui.QAction("Reset color") reset_action.triggered.connect(functools.partial(self.set_tab_icon, ind, icon_name=None)) color_menu.addAction(reset_action) self.tab_contextmenu.addSeparator() # sort_menu = self.tab_contextmenu.addMenu("Sort tabs") - sort_by_name = QtWidgets.QAction("Sort by tab &name") + sort_by_name = QtGui.QAction("Sort by tab &name") sort_by_name.triggered.connect(self.sort_tabs_by_name) - sort_by_time = QtWidgets.QAction("Sort by creation &time") + sort_by_time = QtGui.QAction("Sort by creation &time") sort_by_time.triggered.connect(self.sort_tabs_by_creation_time) - sort_by_type = QtWidgets.QAction("Sort by tab type") + sort_by_type = QtGui.QAction("Sort by tab type") sort_by_type.triggered.connect(self.sort_tabs_by_type) - sort_by_size = QtWidgets.QAction("Sort by number of features") + sort_by_size = QtGui.QAction("Sort by number of features") sort_by_size.triggered.connect(self.sort_tabs_by_n_features) - reverse = QtWidgets.QAction("Reverse tab order") + reverse = QtGui.QAction("Reverse tab order") reverse.triggered.connect(self.sort_reverse) self.tab_contextmenu.addActions([sort_by_name, sort_by_time, sort_by_type, sort_by_size, reverse]) self.tab_contextmenu.addSeparator() - close_this_action = QtWidgets.QAction("Close") + close_this_action = QtGui.QAction("Close") close_this_action.triggered.connect(functools.partial(self.close_tab, ind)) - close_others_action = QtWidgets.QAction("Close other tabs") + close_others_action = QtGui.QAction("Close other tabs") close_others_action.triggered.connect(functools.partial(self.close_other_tabs, ind)) - close_right_action = QtWidgets.QAction("Close tabs to the right") + close_right_action = QtGui.QAction("Close tabs to the right") close_right_action.triggered.connect(functools.partial(self.close_tabs_to_the_right, ind)) - close_left_action = QtWidgets.QAction("Close tabs to the left") + close_left_action = QtGui.QAction("Close tabs to the left") close_left_action.triggered.connect(functools.partial(self.close_tabs_to_the_left, ind)) self.tab_contextmenu.addActions([close_this_action, close_others_action, close_right_action, close_left_action]) @@ -3306,7 +3313,7 @@ def add_new_tab_at(self, index: int, name: str = None, is_set: bool = False): self.tabs.tabBar().moveTab(self.tabs.currentIndex(), index) def add_new_tab(self, name: str = None, is_set: bool = False): - new_undo_stack = QtWidgets.QUndoStack() + new_undo_stack = QtGui.QUndoStack() self.undo_group.addStack(new_undo_stack) if name is None: name = 'New Table' @@ -3354,7 +3361,7 @@ def remove_tab_asterisk(self): self.tabs.setTabText(self.tab.currentIndex(), current_name.rstrip('*')) def new_table_from_folder(self): - folder_name = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose directory", str(Path.home())) + folder_name = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose directory") if folder_name: filter_obj = filtering.CountFilter.from_folder(folder_name) if self.tabs.currentWidget().is_empty(): @@ -3362,13 +3369,13 @@ def new_table_from_folder(self): self.new_tab_from_filter_obj(filter_obj, JOB_COUNTER.get_id()) def new_table_from_folder_htseqcount(self): - folder_name = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose directory", str(Path.home())) + folder_name = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose directory") if folder_name: normalize_answer = QtWidgets.QMessageBox.question(self, 'Normalize values?', "Do you want to normalize your count table to " "reads-per-million (RPM)?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) - to_normalize = normalize_answer == QtWidgets.QMessageBox.Yes + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) + to_normalize = normalize_answer == QtWidgets.QMessageBox.StandardButton.Yes filter_obj = filtering.CountFilter.from_folder_htseqcount(folder_name, norm_to_rpm=to_normalize) if self.tabs.currentWidget().is_empty(): @@ -3376,10 +3383,8 @@ def new_table_from_folder_htseqcount(self): self.new_tab_from_filter_obj(filter_obj, JOB_COUNTER.get_id()) def load_multiple_files(self): - dialog = gui_windows.MultiFileSelectionDialog() - accepted = dialog.exec() - if accepted == QtWidgets.QDialog.Accepted: - filenames = dialog.result() + filenames, _ = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose files") + if filenames: if len(filenames) > 0: window = MultiOpenWindow(filenames, self) accepted = window.exec() @@ -3574,8 +3579,8 @@ def delete_pipeline(self): reply = QtWidgets.QMessageBox.question(self, 'Delete Pipeline?', "Are you sure you want to delete this Pipeline? " "This action cannot be undone!", - QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes) - if reply == QtWidgets.QMessageBox.Yes: + QtWidgets.QMessageBox.StandardButton.No | QtWidgets.QMessageBox.StandardButton.Yes) + if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.pipelines.pop(pipeline_name) print(f"Pipeline '{pipeline_name}' deleted successfully") @@ -3626,7 +3631,7 @@ def import_pipeline(self): def import_multiple_gene_sets(self): dialog = gui_windows.MultiFileSelectionDialog() accepted = dialog.exec() - if accepted == QtWidgets.QDialog.Accepted: + if accepted == QtWidgets.QDialog.DialogCode.Accepted: filenames = dialog.result() tabs_to_close = None if len(filenames) > 0 and self.tabs.currentWidget().is_empty(): @@ -3638,11 +3643,11 @@ def import_multiple_gene_sets(self): self.tabs.removeTab(tabs_to_close) def import_gene_set(self): - filename, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Choose a file", str(Path.home()), - "Text Document (*.txt);;" - "Comma-Separated Values (*.csv);;" - "Tab-Separated Values (*.tsv);;" - "All Files (*)") + filename, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Choose a file", filter= + "Text Document (*.txt);;" + "Comma-Separated Values (*.csv);;" + "Tab-Separated Values (*.tsv);;" + "All Files (*)") if filename: tabs_to_close = None if self.tabs.currentWidget().is_empty(): @@ -3733,13 +3738,13 @@ def save_pipeline(self, pipeline_name: str, pipeline: filtering.Pipeline): response = QtWidgets.QMessageBox.question(self, 'Overwrite Pipeline?', 'A Pipeline with this name already exists. ' 'Are you sure you want to overwrite it?', - defaultButton=QtWidgets.QMessageBox.No) + defaultButton=QtWidgets.QMessageBox.StandardButton.No) else: is_new = True - response = QtWidgets.QMessageBox.Yes + response = QtWidgets.QMessageBox.StandardButton.Yes - if response == QtWidgets.QMessageBox.Yes: + if response == QtWidgets.QMessageBox.StandardButton.Yes: new_pipeline_id = JOB_COUNTER.get_id() if self._generate_report: if is_new: @@ -3754,7 +3759,7 @@ def settings(self): self.settings_window.exec() def create_action(self, name, triggered_func, checkable=False, checked=False, enabled=True, shortcut=None): - action = QtWidgets.QAction(name, self) + action = QtGui.QAction(name, self) action.triggered.connect(triggered_func) action.setCheckable(checkable) action.setChecked(checked) @@ -3923,8 +3928,8 @@ def clear_cache(self): reply = QtWidgets.QMessageBox.question(self, 'Clear cache?', 'Are you sure you want to clear the RNAlysis cache? ' 'This cannot be undone!', - defaultButton=QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: + defaultButton=QtWidgets.QMessageBox.StandardButton.No) + if reply == QtWidgets.QMessageBox.StandardButton.Yes: io.clear_gui_cache() io.clear_cache() @@ -3936,16 +3941,16 @@ def check_for_updates(self, confirm_updated: bool = True): # pragma: no cover reply = QtWidgets.QMessageBox.question(self, 'A new version is available', 'A new version of RNAlysis is available! ' 'Do you wish to download it?') - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: url = QtCore.QUrl('https://github.com/GuyTeichman/RNAlysis/releases/latest') if not QtGui.QDesktopServices.openUrl(url): - QtGui.QMessageBox.warning(self, 'Connection failed', 'Could not download new version') + QtWidgets.QMessageBox.warning(self, 'Connection failed', 'Could not download new version') return reply = QtWidgets.QMessageBox.question(self, 'A new version is available', 'A new version of RNAlysis is available! ' 'Do you wish to update?') - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: io.update_rnalysis() QtCore.QCoreApplication.quit() self.deleteLater() @@ -4187,7 +4192,7 @@ def _populate_pipelines(self, menu: QtWidgets.QMenu, func: Callable, pipeline_ar # Dynamically create the actions actions = [] for name, (pipeline, pipeline_id) in self.pipelines.items(): - action = QtWidgets.QAction(name, self) + action = QtGui.QAction(name, self) args = [] if pipeline_arg: args.append(pipeline) @@ -4225,11 +4230,11 @@ def _apply_table_pipeline(self, pipeline: filtering.Pipeline, pipeline_name: str apply_msg = f"Do you want to apply Pipeline '{pipeline_name}' inplace?" reply = QtWidgets.QMessageBox.question(self, f"Apply Pipeline '{pipeline_name}'", apply_msg, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | - QtWidgets.QMessageBox.Cancel) - if reply == QtWidgets.QMessageBox.Cancel: + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No | + QtWidgets.QMessageBox.StandardButton.Cancel) + if reply == QtWidgets.QMessageBox.StandardButton.Cancel: return - inplace = reply == QtWidgets.QMessageBox.Yes + inplace = reply == QtWidgets.QMessageBox.StandardButton.Yes available_objs = self.get_available_objects() filtered_available_objs = {} @@ -4254,11 +4259,11 @@ def clear_session(self, confirm_action: bool = True) -> bool: response = QtWidgets.QMessageBox.question(self, 'Clear session?', 'Are you sure you want to clear your session? ' 'All unsaved changes will be lost!', - defaultButton=QtWidgets.QMessageBox.No) + defaultButton=QtWidgets.QMessageBox.StandardButton.No) else: - response = QtWidgets.QMessageBox.Yes + response = QtWidgets.QMessageBox.StandardButton.Yes - if response == QtWidgets.QMessageBox.Yes: + if response == QtWidgets.QMessageBox.StandardButton.Yes: self.close_figs_action.trigger() self.close_external_windows() while self.tabs.count() > 1: @@ -4270,13 +4275,12 @@ def clear_session(self, confirm_action: bool = True) -> bool: self._change_undo_stack(0) self._reset_reporting() - return response == QtWidgets.QMessageBox.Yes + return response == QtWidgets.QMessageBox.StandardButton.Yes def load_session(self): session_filename, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Load session", - str(Path.home()), - "RNAlysis session files (*.rnal);;" - "All Files (*)") + filter="RNAlysis session files (*.rnal);;" + "All Files (*)") if session_filename: self._load_session_from(session_filename) @@ -4290,7 +4294,7 @@ def _load_session_from(self, session_filename: Union[str, Path]): load_report = QtWidgets.QMessageBox.question(self, 'Resume previous session report?', 'Do you want to resume the previous session report?\n' 'This will clear the current session and replace it. ', - defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes + defaultButton=QtWidgets.QMessageBox.StandardButton.Yes) == QtWidgets.QMessageBox.StandardButton.Yes if load_report: self.clear_session(confirm_action=False) self._toggle_reporting(True) @@ -4387,9 +4391,10 @@ def closeEvent(self, event): # pragma: no cover "All unsaved progress will be lost" reply = QtWidgets.QMessageBox.question(self, 'Close program', - quit_msg, QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + quit_msg, QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes) - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: plt.close('all') # quit job and STDOUT listener threads try: @@ -4675,25 +4680,34 @@ async def run(): # pragma: no cover app.setDesktopFileName('RNAlysis') icon_pth = str(Path(__file__).parent.parent.joinpath('favicon.ico').absolute()) app.setWindowIcon(QtGui.QIcon(icon_pth)) - matplotlib.use('Qt5Agg') + matplotlib.use('QtAgg') if show_app: splash = gui_windows.splash_screen() app.processEvents() base_message = f"RNAlysis version {__version__}:\t" - splash.showMessage(base_message + 'loading dependencies', QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter) + splash.showMessage(base_message + 'loading dependencies', + QtCore.Qt.AlignmentFlag.AlignBottom | QtCore.Qt.AlignmentFlag.AlignHCenter) gui_widgets.init_color_map_pixmap_cache() if io.check_changed_version(): video_files = gui_quickstart.QuickStartWizard.VIDEO_FILES splash.showMessage(base_message + 'validating tutorial videos', - QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter) + QtCore.Qt.AlignmentFlag.AlignBottom | QtCore.Qt.AlignmentFlag.AlignHCenter) async for i in io.get_gui_videos(video_files): splash.showMessage(base_message + f'getting tutorial videos {i + 1}/{len(video_files)}', - QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter) + QtCore.Qt.AlignmentFlag.AlignBottom | QtCore.Qt.AlignmentFlag.AlignHCenter) + + splash.showMessage(base_message + 'loading application', + QtCore.Qt.AlignmentFlag.AlignBottom | QtCore.Qt.AlignmentFlag.AlignHCenter) + + # set taskbar icon on Windows + if platform.system() == 'Windows': + import ctypes + myappid = u'RNAlysis.{version}'.format(version=__version__) # arbitrary string + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) - splash.showMessage(base_message + 'loading application', QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter) window = MainWindow() sys.excepthook = window.excepthook builtins.input = window.input diff --git a/rnalysis/gui/gui_graphics.py b/rnalysis/gui/gui_graphics.py index ce0893216..2d9aa5245 100644 --- a/rnalysis/gui/gui_graphics.py +++ b/rnalysis/gui/gui_graphics.py @@ -7,7 +7,7 @@ import matplotlib import matplotlib_venn import upsetplot -from PyQt5 import QtCore, QtGui +from PyQt6 import QtCore, QtGui from matplotlib import pyplot as plt from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT @@ -473,6 +473,6 @@ def get_icon(name: str): return icon elif name == 'blank': pixmap = QtGui.QPixmap(32, 32) - pixmap.fill(QtCore.Qt.transparent) + pixmap.fill(QtCore.Qt.GlobalColor.transparent) return QtGui.QIcon(pixmap) return None diff --git a/rnalysis/gui/gui_quickstart.py b/rnalysis/gui/gui_quickstart.py index 46683855e..01590f587 100644 --- a/rnalysis/gui/gui_quickstart.py +++ b/rnalysis/gui/gui_quickstart.py @@ -1,6 +1,6 @@ from pathlib import Path -from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt6 import QtCore, QtWidgets, QtGui from rnalysis.utils import settings, io @@ -134,9 +134,9 @@ def __init__(self, parent=None): self.currentIdChanged.connect(self.play_tutorial) - self.setWizardStyle(QtWidgets.QWizard.ModernStyle) + self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle) self.setWindowTitle('Welcome to RNAlysis!') - self.setPixmap(self.LogoPixmap, + self.setPixmap(self.WizardPixmap.LogoPixmap, QtGui.QPixmap(Path.cwd().parent.parent.joinpath('docs/source/favicon.ico').as_posix())) self.setField('dont_show_again', not settings.get_show_tutorial_settings()) @@ -223,22 +223,22 @@ def __init__(self, video_path: Path, parent=None): self.layout = QtWidgets.QGridLayout(self) self.label = QtWidgets.QLabel(self) self.video = QtGui.QMovie(full_video_path) - self.video.setCacheMode(self.video.CacheAll) + self.video.setCacheMode(self.video.CacheMode.CacheAll) self.label.setMovie(self.video) self.play_button = QtWidgets.QToolButton() self.play_button.clicked.connect(self.change_play_state) self.stop_button = QtWidgets.QToolButton() - self.stop_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaStop)) + self.stop_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaStop)) self.stop_button.clicked.connect(self.stop) self.speed_button = QtWidgets.QToolButton() - self.speed_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaSeekForward)) + self.speed_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSeekForward)) self.speed_button.setCheckable(True) self.speed_button.clicked.connect(self.change_speed) - self.position_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) + self.position_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) self.position_slider.setRange(0, self.video.frameCount()) self.position_slider.valueChanged.connect(self.set_frame) self.position_slider.setTracking(False) @@ -256,7 +256,7 @@ def __init__(self, video_path: Path, parent=None): pixmap = QtGui.QPixmap(full_video_path) size = pixmap.size() - size.scale(750, 750, QtCore.Qt.KeepAspectRatio) + size.scale(750, 750, QtCore.Qt.AspectRatioMode.KeepAspectRatio) self.video.setScaledSize(size) self.layout.addWidget(self.label, 0, 0, 4, 8) @@ -279,19 +279,19 @@ def change_play_state(self): def update_play_button(self): if self.paused: - self.play_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay)) + self.play_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPlay)) else: - self.play_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaPause)) + self.play_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPause)) QtWidgets.QApplication.processEvents() def mousePressEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: + if event.button() == QtCore.Qt.MouseButton.LeftButton: self.press_pos = event.pos() def mouseReleaseEvent(self, event): # ensure that the left button was pressed *and* released within the # geometry of the widget; if so, emit the signal; - if self.press_pos is not None and event.button() == QtCore.Qt.LeftButton and event.pos() in self.rect(): + if self.press_pos is not None and event.button() == QtCore.Qt.MouseButton.LeftButton and event.pos() in self.rect(): self.clicked.emit() self.press_pos = None diff --git a/rnalysis/gui/gui_style.py b/rnalysis/gui/gui_style.py index a5a43eaf7..3696304ba 100644 --- a/rnalysis/gui/gui_style.py +++ b/rnalysis/gui/gui_style.py @@ -43,5 +43,5 @@ def get_stylesheet(): QSplitter::handle:hover { background-color: #788D9C; } """ else: - other_stylesheet = qdarkstyle.load_stylesheet(qt_api='pyqt5', palette=palette) + other_stylesheet = qdarkstyle.load_stylesheet(qt_api='pyqt6', palette=palette) return param_stylesheet + '\n' + other_stylesheet diff --git a/rnalysis/gui/gui_widgets.py b/rnalysis/gui/gui_widgets.py index 6a148b9e6..0cea4c6b2 100644 --- a/rnalysis/gui/gui_widgets.py +++ b/rnalysis/gui/gui_widgets.py @@ -14,7 +14,7 @@ import numpy as np import polars as pl import polars.selectors as cs -from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt6 import QtCore, QtWidgets, QtGui from joblib import Parallel, parallel_backend from tqdm.auto import tqdm from typing_extensions import get_origin, get_args @@ -93,13 +93,13 @@ def value(self): return picked_cols def _update_window_size(self): - self.dialog_table.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) - # self.dialog_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.dialog_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.dialog_table.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + # self.dialog_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.dialog_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.dialog_table.resizeRowsToContents() self.dialog_table.resizeColumnsToContents() - self.dialog_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - self.dialog_table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) + self.dialog_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents) + self.dialog_table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) screen_height = QtWidgets.QApplication.primaryScreen().size().height() @@ -303,7 +303,7 @@ def __init__(self, message: str = "No prompt available", parent=None): super().__init__(parent) self.message = message self.layout = QtWidgets.QVBoxLayout(self) - self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok) + self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok) self.path = PathLineEdit(parent=self) self.init_ui() @@ -480,7 +480,7 @@ def create_colormap_pixmap(map_name: str): fig.canvas.draw() data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - image = QtGui.QImage(data, data.shape[1], data.shape[0], QtGui.QImage.Format_RGB888) + image = QtGui.QImage(data, data.shape[1], data.shape[0], QtGui.QImage.Format.Format_RGB888) pixmap = QtGui.QPixmap.fromImage(image) plt.close(fig) @@ -530,7 +530,7 @@ def init_ui(self): def copy_to_clipboard(self): cb = QtWidgets.QApplication.clipboard() - cb.clear(mode=cb.Clipboard) + cb.clear(mode=QtGui.QClipboard.Mode.Clipboard) cb.setText(self.text_edit.toPlainText()) self.copied_label.setText('Copied to clipboard') @@ -570,7 +570,7 @@ def paintEvent(self, event): self.setMinimumHeight((radius + self.BORDER) * 2) painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) painter.translate(center) painter.setBrush(QtGui.QColor("#cccccc")) @@ -584,7 +584,7 @@ def paintEvent(self, event): if not self.isChecked(): sw_rect.moveLeft(-width) painter.drawRoundedRect(sw_rect, radius, radius) - painter.drawText(sw_rect, QtCore.Qt.AlignCenter, label) + painter.drawText(sw_rect, QtCore.Qt.AlignmentFlag.AlignCenter, label) class ToggleSwitch(QtWidgets.QWidget): @@ -664,7 +664,7 @@ class HelpButton(QtWidgets.QToolButton): def __init__(self, parent=None): super().__init__(parent) - self.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MessageBoxQuestion)) + self.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxQuestion)) self.param_name = '' self.desc = '' @@ -679,7 +679,7 @@ def set_param_help(self, param_name: str, desc: str): def mouseReleaseEvent(self, event: QtGui.QMouseEvent): super().mouseReleaseEvent(event) - help_event = QtGui.QHelpEvent(QtCore.QEvent.Type.ToolTip, event.pos(), event.globalPos()) + help_event = QtGui.QHelpEvent(QtCore.QEvent.Type.ToolTip, event.pos(), event.globalPosition().toPoint()) self.event(help_event) @@ -752,7 +752,7 @@ def __init__(self, items: Sequence, icons: Sequence = None, parent=None): self.items = [] self.list_items = [] self.list = QtWidgets.QListWidget(self) - self.list.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + self.list.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection) self.layout.addWidget(self.list, 1, 1, 4, 1) self.select_all_button = QtWidgets.QPushButton('Select all', self) @@ -831,8 +831,8 @@ def delete_selected(self): def delete_all(self): accepted = QtWidgets.QMessageBox.question(self, f"{self.delete_text.capitalize()} all items?", f"Are you sure you want to {self.delete_text} all items?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) - if accepted == QtWidgets.QMessageBox.Yes: + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) + if accepted == QtWidgets.QMessageBox.StandardButton.Yes: for n_item in reversed(range(len(self.items))): self.itemDeleted.emit(n_item) self.delete_all_quietly() @@ -1055,7 +1055,7 @@ class MinMaxDialog(QtWidgets.QDialog): def __init__(self, parent=None): super().__init__(parent) - self.setWindowFlag(QtCore.Qt.WindowMinMaxButtonsHint) + self.setWindowFlag(QtCore.Qt.WindowType.WindowMinMaxButtonsHint) class TrueFalseBoth(QtWidgets.QWidget): @@ -1796,7 +1796,7 @@ def __init__(self, parent=None): @QtCore.pyqtSlot(str) def append_text(self, text: str): - self.moveCursor(QtGui.QTextCursor.End) + self.moveCursor(QtGui.QTextCursor.MoveOperation.End) if text == '\n': return text = text.replace("<", "<").replace(">", ">") @@ -1805,8 +1805,8 @@ def append_text(self, text: str): self.carriage = False diff = self.document().characterCount() - self.prev_coord cursor = self.textCursor() - cursor.movePosition(QtGui.QTextCursor.PreviousCharacter, QtGui.QTextCursor.MoveAnchor, n=diff) - cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) + cursor.movePosition(QtGui.QTextCursor.MoveOperation.PreviousCharacter, QtGui.QTextCursor.MoveMode.MoveAnchor, n=diff) + cursor.movePosition(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.removeSelectedText() if text.endswith('\r'): @@ -2180,7 +2180,7 @@ def __init__(self, *args, **kwargs): def contextMenu(self, value: str): self.context_menu = QtWidgets.QMenu(self) - copy_action = QtWidgets.QAction(f'Copy "{value}"') + copy_action = QtGui.QAction(f'Copy "{value}"') copy_action.triggered.connect(functools.partial(QtWidgets.QApplication.clipboard().setText, value)) self.context_menu.addAction(copy_action) @@ -2189,7 +2189,7 @@ def contextMenu(self, value: str): self.db_actions = [] for name in settings.get_databases_settings(): - action = QtWidgets.QAction(f'Search "{value}" on {name}') + action = QtGui.QAction(f'Search "{value}" on {name}') self.db_actions.append(action) open_url_partial = functools.partial(QtGui.QDesktopServices.openUrl, QtCore.QUrl(f'{databases[name]}{value}')) @@ -2225,7 +2225,7 @@ def __init__(self, *args, **kwargs): def contextMenu(self, value: str): self.context_menu = QtWidgets.QMenu(self) - copy_action = QtWidgets.QAction(f'Copy "{value}"') + copy_action = QtGui.QAction(f'Copy "{value}"') copy_action.triggered.connect(functools.partial(QtWidgets.QApplication.clipboard().setText, value)) self.context_menu.addAction(copy_action) @@ -2234,7 +2234,7 @@ def contextMenu(self, value: str): self.db_actions = [] for name in settings.get_databases_settings(): - action = QtWidgets.QAction(f'Search "{value}" on {name}') + action = QtGui.QAction(f'Search "{value}" on {name}') self.db_actions.append(action) open_url_partial = functools.partial(QtGui.QDesktopServices.openUrl, QtCore.QUrl(f'{databases[name]}{value}')) @@ -2268,7 +2268,7 @@ def __init__(self, *args, **kwargs): def contextMenu(self, value: str): self.context_menu = QtWidgets.QMenu(self) - copy_action = QtWidgets.QAction(f'Copy "{value}"') + copy_action = QtGui.QAction(f'Copy "{value}"') copy_action.triggered.connect(functools.partial(QtWidgets.QApplication.clipboard().setText, value)) self.context_menu.addAction(copy_action) diff --git a/rnalysis/gui/gui_windows.py b/rnalysis/gui/gui_windows.py index 0f2af4fdb..1a9539225 100644 --- a/rnalysis/gui/gui_windows.py +++ b/rnalysis/gui/gui_windows.py @@ -1,223 +1,26 @@ import functools -import itertools import json import time import traceback -import warnings from pathlib import Path -from queue import Queue from typing import Callable, Union, Tuple import polars as pl import yaml -from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt6 import QtCore, QtWidgets, QtGui from rnalysis import __version__ from rnalysis.gui import gui_style, gui_widgets from rnalysis.utils import settings, io, generic, parsing -class CheckableFileSystemModel(QtWidgets.QFileSystemModel): - checkStateChanged = QtCore.pyqtSignal(str, bool) - finishedDataChange = QtCore.pyqtSignal() - - def __init__(self): - super().__init__() - self.checkStates = {} - self.rowsInserted.connect(self.checkAdded) - self.rowsRemoved.connect(self.checkParent) - self.rowsAboutToBeRemoved.connect(self.checkRemoved) - - def checkState(self, index): - return self.checkStates.get(self.filePath(index), QtCore.Qt.Unchecked) - - def setCheckState(self, index, state, emitStateChange=True): - path = self.filePath(index) - if self.checkStates.get(path) == state: - return - self.checkStates[path] = state - if emitStateChange: - self.checkStateChanged.emit(path, bool(state)) - - def checkAdded(self, parent, first, last): - # if a file/directory is added, ensure it follows the parent state as long - # as the parent is already tracked; note that this happens also when - # expanding a directory that has not been previously loaded - if not parent.isValid(): - return - if self.filePath(parent) in self.checkStates: - state = self.checkState(parent) - for row in range(first, last + 1): - index = self.index(row, 0, parent) - path = self.filePath(index) - if path not in self.checkStates: - self.checkStates[path] = state - self.checkParent(parent) - - def checkRemoved(self, parent, first, last): - # remove items from the internal dictionary when a file is deleted; - # note that this *has* to happen *before* the model actually updates, - # that's the reason this function is connected to rowsAboutToBeRemoved - for row in range(first, last + 1): - path = self.filePath(self.index(row, 0, parent)) - if path in self.checkStates: - self.checkStates.pop(path) - - def checkParent(self, parent): - # verify the state of the parent according to the children states - if not parent.isValid(): - self.finishedDataChange.emit() - return - childStates = [self.checkState(self.index(r, 0, parent)) for r in range(self.rowCount(parent))] - newState = QtCore.Qt.Checked if all(childStates) else QtCore.Qt.Unchecked - oldState = self.checkState(parent) - if newState != oldState: - self.setCheckState(parent, newState) - self.dataChanged.emit(parent, parent) - self.checkParent(parent.parent()) - - def flags(self, index): - return super().flags(index) | QtCore.Qt.ItemIsUserCheckable - - def data(self, index, role=QtCore.Qt.DisplayRole): - if role == QtCore.Qt.CheckStateRole and index.column() == 0: - return self.checkState(index) - return super().data(index, role) - - def setData(self, index, value, role, checkParent=True, emitStateChange=True): - if role == QtCore.Qt.CheckStateRole and index.column() == 0: - self.setCheckState(index, value, emitStateChange) - for row in range(self.rowCount(index)): - # set the data for the children, but do not emit the state change, - # and don't check the parent state (to avoid recursion) - self.setData(index.child(row, 0), value, QtCore.Qt.CheckStateRole, - checkParent=False, emitStateChange=False) - self.dataChanged.emit(index, index) - if checkParent: - self.checkParent(index.parent()) - return True - - return super().setData(index, value, role) - - -class FilterProxy(QtCore.QSortFilterProxyModel): - ''' - Based on StackOverflow answer by user ekhumoro: - https://stackoverflow.com/questions/72587813/how-to-filter-by-no-extension-in-qfilesystemmodel - ''' - - def __init__(self, disables=False, parent=None): - super().__init__(parent) - self._disables = bool(disables) - - def filterAcceptsRow(self, row, parent): - index = self.sourceModel().index(row, 0, parent) - if not self._disables: - return self.matchIndex(index) - return index.isValid() - - def matchIndex(self, index): - return (super().filterAcceptsRow(index.row(), index.parent())) - - def flags(self, index): - flags = super().flags(index) - if (self._disables and - not self.matchIndex(self.mapToSource(index))): - flags &= ~QtCore.Qt.ItemIsEnabled - return flags - - -class MultiFileSelectionDialog(gui_widgets.MinMaxDialog): - ''' - Based on a Stack Overflow answer by user 'musicamante': - https://stackoverflow.com/questions/63309406/qfilesystemmodel-with-checkboxes - ''' - - def __init__(self): - super().__init__() - self.layout = QtWidgets.QGridLayout(self) - self.tree_mycomputer = QtWidgets.QTreeView() - self.tree_home = QtWidgets.QTreeView() - self.open_button = QtWidgets.QPushButton('Open') - self.cancel_button = QtWidgets.QPushButton('Cancel') - self.logger = QtWidgets.QPlainTextEdit() - self.init_models() - self.init_ui() - - def init_models(self): - model_mycomputer = CheckableFileSystemModel() - model_mycomputer.setRootPath('') - model_home = CheckableFileSystemModel() - model_home.setRootPath('.') - proxy = FilterProxy(False, self) - proxy.setFilterRegularExpression(r'^(?![.])(?!.*[-_.]$).+') - proxy.setSourceModel(model_home) - self.tree_mycomputer.setModel(model_mycomputer) - self.tree_mycomputer.setRootIndex(model_mycomputer.index(model_mycomputer.myComputer())) - self.tree_home.setModel(proxy) - self.tree_home.setRootIndex(proxy.mapFromSource( - model_home.index(QtCore.QStandardPaths.standardLocations(QtCore.QStandardPaths.HomeLocation)[0]))) - model_mycomputer.finishedDataChange.connect(self.update_log) - model_home.finishedDataChange.connect(self.update_log) - - def init_ui(self): - self.setWindowTitle('Choose files:') - self.resize(1000, 750) - - self.tree_mycomputer.setSortingEnabled(True) - self.tree_home.setSortingEnabled(True) - self.tree_mycomputer.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) - self.tree_home.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) - - self.layout.addWidget(self.tree_mycomputer, 0, 0, 4, 8) - self.layout.addWidget(self.tree_home, 4, 0, 4, 8) - self.layout.setRowStretch(3, 2) - self.layout.setRowStretch(7, 2) - - self.layout.addWidget(self.open_button, 8, 7) - self.layout.addWidget(self.cancel_button, 9, 7) - - self.layout.addWidget(self.logger, 8, 0, 2, 7) - self.logger.setReadOnly(True) - - self.open_button.clicked.connect(self.accept) - self.cancel_button.clicked.connect(self.reject) - - self.update_log() - - def update_log(self): - self.logger.setPlainText("\n".join(self.result())) - self.logger.verticalScrollBar().setValue( - self.logger.verticalScrollBar().maximum()) - - def result(self): - files_to_open = [] - queue = Queue() - for pth, state in itertools.chain(self.tree_mycomputer.model().checkStates.items(), - self.tree_home.model().sourceModel().checkStates.items()): - if state: - queue.put(pth) - - while not queue.empty(): - this_path = Path(queue.get()) - if this_path.is_file(): - files_to_open.append(str(this_path)) - else: - try: - for item in this_path.iterdir(): - queue.put(item) - except PermissionError: - warnings.warn(f'Cannot access items under {this_path} - permission denied. ') - return files_to_open - - class DataFrameModel(QtCore.QAbstractTableModel): """ Based upon: https://stackoverflow.com/a/44605011 """ - DtypeRole = QtCore.Qt.UserRole + 1000 - ValueRole = QtCore.Qt.UserRole + 1001 + DtypeRole = QtCore.Qt.ItemDataRole.UserRole + 1000 + ValueRole = QtCore.Qt.ItemDataRole.UserRole + 1001 def __init__(self, df=pl.DataFrame(), parent=None): super().__init__(parent) @@ -236,9 +39,10 @@ def dataFrame(self): dataFrame = QtCore.pyqtProperty(pl.DataFrame, fget=dataFrame, fset=setDataFrame) @QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str) - def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: + def headerData(self, section: int, orientation: QtCore.Qt.Orientation, + role: int = QtCore.Qt.ItemDataRole.DisplayRole): + if role == QtCore.Qt.ItemDataRole.DisplayRole: + if orientation == QtCore.Qt.Orientation.Horizontal: return self._dataframe.columns[section + 1] else: return str(self._dataframe.row(section)[0]) @@ -254,7 +58,7 @@ def columnCount(self, parent=QtCore.QModelIndex()): return 0 return max(0, self._dataframe.width - 1) - def data(self, index, role=QtCore.Qt.DisplayRole): + def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): if not index.isValid() or not (0 <= index.row() < self.rowCount() and 0 <= index.column() < self.columnCount()): return QtCore.QVariant() @@ -267,7 +71,7 @@ def data(self, index, role=QtCore.Qt.DisplayRole): val = self._dataframe[row, col] except IndexError: print(row, col, self._dataframe.shape) - if role == QtCore.Qt.DisplayRole: + if role == QtCore.Qt.ItemDataRole.DisplayRole: return str(val) elif role == DataFrameModel.ValueRole: return val @@ -277,7 +81,7 @@ def data(self, index, role=QtCore.Qt.DisplayRole): def roleNames(self): roles = { - QtCore.Qt.DisplayRole: b'display', + QtCore.Qt.ItemDataRole.DisplayRole: b'display', DataFrameModel.DtypeRole: b'dtype', DataFrameModel.ValueRole: b'value' } @@ -409,7 +213,7 @@ def __init__(self, exc_type, exc_value, exc_tb, parent=None): def init_ui(self): self.setWindowTitle("Error") - self.setWindowIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MessageBoxCritical)) + self.setWindowIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical)) self.widgets['error_label'] = QtWidgets.QLabel('RNAlysis has encountered the following error:') self.layout.addWidget(self.widgets['error_label']) @@ -442,8 +246,8 @@ def init_ui(self): def copy_to_clipboard(self): cb = QtWidgets.QApplication.clipboard() - cb.clear(mode=cb.Clipboard) - cb.setText("".join(traceback.format_exception(*self.exception)), mode=cb.Clipboard) + cb.clear(mode=QtGui.QClipboard.Mode.Clipboard) + cb.setText("".join(traceback.format_exception(*self.exception)), mode=QtGui.QClipboard.Mode.Clipboard) self.widgets['copied_label'].setText('Copied to clipboard') @@ -464,12 +268,12 @@ def __init__(self, parent=None): self.text.setTextFormat(QtCore.Qt.TextFormat.MarkdownText) self.text.setText(text) self.text.setWordWrap(True) - self.scroll_layout.addWidget( self.text) + self.scroll_layout.addWidget(self.text) self.layout().addWidget(self.scroll, 0, 0, 1, self.layout().columnCount()) self.setWindowTitle(f"What's new in version {__version__}") self.setStyleSheet("QScrollArea{min-width:900 px; min-height: 600px}" "QScrollBar:vertical {width: 40;}") - self.setStandardButtons(QtWidgets.QMessageBox.Ok) + self.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) self.buttonClicked.connect(self.close) @@ -490,7 +294,7 @@ def __init__(self, parent=None):
""" self.setText(text) self.setWindowTitle("About RNAlysis") - self.setStandardButtons(QtWidgets.QMessageBox.Ok) + self.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) self.buttonClicked.connect(self.close) @@ -516,8 +320,8 @@ def __init__(self, parent=None): self.tables_widgets = {} self.button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel | - QtWidgets.QDialogButtonBox.Apply | QtWidgets.QDialogButtonBox.RestoreDefaults) + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel | + QtWidgets.QDialogButtonBox.StandardButton.Apply | QtWidgets.QDialogButtonBox.StandardButton.RestoreDefaults) self.layout.addWidget(self.appearance_group) self.layout.addWidget(self.tables_group) @@ -552,7 +356,7 @@ def set_choices(self): for i in range(self.appearance_widgets['databases'].count()): item = self.appearance_widgets['databases'].item(i) if item.text() in current_dbs: - item.setCheckState(QtCore.Qt.Checked) + item.setCheckState(QtCore.Qt.CheckState.Checked) attr_ref_path = settings.get_attr_ref_path('predefined') if settings.is_setting_in_file( settings.__attr_file_key__) else 'No file chosen' @@ -567,10 +371,11 @@ def init_appearance_ui(self): self.appearance_widgets['app_theme'].addItems(self.THEMES.keys()) self.appearance_widgets['app_font'] = QtWidgets.QFontComboBox(self.appearance_group) - self.appearance_widgets['app_font'].setFontFilters(QtWidgets.QFontComboBox.ScalableFonts) + self.appearance_widgets['app_font'].setFontFilters(QtWidgets.QFontComboBox.FontFilter.ScalableFonts) self.appearance_widgets['app_font'].setEditable(True) - self.appearance_widgets['app_font'].completer().setCompletionMode(QtWidgets.QCompleter.PopupCompletion) - self.appearance_widgets['app_font'].setInsertPolicy(QtWidgets.QComboBox.NoInsert) + self.appearance_widgets['app_font'].completer().setCompletionMode( + QtWidgets.QCompleter.CompletionMode.PopupCompletion) + self.appearance_widgets['app_font'].setInsertPolicy(QtWidgets.QComboBox.InsertPolicy.NoInsert) self.appearance_widgets['app_font_size'] = QtWidgets.QComboBox(self.appearance_group) self.appearance_widgets['app_font_size'].addItems(self.FONT_SIZES) @@ -579,13 +384,13 @@ def init_appearance_ui(self): with open(self.LOOKUP_DATABASES_PATH) as f: for key in json.load(f).keys(): item = QtWidgets.QListWidgetItem(key) - item.setCheckState(QtCore.Qt.Unchecked) + item.setCheckState(QtCore.Qt.CheckState.Unchecked) self.appearance_widgets['databases'].addItem(item) self.appearance_widgets['show_tutorial'] = QtWidgets.QCheckBox("Show tutorial page on startup") self.appearance_widgets['report_gen'] = QtWidgets.QComboBox() self.appearance_widgets['report_gen'].addItems(self.REPORT_GEN_OPTIONS.keys()) - self.appearance_widgets['report_gen'].setInsertPolicy(QtWidgets.QComboBox.NoInsert) + self.appearance_widgets['report_gen'].setInsertPolicy(QtWidgets.QComboBox.InsertPolicy.NoInsert) for widget_name in ['app_theme', 'app_font', 'app_font_size', 'report_gen']: self.appearance_widgets[widget_name].currentIndexChanged.connect(self._trigger_settings_changed) @@ -659,9 +464,9 @@ def init_buttons(self): def handle_button_click(self, button): role = self.button_box.buttonRole(button) - if role == QtWidgets.QDialogButtonBox.ApplyRole: + if role == QtWidgets.QDialogButtonBox.ButtonRole.ApplyRole: self.save_settings() - elif role == QtWidgets.QDialogButtonBox.ResetRole: + elif role == QtWidgets.QDialogButtonBox.ButtonRole.ResetRole: self.reset_settings() def closeEvent(self, event): @@ -670,8 +475,9 @@ def closeEvent(self, event): quit_msg = "Are you sure you want to close settings without saving?" reply = QtWidgets.QMessageBox.question(self, 'Close settings without saving?', - quit_msg, QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) - to_exit = reply == QtWidgets.QMessageBox.Yes + quit_msg, QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes) + to_exit = reply == QtWidgets.QMessageBox.StandardButton.Yes if to_exit: event.accept() @@ -681,49 +487,49 @@ def closeEvent(self, event): class HowToCiteWindow(gui_widgets.MinMaxDialog): CITATION_RNALYSIS = """ - Teichman, G., Cohen, D., Ganon, O., Dunsky, N., Shani, S., Gingold, H., and Rechavi, O. (2022). - RNAlysis: analyze your RNA sequencing data without writing a single line of code. BioRxiv 2022.11.25.517851. -
- Eden, E., Lipson, D., Yogev, S., and Yakhini, Z. (2007).
- Discovering Motifs in Ranked Lists of DNA Sequences. PLOS Comput. Biol. 3, e39.
-
- doi.org/10.1371/journal.pcbi.0030039
-
- Wagner, F. (2017). The XL-mHG test for gene set enrichment. ArXiv.
-
- doi.org/10.48550/arXiv.1507.07905
-
+ Eden, E., Lipson, D., Yogev, S., and Yakhini, Z. (2007).
+ Discovering Motifs in Ranked Lists of DNA Sequences. PLOS Comput. Biol. 3, e39.
+
+ doi.org/10.1371/journal.pcbi.0030039
+
+ Wagner, F. (2017). The XL-mHG test for gene set enrichment. ArXiv.
+
+ doi.org/10.48550/arXiv.1507.07905
+