From 3c1f76d87c4ec31f962b7b45f9198dd0ea597f2c Mon Sep 17 00:00:00 2001 From: Caleb Weinreb Date: Sat, 29 Jun 2024 13:23:21 -0400 Subject: [PATCH] refactored context menu and added menu for tab label --- snub/gui/main.py | 42 +++++++++++++++++++++++++-- snub/gui/panels/roi.py | 27 +++-------------- snub/gui/panels/scatter.py | 56 ++++++++++++++---------------------- snub/gui/tracks/annotator.py | 41 ++++++++------------------ snub/gui/tracks/heatmap.py | 56 ++++++++++++------------------------ snub/gui/tracks/trace.py | 39 ++++--------------------- snub/gui/utils/__init__.py | 1 + snub/gui/utils/widgets.py | 38 +++++++++++++++++++++++- 8 files changed, 139 insertions(+), 161 deletions(-) diff --git a/snub/gui/main.py b/snub/gui/main.py index c6e5163..121e422 100644 --- a/snub/gui/main.py +++ b/snub/gui/main.py @@ -4,7 +4,7 @@ import sys, os, json import numpy as np from functools import partial -from snub.gui.utils import IntervalIndex, CheckBox +from snub.gui.utils import IntervalIndex, CheckBox, CustomContextMenu from snub.gui.stacks import PanelStack, TrackStack from snub.gui.tracks import TracePlot from snub.gui.help import HelpMenu @@ -60,6 +60,7 @@ def __init__(self, project_directory): super().__init__() # load config self.project_directory = project_directory + self.name = project_directory.strip(os.path.sep).split(os.path.sep)[-1] self.layout_mode = None config_path = os.path.join(self.project_directory, "config.json") config = json.load(open(config_path, "r")) @@ -320,6 +321,41 @@ def pause(self): self.play_button.setIcon(self.play_icon) self.playing = False + def copy_tab_name(self): + clipboard = QApplication.clipboard() + clipboard.setText(self.name) + + +class CustomTabBar(QTabBar): + def __init__(self, parent=None): + super(CustomTabBar, self).__init__(parent) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.showContextMenu) + + def mousePressEvent(self, event): + if event.button() == Qt.RightButton: + self.showContextMenu(event.pos()) + else: + super(CustomTabBar, self).mousePressEvent(event) + + def showContextMenu(self, pos): + global_pos = self.mapToGlobal(pos) + index = self.tabAt(pos) + if index != -1: + contextMenu = CustomContextMenu(self) + close_action = contextMenu.add_item( + "Close Tab", lambda: self.parent().removeTab(index) + ) + copy_action = contextMenu.add_item( + "Copy Name", lambda: self.copyTabName(index) + ) + contextMenu.exec_(global_pos) + + def copyTabName(self, index): + clipboard = QApplication.clipboard() + tab_name = self.tabText(index) + clipboard.setText(tab_name) + class MainWindow(QMainWindow): """ @@ -330,6 +366,7 @@ class MainWindow(QMainWindow): def __init__(self, args): super().__init__() self.tabs = QTabWidget() + self.tabs.setTabBar(CustomTabBar(self.tabs)) self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self.close_tab) self.tabs.currentChanged.connect(self.tab_changed) @@ -435,9 +472,8 @@ def reload_data(self): self.load_project(project_dir) def load_project(self, project_directory): - name = project_directory.strip(os.path.sep).split(os.path.sep)[-1] project_tab = ProjectTab(project_directory) - self.tabs.addTab(project_tab, name) + self.tabs.addTab(project_tab, project_tab.name) self.tabs.setCurrentWidget(project_tab) def getExistingDirectories(self): diff --git a/snub/gui/panels/roi.py b/snub/gui/panels/roi.py index 880ee50..8b61b24 100644 --- a/snub/gui/panels/roi.py +++ b/snub/gui/panels/roi.py @@ -11,7 +11,7 @@ from vispy.scene.visuals import Image, Line from snub.gui.panels import Panel -from snub.gui.utils import HeaderMixin, AdjustColormapDialog +from snub.gui.utils import HeaderMixin, AdjustColormapDialog, CustomContextMenu from snub.io.project import _random_color @@ -161,27 +161,8 @@ def mouse_release(self, event): self.context_menu(event) def context_menu(self, event): - contextMenu = QMenu(self) - - def add_menu_item(name, slot, item_type="label"): - action = QWidgetAction(self) - if item_type == "checkbox": - widget = QCheckBox(name) - widget.stateChanged.connect(slot) - elif item_type == "label": - widget = QLabel(name) - action.triggered.connect(slot) - action.setDefaultWidget(widget) - contextMenu.addAction(action) - return widget - - # click to show adjust colormap range dialog - label = add_menu_item("Adjust colormap range", self.show_adjust_colormap_dialog) - - contextMenu.setStyleSheet( - """ - QMenu::item, QLabel, QCheckBox { background-color : #3E3E3E; padding: 5px 6px 5px 6px;} - QMenu::item:selected, QLabel:hover, QCheckBox:hover { background-color: #999999;} - QMenu::separator { background-color: rgb(20,20,20);} """ + contextMenu = CustomContextMenu(self) + label = contextMenu.add_item( + "Adjust colormap range", self.show_adjust_colormap_dialog ) action = contextMenu.exec_(event.native.globalPos()) diff --git a/snub/gui/panels/scatter.py b/snub/gui/panels/scatter.py index 995a601..d27c976 100644 --- a/snub/gui/panels/scatter.py +++ b/snub/gui/panels/scatter.py @@ -17,6 +17,7 @@ IntervalIndex, UNCHECKED_ICON_PATH, CHECKED_ICON_PATH, + CustomContextMenu, ) @@ -191,38 +192,30 @@ def update_current_time(self, t): self.current_node_marker.parent = None def context_menu(self, event): - contextMenu = QMenu(self) - - def add_menu_item(name, slot, item_type="label"): - action = QWidgetAction(self) - if item_type == "checkbox": - widget = QCheckBox(name) - widget.stateChanged.connect(slot) - elif item_type == "label": - widget = QLabel(name) - action.triggered.connect(slot) - action.setDefaultWidget(widget) - contextMenu.addAction(action) - return widget + contextMenu = CustomContextMenu(self) # show/hide variable menu if self.variable_menu.isVisible(): - add_menu_item("Hide variables menu", self.hide_variable_menu) + contextMenu.add_item("Hide variables menu", self.hide_variable_menu) else: - add_menu_item("Show variables menu", self.show_variable_menu) + contextMenu.add_item("Show variables menu", self.show_variable_menu) # get enriched variables (only available is nodes are selected) - label = add_menu_item( - "Sort variables by enrichment", self.get_enriched_variables + label = contextMenu.add_item( + "Sort variables by enrichment", + self.get_enriched_variables, ) if self.is_selected.sum() == 0: label.setStyleSheet("QLabel { color: rgb(120,120,120); }") - add_menu_item("Restore original variable order", self.show_variable_menu) + contextMenu.add_item( + "Restore original variable order", + self.show_variable_menu, + ) contextMenu.addSeparator() # toggle whether to plot high-variable-val nodes on top - checkbox = add_menu_item( + checkbox = contextMenu.add_item( "Plot high values on top", self.toggle_sort_by_color_value, item_type="checkbox", @@ -234,25 +227,20 @@ def add_menu_item(name, slot, item_type="label"): contextMenu.addSeparator() # click to show adjust colormap range dialog - add_menu_item("Adjust colormap range", self.show_adjust_colormap_dialog) - add_menu_item("Adjust marker appearance", self.show_adjust_marker_dialog) + contextMenu.add_item("Adjust colormap range", self.show_adjust_colormap_dialog) + contextMenu.add_item("Adjust marker appearance", self.show_adjust_marker_dialog) contextMenu.addSeparator() if self.show_marker_trail: - add_menu_item("Hide marker trail", partial(self.toggle_marker_trail, False)) + contextMenu.add_item( + "Hide marker trail", + partial(self.toggle_marker_trail, False), + ) else: - add_menu_item("Show marker trail", partial(self.toggle_marker_trail, True)) - - contextMenu.setStyleSheet( - f""" - QMenu::item, QLabel, QCheckBox {{ background-color : #3e3e3e; padding: 5px 6px 5px 6px;}} - QMenu::item:selected, QLabel:hover, QCheckBox:hover {{ background-color: #999999;}} - QMenu::separator {{ background-color: rgb(20,20,20);}} - QCheckBox::indicator:unchecked {{ image: url({UNCHECKED_ICON_PATH}); }} - QCheckBox::indicator:checked {{ image: url({CHECKED_ICON_PATH}); }} - QCheckBox::indicator {{ width: 14px; height: 14px;}} - """ - ) + contextMenu.add_item( + "Show marker trail", + partial(self.toggle_marker_trail, True), + ) action = contextMenu.exec_(event.native.globalPos()) def toggle_marker_trail(self, visibility): diff --git a/snub/gui/tracks/annotator.py b/snub/gui/tracks/annotator.py index 3cf8426..e03fbf7 100644 --- a/snub/gui/tracks/annotator.py +++ b/snub/gui/tracks/annotator.py @@ -4,7 +4,12 @@ import numpy as np import json from snub.gui.tracks import Track, TrackGroup, position_to_time -from snub.gui.utils import IntervalIndex, CHECKED_ICON_PATH, UNCHECKED_ICON_PATH +from snub.gui.utils import ( + IntervalIndex, + CHECKED_ICON_PATH, + UNCHECKED_ICON_PATH, + CustomContextMenu, +) class AnnotatorLabels(QWidget): @@ -222,22 +227,10 @@ def _load(self, file_name): self.update() def contextMenuEvent(self, event): - contextMenu = QMenu(self) - - def add_menu_item(name, slot, item_type="label"): - action = QWidgetAction(self) - if item_type == "checkbox": - widget = QCheckBox(name) - widget.stateChanged.connect(slot) - elif item_type == "label": - widget = QLabel(name) - action.triggered.connect(slot) - action.setDefaultWidget(widget) - contextMenu.addAction(action) - return widget + contextMenu = CustomContextMenu(self) # toggle autosave - checkbox = add_menu_item( + checkbox = contextMenu.add_item( "Automatically save", self.toggle_autosave, item_type="checkbox", @@ -248,7 +241,7 @@ def add_menu_item(name, slot, item_type="label"): checkbox.setChecked(False) # toggle update_time_on_drag - checkbox = add_menu_item( + checkbox = contextMenu.add_item( "Update time on drag", self.toggle_update_time_on_drag, item_type="checkbox", @@ -259,19 +252,9 @@ def add_menu_item(name, slot, item_type="label"): checkbox.setChecked(False) # import/export annotations - add_menu_item("Export annotations", self.export_annotations) - add_menu_item("Import annotations", self.import_annotations) - - contextMenu.setStyleSheet( - f""" - QMenu::item, QLabel, QCheckBox {{ background-color : #3e3e3e; padding: 5px 6px 5px 6px;}} - QMenu::item:selected, QLabel:hover, QCheckBox:hover {{ background-color: #999999;}} - QMenu::separator {{ background-color: rgb(20,20,20);}} - QCheckBox::indicator:unchecked {{ image: url({UNCHECKED_ICON_PATH}); }} - QCheckBox::indicator:checked {{ image: url({CHECKED_ICON_PATH}); }} - QCheckBox::indicator {{ width: 14px; height: 14px;}} - """ - ) + contextMenu.add_item("Export annotations", self.export_annotations) + contextMenu.add_item("Import annotations", self.import_annotations) + action = contextMenu.exec_(self.mapToGlobal(event.pos())) def toggle_autosave(self, state): diff --git a/snub/gui/tracks/heatmap.py b/snub/gui/tracks/heatmap.py index 5bdc1ac..1d138e9 100644 --- a/snub/gui/tracks/heatmap.py +++ b/snub/gui/tracks/heatmap.py @@ -8,7 +8,12 @@ from numba import njit, prange from snub.gui.tracks import Track, TracePlot, TrackGroup -from snub.gui.utils import AdjustColormapDialog, CHECKED_ICON_PATH, UNCHECKED_ICON_PATH +from snub.gui.utils import ( + AdjustColormapDialog, + CHECKED_ICON_PATH, + UNCHECKED_ICON_PATH, + CustomContextMenu, +) from snub.io.project import _random_color @@ -310,24 +315,7 @@ def update_current_range(self, current_range): self.heatmap_image.update_current_range(current_range) def contextMenuEvent(self, event): - contextMenu = QMenu(self) - - def add_menu_item(name, slot, item_type="label"): - action = QWidgetAction(self) - if item_type == "checkbox": - widget = QCheckBox(name) - widget.stateChanged.connect(slot) - elif item_type == "button": - widget = QPushButton(name) - widget.clicked.connect(slot) - elif item_type == "label": - widget = QLabel(name) - action.triggered.connect(slot) - else: - return - action.setDefaultWidget(widget) - contextMenu.addAction(action) - return widget + contextMenu = CustomContextMenu(self) # used to get row label and for zooming y = ( @@ -341,45 +329,37 @@ def add_menu_item(name, slot, item_type="label"): if self.add_traceplot: row_label = self.labels[self.row_order[int(y)]] display_trace_slot = lambda: self.display_trace_signal.emit(row_label) - add_menu_item("Plot trace: {}".format(row_label), display_trace_slot) + contextMenu.add_item( + "Plot trace: {}".format(row_label), + display_trace_slot, + ) # show adjust colormap dialog - add_menu_item("Adjust colormap range", self.show_adjust_colormap_dialog) + contextMenu.add_item("Adjust colormap range", self.show_adjust_colormap_dialog) contextMenu.addSeparator() if self.heatmap_labels.isVisible(): - add_menu_item("Hide row labels", self.hide_labels) + contextMenu.add_item("Hide row labels", self.hide_labels) else: - add_menu_item("Show row labels", self.show_labels) + contextMenu.add_item("Show row labels", self.show_labels) contextMenu.addSeparator() # for reordering rows - add_menu_item("Reorder by selection", self.reorder_by_selection) - add_menu_item("Restore original order", self.restore_original_order) + contextMenu.add_item("Reorder by selection", self.reorder_by_selection) + contextMenu.add_item("Restore original order", self.restore_original_order) contextMenu.addSeparator() # for changing vertical range - add_menu_item( + contextMenu.add_item( "Zoom in (vertical)", partial(self.zoom_vertical, y, 2 / 3), item_type="button", ) - add_menu_item( + contextMenu.add_item( "Zoom out (vertical)", partial(self.zoom_vertical, y, 3 / 2), item_type="button", ) - - contextMenu.setStyleSheet( - f""" - QMenu::item, QLabel, QCheckBox {{ background-color : #3e3e3e; padding: 5px 6px 5px 6px;}} - QMenu::item:selected, QLabel:hover, QCheckBox:hover {{ background-color: #999999;}} - QMenu::separator {{ background-color: rgb(20,20,20);}} - QCheckBox::indicator:unchecked {{ image: url({UNCHECKED_ICON_PATH}); }} - QCheckBox::indicator:checked {{ image: url({CHECKED_ICON_PATH}); }} - QCheckBox::indicator {{ width: 14px; height: 14px;}} - """ - ) action = contextMenu.exec_(self.mapToGlobal(event.pos())) def show_labels(self): diff --git a/snub/gui/tracks/trace.py b/snub/gui/tracks/trace.py index 23da8ab..630452a 100644 --- a/snub/gui/tracks/trace.py +++ b/snub/gui/tracks/trace.py @@ -8,7 +8,7 @@ from snub.gui.tracks import Track, TrackGroup from snub.io.project import _random_color -from snub.gui.utils import CHECKED_ICON_PATH, UNCHECKED_ICON_PATH +from snub.gui.utils import CHECKED_ICON_PATH, UNCHECKED_ICON_PATH, CustomContextMenu class CheckableComboBox(QComboBox): @@ -214,21 +214,8 @@ def bind_rois(self, roiplot): self.visible_traces_signal.emit(self.visible_traces) def contextMenuEvent(self, event): - contextMenu = QMenu(self) - - def add_menu_item(name, slot, item_type="label"): - action = QWidgetAction(self) - if item_type == "checkbox": - widget = QCheckBox(name) - widget.stateChanged.connect(slot) - elif item_type == "label": - widget = QLabel(name) - action.triggered.connect(slot) - action.setDefaultWidget(widget) - contextMenu.addAction(action) - return widget - - checkbox = add_menu_item( + contextMenu = CustomContextMenu(self) + checkbox = contextMenu.add_item( "Automatic y-axis limits", self.toggle_auto_yaxis_limits, item_type="checkbox", @@ -238,25 +225,11 @@ def add_menu_item(name, slot, item_type="label"): else: checkbox.setChecked(False) - add_menu_item("Adjust y-axis limits", self.show_adjust_yaxis_dialog) + contextMenu.add_item("Adjust y-axis limits", self.show_adjust_yaxis_dialog) contextMenu.addSeparator() - - add_menu_item("Adjust line width", self.show_adjust_linewidth_dialog) + contextMenu.add_item("Adjust line width", self.show_adjust_linewidth_dialog) contextMenu.addSeparator() - - add_menu_item("Hide all traces", self.clear) - - contextMenu.setStyleSheet( - f""" - QMenu::item, QLabel, QCheckBox {{ background-color : #3e3e3e; padding: 5px 6px 5px 6px;}} - QMenu::item:selected, QLabel:hover, QCheckBox:hover {{ background-color: #999999;}} - QMenu::separator {{ background-color: rgb(20,20,20);}} - QCheckBox::indicator:unchecked {{ image: url({UNCHECKED_ICON_PATH}); }} - QCheckBox::indicator:checked {{ image: url({CHECKED_ICON_PATH}); }} - QCheckBox::indicator {{ width: 14px; height: 14px;}} - """ - ) - + contextMenu.add_item("Hide all traces", self.clear) action = contextMenu.exec_(self.mapToGlobal(event.pos())) def show_adjust_yaxis_dialog(self): diff --git a/snub/gui/utils/__init__.py b/snub/gui/utils/__init__.py index db84563..d494892 100644 --- a/snub/gui/utils/__init__.py +++ b/snub/gui/utils/__init__.py @@ -5,4 +5,5 @@ CheckBox, CHECKED_ICON_PATH, UNCHECKED_ICON_PATH, + CustomContextMenu, ) diff --git a/snub/gui/utils/widgets.py b/snub/gui/utils/widgets.py index 682cb49..b4e75b3 100644 --- a/snub/gui/utils/widgets.py +++ b/snub/gui/utils/widgets.py @@ -62,7 +62,7 @@ def initUI( header_height=20, orientation="horizontal", min_size_at_show=100, - **kwargs + **kwargs, ): self.name = name self.saved_size = initial_saved_size @@ -212,3 +212,39 @@ def update_icon(self): self.setIcon(self.checked_icon) else: self.setIcon(self.unchecked_icon) + + +class CustomContextMenu(QMenu): + def __init__(self, parent): + super().__init__(parent) + self.parent = parent + self.initUI() + + def add_item(self, name, slot, item_type="label"): + action = QWidgetAction(self.parent) + if item_type == "checkbox": + widget = QCheckBox(name) + widget.stateChanged.connect(slot) + elif item_type == "button": + widget = QPushButton(name) + widget.clicked.connect(slot) + elif item_type == "label": + widget = QLabel(name) + action.triggered.connect(slot) + else: + return + action.setDefaultWidget(widget) + self.addAction(action) + return widget + + def initUI(self): + self.setStyleSheet( + f""" + QMenu::item, QLabel, QCheckBox {{ background-color : #3e3e3e; padding: 5px 6px 5px 6px;}} + QMenu::item:selected, QLabel:hover, QCheckBox:hover {{ background-color: #999999;}} + QMenu::separator {{ background-color: rgb(20,20,20);}} + QCheckBox::indicator:unchecked {{ image: url({UNCHECKED_ICON_PATH}); }} + QCheckBox::indicator:checked {{ image: url({CHECKED_ICON_PATH}); }} + QCheckBox::indicator {{ width: 14px; height: 14px;}} + """ + )