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)
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()
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)