Source code for schrodinger.application.msv.gui.tab_widget

from functools import partial
from itertools import zip_longest

from schrodinger.application.msv import command
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui import msv_widget
from schrodinger.application.msv.gui import stylesheets
from schrodinger.application.msv.gui.gui_models import MsvGuiModel
from schrodinger.infra import util
from schrodinger.models import diffy
from schrodinger.models import mappers
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.utils import suppress_signals
from schrodinger.utils.scollections import IdSet


[docs]class NewTabNameContexMenu(QtWidgets.QMenu): """ Simple context menu containing a QLineEdit for changing a tab's name. :ivar renameTab: Signal emitted with the index and name of the tab to be renamed. :vartype renameTab: `QtCore.pyqtSignal` """ renameTab = QtCore.pyqtSignal(int, str)
[docs] def __init__(self, parent=None): super().__init__(parent) self.pos = None self.tab_idx = None self.rename_le = QtWidgets.QLineEdit() self.rename_act = QtWidgets.QWidgetAction(self) self.rename_act.setDefaultWidget(self.rename_le) self.addAction(self.rename_act) self.rename_le.editingFinished.connect(self.onTabNameEdited)
[docs] def setTab(self, tab_idx, tab_name): """ :param tab_idx: Current tab index :type tab_idx: int :param tab_name: Current tab name :type tab_name: str """ self.tab_idx = tab_idx self.rename_le.setText(tab_name)
[docs] def popup(self): super().popup(self.pos) self.rename_le.setFocus() self.rename_le.selectAll()
[docs] def onTabNameEdited(self): """ Emits the renameTab signal if a valid new tab name was entered. """ if self.tab_idx is None: return text = self.rename_le.text() if text.strip(): self.renameTab.emit(self.tab_idx, text) self.hide()
[docs]class TabLabelContextMenu(QtWidgets.QMenu): """ Context menu for the tab label. Only shown when a label is clicked on. Note that setIndex must be called before the menu is shown so that the information about the tab to be operated on is correctly emitted. :ivar duplicateTab: A signal emitted with the index of the tab to duplicate :vartype duplicateTab: `QtCore.pyqtSignal` :ivar renameTab: A signal emitted with the index of the tab to renamed :type renameTab: `QtCore.pyqtSignal` """ duplicateTab = QtCore.pyqtSignal(int) renameTab = QtCore.pyqtSignal(int)
[docs] def __init__(self, parent=None): super().__init__(parent) self.setStyleSheet(stylesheets.MENU) self.setToolTipsVisible(True) self.tab_index = None self.duplicate_tab = self.addAction("Duplicate") self.duplicate_tab.triggered.connect(self.onDuplicateTabRequested) self.rename_tab = self.addAction("Rename") self.rename_tab.triggered.connect(self.onRenameTabRequested)
[docs] def onRenameTabRequested(self): """ Emits the renameTab signal with the current index """ self.renameTab.emit(self.tab_index)
[docs] def onDuplicateTabRequested(self): """ Emits the duplicateTab signals with the current index """ self.duplicateTab.emit(self.tab_index)
[docs] def setIndex(self, index): """ Set the index on the context menu so we can track which tab should be operated on :param index: The index of the tab to operate on :type index: int """ self.tab_index = index
[docs]class TabBarWithLockableLeftTab(QtWidgets.QTabBar): """ Custom tab bar with the ability to lock the leftmost tab (e.g. the Workspace tab in Maestro). The locked tab cannot be closed, moved, or renamed. :ivar renameTabRequested: Signal emitted to request renaming a tab. Emitted with the index of the tab and the new name. :ivar duplicateTabRequested: Signal emitted to request duplicating a tab. Emitted with the index of the tab. """ renameTabRequested = QtCore.pyqtSignal(int, str) duplicateTabRequested = QtCore.pyqtSignal(int)
[docs] def __init__(self, parent=None): super().__init__() self.setMovable(True) self.setTabsClosable(True) self._locked_tab_present = False self._leftmost_drag = -1 self._rename_menu = NewTabNameContexMenu(self) self._rename_menu.renameTab.connect(self.renameTabRequested) self._context_menu = TabLabelContextMenu(self) self._context_menu.duplicateTab.connect(self.duplicateTabRequested) self._context_menu.renameTab.connect(self._showRenameDialog)
[docs] def resizeEvent(self, event): """ Resize widget """ super().resizeEvent(event) self.resizeTabs()
[docs] def resizeTabs(self): """ This is called when resizeEvent is triggered or when the width of the MSV changes. """ tab_widget = self.parent() # Experimented offset width_offset = 500 if self.sizeHint().width() > tab_widget.width() - width_offset: avg = (tab_widget.width() - width_offset) // self.count() style = "QTabBar::tab {{ width: {0}px; }}".format(avg) self.setStyleSheet(style) else: self.setStyleSheet("")
[docs] def lockLeftmostTab(self): """ Prevent the leftmost tab from being moved or closed. """ self._locked_tab_present = True # Remove close button self.setTabButton(0, QtWidgets.QTabBar.LeftSide, None) # darwin self.setTabButton(0, QtWidgets.QTabBar.RightSide, None) # other oses
[docs] def mousePressEvent(self, event): """ If a locked tab is present and clicked on, prevent it from being dragged. If another tab is clicked on, figure out where we need to stop dragging to prevent that tab from being dragged over or past the locked tab. See Qt documentation for additional method documentation. """ if self._locked_tab_present: pos = event.pos() clicked_tab = self.tabAt(pos) if clicked_tab == 0: self.setMovable(False) elif clicked_tab > 0: style = self.style() # The QTabBar code offsets the dragged tab by # this many pixels as soon as dragging starts overlap = style.pixelMetric(style.PM_TabBarTabOverlap, None, self) # This is the mouse x-coordinate that places the left edge of # the dragged tab against the right edge of the locked tab self._leftmost_drag = (self.tabRect(0).right() + pos.x() - self.tabRect(clicked_tab).left() + overlap - 1) super().mousePressEvent(event)
[docs] def mouseReleaseEvent(self, event): """ Undo any changes that were made in mousePressEvent in preparation for tab dragging. See Qt documentation for additional method documentation. """ if self._locked_tab_present: self.setMovable(True) self._mouse_press_pos = None # Set _leftmost_drag to a dummy integer instead of None so that we # don't get a type error from mouseMoveEvent if that method gets # triggered for some reason other than tab dragging. self._leftmost_drag = -1 super().mouseReleaseEvent(event)
[docs] def mouseMoveEvent(self, event): """ Undo any changes that were made in mousePressEvent in preparation for tab dragging. See Qt documentation for additional method documentation. """ if (self._locked_tab_present and event.localPos().x() < self._leftmost_drag): # If the coordinates don't change much from the last mouse move # event, then the dragged tab won't move much (to keep the animation # smooth). As a result, the dragged tab may get stuck ~5-25 pixels # away from the locked tab when we lock the mouse's x-coordinate at # self._leftmost_drag. To avoid this, we make sure that the # y-coordinate changes a lot by adding an arbitrary large number to # it. new_y = event.localPos().y() + 1000 new_pos = QtCore.QPointF(self._leftmost_drag, new_y) event = QtGui.QMouseEvent(event.type(), new_pos, Qt.NoButton, event.buttons(), event.modifiers()) super().mouseMoveEvent(event)
[docs] @QtCore.pyqtSlot(bool, str) def updateCanAddTab(self, enable: bool, tooltip: str): self._context_menu.duplicate_tab.setEnabled(enable) self._context_menu.duplicate_tab.setToolTip(tooltip)
[docs] def contextMenuEvent(self, event): point = event.pos() mapped_point = self.mapFromParent(point) tab_index = self.tabAt(mapped_point) if tab_index == -1: return global_point = self.mapToGlobal(point) self._context_menu.setIndex(tab_index) self._rename_menu.pos = global_point is_ws_tab = self._locked_tab_present and tab_index == 0 self._context_menu.rename_tab.setEnabled(not is_ws_tab) self._context_menu.popup(global_point)
def _showRenameDialog(self, index): """ Shows a dialog allowing the user to rename the tab :param index: The index of the tab to rename :type index: int """ current_tab_name = self.tabText(index) self._rename_menu.setTab(index, current_tab_name) self._rename_menu.popup()
[docs]class NewTabBtn(QtWidgets.QPushButton):
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setObjectName("MSVTabButton")
[docs] @QtCore.pyqtSlot(bool, str) def updateCanAddTab(self, enable: bool, tooltip: str): self.setEnabled(enable) self.setToolTip(tooltip)
[docs]class DetachedTabWidget(widgetmixins.InitMixin, QtWidgets.QWidget): """ This widget is used to create a QTabWidget where the QTabBar and the QStackedWidget (i.e. tab content area) are laid out separately. This class creates the QStackedWidget and connects the appropriate signals from the supplied QTabBar. Calling code is responsible for laying out the QTabBar and this widget. Most of the methods can be found in QTabWidget and thus, most of this class' documentation refers back to QTabWidget's docs. See `QTabWidget` documentation for signal documentation. """ _currentChanged = QtCore.pyqtSignal(int) tabBarClicked = QtCore.pyqtSignal(int) tabBarDoubleClicked = QtCore.pyqtSignal(int) tabNumberChanged = QtCore.pyqtSignal() CONTENT_MARGINS = (0, 0, 0, 0)
[docs] def __init__(self, parent=None, *, tab_bar): """ :param tab_bar: a custom tab bar. (mandatory keyword-only argument) :type tab_bar: `QtWidgets.QTabBar` """ self._tabs = tab_bar super().__init__(parent)
[docs] def initLayOut(self): super().initLayOut() self._stack = QtWidgets.QStackedWidget(self) self._setUpTabBar() self._setUpTabContentArea() self.setSizePolicy( QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.TabWidget)) self.setFocusPolicy(Qt.TabFocus) self.setFocusProxy(self._tabs) layout = self.main_layout layout.setContentsMargins(*self.CONTENT_MARGINS) layout.addWidget(self._stack)
def _setUpTabBar(self): """ Sets up the tab bar and hooks up the necessary signal / slot connections. """ tb = self._tabs tb.setDrawBase(False) tb.currentChanged.connect(self._showTab) tb.tabMoved.connect(self._tabMoved) tb.tabBarClicked.connect(self.tabBarClicked) tb.tabBarDoubleClicked.connect(self.tabBarDoubleClicked) tb.tabCloseRequested.connect(self.onTabCloseRequested) tb.setExpanding(not self.documentMode()) self._updateWidget()
[docs] def onTabCloseRequested(self, idx): """ Subclasses must override this method """ raise NotImplementedError
def _setUpTabContentArea(self): """ Sets up the tab content area (QStackedWidget) and hooks up the necessary signal / slot connections. """ self._stack.setLineWidth(0) self._stack.setSizePolicy( QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.TabWidget)) self._stack.widgetRemoved.connect(self._removeTab) def _updateWidget(self): """ Helper method to update the widget's geometry. """ self.update() self.updateGeometry()
[docs] def addTab(self, widget, label, icon=QtGui.QIcon()): # noqa: M511 # See QTabWidget documentation for method documentation return self.insertTab(-1, widget, label, icon)
[docs] def insertTab(self, idx, widget, label, icon=QtGui.QIcon()): # noqa: M511 # See QTabWidget documentation for method documentation if not widget: return -1 idx = self._stack.insertWidget(idx, widget) self._tabs.insertTab(idx, icon, label) self._updateWidget() self.tabNumberChanged.emit() return idx
[docs] def setTabText(self, idx, label): # See QTabWidget documentation for method documentation self._tabs.setTabText(idx, label) self._updateWidget()
[docs] def tabText(self, idx): # See QTabWidget documentation for method documentation return self._tabs.tabText(idx)
[docs] def setTabIcon(self, idx, icon): # See QTabWidget documentation for method documentation self._tabs.setTabIcon(idx, icon) self._updateWidget()
[docs] def tabIcon(self, idx): # See QTabWidget documentation for method documentation return self._tabs.tabIcon(idx)
[docs] def isTabEnabled(self, idx): # See QTabWidget documentation for method documentation return self._tabs.isTabEnabled(idx)
[docs] def setTabEnabled(self, idx, enable): # See QTabWidget documentation for method documentation self._tabs.setTabEnabled(idx, enable) w = self._stack.widget(idx) if w: w.setEnabled(enable)
[docs] def removeTab(self, idx): # See QTabWidget documentation for method documentation w = self._stack.widget(idx) if w: self._stack.removeWidget(w) self.tabNumberChanged.emit()
[docs] def currentWidget(self): # See QTabWidget documentation for method documentation return self._stack.currentWidget()
[docs] def setCurrentWidget(self, widget): # See QTabWidget documentation for method documentation self._tabs.setCurrentIndex(self.indexOf(widget))
[docs] def currentIndex(self): # See QTabWidget documentation for method documentation return self._tabs.currentIndex()
[docs] def setCurrentIndex(self, idx): # See QTabWidget documentation for method documentation self._tabs.setCurrentIndex(idx)
[docs] def indexOf(self, widget): # See QTabWidget documentation for method documentation return self._stack.indexOf(widget)
[docs] def tabBar(self): # See QTabWidget documentation for method documentation return self._tabs
def _showTab(self, idx): """ Internal slot to react to tab change. :param idx: index of current tab :type idx: int """ if 0 <= idx < self._stack.count(): self._stack.setCurrentIndex(idx) # Only emit currentChanged if it's valid # (0-tab states should always be transient so we can ignore them) self._currentChanged.emit(idx) def _removeTab(self, idx): """ Internal slot to remove a tab. :param idx: index of tab to remove :type idx: int """ self._tabs.removeTab(idx) self._updateWidget() def _tabMoved(self, from_idx, to_idx): """ Internal slot to react to a tab being moved. :param from_idx: index of tab's original position :type from_idx: int :param to_idx: index of tab's new position :type to_idx: int """ stack = self._stack with suppress_signals(stack): w = stack.widget(from_idx) stack.removeWidget(w) stack.insertWidget(to_idx, w)
[docs] def tabsClosable(self): # See QTabWidget documentation for method documentation return self._tabs.tabsClosable()
[docs] def setTabsClosable(self, closable): # See QTabWidget documentation for method documentation if self.tabsClosable() is closable: return self._tabs.setTabsClosable(closable) self._updateWidget()
[docs] def isMovable(self): # See QTabWidget documentation for method documentation return self._tabs.isMovable()
[docs] def setMovable(self, movable): # See QTabWidget documentation for method documentation self._tabs.setMovable(movable)
[docs] def widget(self, idx): # See QTabWidget documentation for method documentation return self._stack.widget(idx)
[docs] def count(self): # See QTabWidget documentation for method documentation return self._tabs.count()
[docs] def iconSize(self): # See QTabWidget documentation for method documentation return self._tabs.iconSize()
[docs] def setIconSize(self, size): # See QTabWidget documentation for method documentation self._tabs.setIconSize(size)
[docs] def documentMode(self): # See QTabWidget documentation for method documentation return self._tabs.documentMode()
[docs] def setDocumentMode(self, enabled): # See QTabWidget documentation for method documentation tb = self._tabs tb.setDocumentMode(enabled) tb.setExpanding(enabled) tb.setDrawBase(enabled) self._updateWidget()
[docs] def clear(self): # See QTabWidget documentation for method documentation while self.count(): self.removeTab(0)
[docs] def setTabToolTip(self, idx, tip): # See QTabWidget documentation for method documentation self._tabs.setTabToolTip(idx, tip)
[docs] def tabToolTip(self, idx): # See QTabWidget documentation for method documentation return self._tabs.tabToolTip(idx)
[docs]class MSVTabWidget(mappers.MapperMixin, DetachedTabWidget): """ QTabWidget customized for MSV. This widget acts as a view to an `MsvGuiModel`, where each tab corresponds to a `PageModel`. Actions initiated through this widget are undoable using the undo_stack passed to `__init__`. Actions initiated through the `MsvGuiModel` are *not* undoable. :ivar canAddTabChanged: A signal emitted to indicate whether it's possible to add new tabs. Emitted with a bool of whether new tabs can be added and a tooltip for the new tab button. :ivar newWidgetCreated: A signal emitted when a new tab is created. Emitted with the new widget. :vartype newWidgetCreated: QtCore.pyqtSignal """ MAX_TAB_NUM = 15 ADD_TAB_TOOLTIP = "Add Tab" TAB_LIMIT_REACHED_TOOLTIP = f"Tab limit ({MAX_TAB_NUM}) reached" model_class = MsvGuiModel canAddTabChanged = QtCore.pyqtSignal(bool, str) newWidgetCreated = QtCore.pyqtSignal(msv_widget.AbstractMsvWidget) _changingTab = util.flag_context_manager("_changing_tab") _movingTab = util.flag_context_manager("_moving_tab") _insertingOrRemovingTab = util.flag_context_manager( "_inserting_or_removing_tab")
[docs] def __init__(self, parent=None, *, tab_bar, struc_model, undo_stack): """ :param parent: The Qt parent :type parent: QtWidgets.QWidget or None :param tab_bar: The tab bar that will control which page is shown :type tab_bar: QtWidgets.QTabBar :param struc_model: The structure model used to keep sequences in sync with their associated structures. :type struc_model: schrodinger.application.msv.structure_model or None :param undo_stack: The undo stack to add commands to. :type undo_stack: schrodinger.application.msv.command.UndoStack """ self.tab = tab_bar self._structure_model = struc_model self.undo_stack = undo_stack self._changing_tab = False self._moving_tab = False self._inserting_or_removing_tab = False self._just_moved_tab = False self._prev_tab_index = 0 # don't respond to tab changes while we're instantiating self._initing_self = True super().__init__(parent, tab_bar=self.tab) self._initing_self = False self._locked_tab_present = False self.auto_update_model = False self._currentChanged.connect(self.onCurrentTabChanged)
[docs] def makeInitialModel(self): """ @overrides: MapperMixin We use `None` as our initial model as a performance optimization. MsvGui is responsible for setting the model """ return None
[docs] def setModel(self, model): if model is self.model: return elif model is None: super().setModel(model) return if not model.current_page.isNullPage(): current_idx = model.pages.index(model.current_page) else: current_idx = None with self._insertingOrRemovingTab(): # without this context manager, we'll get undo commands for any tab # changes (i.e. _showTab) that happen for _ in range(self.count()): self._removeTabAfterPageRemoved(0, force=True) super().setModel(model) for idx, page in enumerate(self.model.pages): page_widget = self._createNewPageWidget(page) self._insertTabAfterPageInserted(idx, page_widget, page.title) if current_idx is not None: self.setCurrentIndex(current_idx)
[docs] def defineMappings(self): M = self.model_class current_widget_target = mappers.TargetSpec( setter=self.setCurrentPageModel,) return [ (current_widget_target, M.current_page), (self._onLightModeChanged, M.light_mode), ]
[docs] def getSignalsAndSlots(self, model): ss = super().getSignalsAndSlots(model) ss.extend([ (model.pages.mutated, self.onPagesMutated), ]) # yapf:disable return ss
def _setUpTabBar(self): super()._setUpTabBar() self.canAddTabChanged.connect(self._tabs.updateCanAddTab) self._tabs.renameTabRequested.connect(self.renameTab) self._tabs.duplicateTabRequested.connect(self.duplicateTab)
[docs] def onTabCloseRequested(self, idx): """ Respond to the user clicking the tab close button on the specified tab. :param idx: The index of the tab to close. :type idx: int """ aln = self.model.pages[idx].aln any_hidden = aln.anyHidden() if any_hidden: resp = messagebox.show_question( parent=self, title="Hidden Sequences", text="This tab includes hidden sequences. " "Are you sure you want to delete it?") if not resp: return self._tabCloseCommand(idx)
[docs] def removeAllViewPages(self): with command.compress_command(self.undo_stack, "Remove all View pages"): for idx in reversed(range(len(self.model.pages))): self._tabCloseCommand(idx)
@command.do_command def _tabCloseCommand(self, idx): """ Create and execute an undoable command to close the specified tab. :param idx: The index of the tab to close. :type idx: int """ page = self.model.pages[idx] if page.is_workspace: return command.NO_COMMAND current_page = self.model.current_page min_count = 2 if self.model.hasWorkspacePage() else 1 should_replace_page = (self.count() == min_count) new_page = None def redo(): with self._insertingOrRemovingTab(): self.model.pages.pop(idx) if should_replace_page: nonlocal new_page if new_page is None: self.model.addViewPage() new_page = self.model.pages[-1] else: self.model.pages.append(new_page) def undo(): with self._insertingOrRemovingTab(): nonlocal new_page if new_page is not None: self.model.pages.pop() self.model.pages.insert(idx, page) self.model.current_page = current_page return redo, undo, f'Remove Tab "{page.title}"'
[docs] @QtCore.pyqtSlot(object, object) @util.skip_if("_moving_tab") def onPagesMutated(self, new_pages, old_pages): added, removed, moved = diffy.get_diff(new_pages, old_pages) self.onPagesAdded(added) self.onPagesRemoved(removed) self.onPagesMoved(moved) self._updateCanAddTab()
[docs] @util.skip_if("_initing_self") def onPagesAdded(self, added_pages): if not added_pages: return for page, insertion_idx in sorted(added_pages, key=lambda x: x[1]): page_widget = self._createNewPageWidget(page) self._insertTabAfterPageInserted(insertion_idx, page_widget, page.title) self._doShowTab(len(self.model.pages) - 1)
[docs] @util.skip_if("_initing_self") def onPagesRemoved(self, removed_pages): if not removed_pages: return removed_alignments = IdSet() for page_model, _ in removed_pages: removed_alignments.add(page_model.aln) for tab_idx, page in reversed(list(enumerate(self))): if page.getAlignment() in removed_alignments: self._removeTabAfterPageRemoved(tab_idx, force=True)
[docs] def onPagesMoved(self, moved_pages): if not moved_pages: return if not self._isModelAndTabOrderSynced(): err_msg = ("Moving pages directly on the model is not currently " "supported with the MSV tab widget.") raise NotImplementedError(err_msg)
def _isModelAndTabOrderSynced(self): return all(model.aln is wid.getAlignment() for model, wid in zip_longest(self.model.pages, self)) def __iter__(self): """ Iterate through all the tabs in the tab widget """ count = self.count() for i in range(count): yield self.widget(i)
[docs] def resizeEvent(self, event): """ When expanding the window, the tab bar size does not change, but we still want to resize the tabs. """ super().resizeEvent(event) self.tab.resizeTabs()
[docs] def removeTab(self, idx): raise RuntimeError("Removing tabs not supported.")
def _removeTabAfterPageRemoved(self, index, force=False): """ Remove the tab at the specified index from the tab widget, if the tab is not the last tab. This method should only be called after the corresponding page has been removed from the page model. :param index: The index of the tab to be removed :type index: int :param force: Whether we should allow removal of the last tab. :type force: bool """ if index == 0 and self._locked_tab_present: raise RuntimeError("Removing a locked tab is not allowed.") if force or self.count() > 1: widget = self.widget(index) super().removeTab(index) # The widget will stay in memory unless we explicitly delete it widget.deleteLater()
[docs] def lockLeftmostTab(self): """ Prevent the leftmost tab from being moved or closed. """ self.tab.lockLeftmostTab() self._locked_tab_present = True
@QtCore.pyqtSlot() def _updateCanAddTab(self): can_add = self.count() < self.MAX_TAB_NUM if can_add: msg = self.ADD_TAB_TOOLTIP else: msg = self.TAB_LIMIT_REACHED_TOOLTIP self.canAddTabChanged.emit(can_add, msg)
[docs] def renameTab(self, index, new_name): """ Rename the tab at the given index with the new name :param index: The index of the tab to rename :type index: int :param new_name: The new name of the tab :type new_name: str """ if index == 0 and self._locked_tab_present: raise RuntimeError("Renaming a locked tab is not allowed.") old_name = self.tabText(index) self._renameTabCommand(index, new_name, old_name)
@command.do_command def _renameTabCommand(self, index, new_name, old_name): """ Create and execute an undoable command to rename the tab at the given index. :param index: The index of the tab to rename :type index: int :param new_name: The new name of the tab :type new_name: str :param old_name: The previous name of the tab :type old_name: str """ redo = partial(self._doRenameTab, index, new_name) undo = partial(self._doRenameTab, index, old_name) return redo, undo, f"Rename Tab to {new_name}" def _doRenameTab(self, index, new_name): """ Rename the specified tab. :param index: The index of the tab to rename :type index: int :param new_name: The new name of the tab :type new_name: str """ tab_bar = self.tabBar() tab_bar.setTabText(index, new_name) self.setTabToolTip(index, new_name) self.model.pages[index].title = new_name
[docs] def addTab(self, widget, label, icon=None): # See QTabWidget documentation for method documentation raise RuntimeError("Adding tabs not supported.")
[docs] def insertTab(self, idx, widget, label, icon=None): # See QTabWidget documentation for method documentation raise RuntimeError("Inserting tabs not supported.")
def _insertTabAfterPageInserted(self, idx, widget, tab_label): """ Insert a tab at the specified location. This method should only be called after the corresponding page has been inserting into the page model. :param idx: The index of the tab to be inserted. :type idx: int :param widget: The widget for the tab. :type widget: msv_widget.AbstractMsvWidget :param tab_label: The name of the tab. :type tab_label: str """ super().insertTab(idx, widget, tab_label) if widget.isWorkspace(): if len(self.model.pages) > 1: raise RuntimeError("Workspace tab must be the first tab.") self.lockLeftmostTab() self.newWidgetCreated.emit(widget)
[docs] @QtCore.pyqtSlot() @command.do_command def createNewTab(self, *, aln=None): """ Insert a new tab in response to the user clicking on the new tab button. This method should not be called if the tab limit has been reached. (See `MAX_TAB_NUM`.) :param aln: An alignment to use for the page :type aln: gui_alignment.GuiProteinAlignment """ old_index = self.currentIndex() new_index = self.count() def redo(): with self._insertingOrRemovingTab(): self.model.addViewPage(aln=aln) def undo(): with self._insertingOrRemovingTab(): self.model.pages.pop(new_index) self.setCurrentIndex(old_index) return redo, undo, "Create New Tab"
[docs] @command.do_command def duplicateTab(self, index): """ Duplicate the specified tab. This method should not be called if the tab limit has been reached. (See `MAX_TAB_NUM`.) :param index: The index of the tab to duplicate. :type index: int """ cur_index = self.currentIndex() new_index = self.count() tab_name = self.tabText(index) def redo(): with self._insertingOrRemovingTab(): self.model.duplicatePage(index) def undo(): with self._insertingOrRemovingTab(): self.model.pages.pop(new_index) self.setCurrentIndex(cur_index) return redo, undo, f"Duplicate {tab_name}"
@util.skip_if("_moving_tab") def _tabMoved(self, from_idx, to_idx): """ Respond to the user dragging a tab to a new position on the tab bar. Note that this method will be called once per tab swap while dragging a tab. (I.e., this method can be called many times during a single drag and drop.) Because of this, sequential commands generated by `_tabMovedCommand` will automatically be merged into a single command on the undo stack. :note: When this method is called, the tab has already been moved in the tab bar (but not in the stack widget or the page model). However, we need tab moves to be undoable and redoable, which means that the redo method needs to carry out the move in the tab bar. As a result, we undo the tab move in this method and then redo/un-undo it in `_doMoveTab` below. :param from_idx: The previous index of the dragged tab. :type from_idx: int :param to_idx: The new index of the dragged tab. :type to_idx: int """ with self._movingTab(): self._tabs.moveTab(to_idx, from_idx) self._tabMovedCommand(from_idx, to_idx) # As soon as this method returns, the tab bar will emit a currentChanged # signal because of the drag-and-drop (*not* in response to _doMoveTab). # We set _just_moved_tab to make sure that _showTab knows not to create # an undoable command in response to that signal. _showTab is # responsible for setting _just_moved_tab back to False. self._just_moved_tab = True @command.do_command(command_id=command.CommandType.MoveTab) def _tabMovedCommand(self, from_idx, to_idx): """ Create and execute an undoable command to move the specified tab to a new position on the tab bar. Note that multiple sequential tab move commands will automatically be merged into a single command on the undo stack because this method return a command ID. :param from_idx: The previous index of the tab. :type from_idx: int :param to_idx: The new index of the tab. :type to_idx: int """ redo = partial(self._doMoveTab, from_idx, to_idx) undo = partial(self._doMoveTab, to_idx, from_idx) return redo, undo, "Move Tab" def _doMoveTab(self, from_idx, to_idx): """ Move the specified tab to a new position on the tab bar. :param from_idx: The previous index of the tab. :type from_idx: int :param to_idx: The new index of the tab. :type to_idx: int """ pages = self.model.pages with self._movingTab(): moving_page = pages.pop(from_idx) pages.insert(to_idx, moving_page) self._tabs.moveTab(from_idx, to_idx) super()._tabMoved(from_idx, to_idx) self._currentChanged.emit(self.currentIndex())
[docs] def currentPageModel(self): if self.model is None or self.count() == 0: return gui_models.NullPage() return self.model.pages[self.currentIndex()]
[docs] def setCurrentPageModel(self, page_model): if page_model is None or page_model.isNullPage(): return for idx, widget in enumerate(self): if widget.getAlignment() is page_model.aln: self.setCurrentIndex(idx) break
[docs] @util.skip_if("_inserting_or_removing_tab") def onCurrentTabChanged(self): if self.model is None: return self.model.current_page = self.currentPageModel() if self._locked_tab_present: # Show Workspace Sync icons on first tab if self.model.current_page.is_workspace: icon = QtGui.QIcon(":/msv/icons/synch-active.png") else: icon = QtGui.QIcon(":/msv/icons/synch-inactive.png") self.setTabIcon(0, icon)
def _createNewPageWidget(self, page_model): page_widget = msv_widget.ProteinAlignmentMsvWidget( self, self._structure_model, undo_stack=self.undo_stack) page_widget.setModel(page_model) page_widget.setLightMode(self.model.light_mode) return page_widget
[docs] def setStructureModel(self, struc_model): """ Update the structure model on all existing tabs and used to create new tabs. :param struc_model: The structure model for interacting with sequences associated with three-dimensional structures. :type struc_model: schrodinger.application.msv.structure_model.StructureModel """ self._structure_model = struc_model for tab_widget in self: tab_widget.setStructureModel(struc_model)
@util.skip_if("_changing_tab") def _showTab(self, idx): # See parent class for method documentation if any((self._inserting_or_removing_tab, self._moving_tab, self._just_moved_tab)): # if we're already in an undo command, don't create a new one self._doShowTab(idx) if self._just_moved_tab: self._just_moved_tab = False else: self._showTabCommand(idx, self._prev_tab_index) @command.do_command def _showTabCommand(self, idx, prev_idx): """ Create and execute an undoable command for switching the current tab. :param idx: The tab to switch to. :type idx: int :param prev_idx: The tab that we switched from. :type prev_idx: int """ tab_name = self._tabs.tabText(idx) redo = partial(self._doShowTab, idx) undo = partial(self._doShowTab, prev_idx) return redo, undo, f"Switch to {tab_name}" def _doShowTab(self, idx): """ Switch to the specified tab. :param idx: The tab to switch to. :type idx: int """ with self._changingTab(): self._prev_tab_index = idx self._tabs.setCurrentIndex(idx) super()._showTab(idx) @QtCore.pyqtSlot() def _onLightModeChanged(self): """ Enable or disable lightmode on all widgets' views and model """ enabled = self.model.light_mode for widget in self: widget.setLightMode(enabled)