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

import sys
from functools import partial

import schrodinger
from schrodinger.application.msv import utils
from schrodinger.application.msv.dependencies import INSTALLED_DEPENDENCIES
from schrodinger.application.msv.dependencies import Dependency
from schrodinger.application.msv.dependencies import is_prime_installed
from schrodinger.application.msv.gui import color_widgets
from schrodinger.application.msv.gui import dialogs
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui import stylesheets
from schrodinger.application.msv.gui import viewconstants
from schrodinger.application.msv.gui.color import AbstractResiduePropertyScheme
from schrodinger.infra import util as infra_util
from schrodinger.models import mappers
from schrodinger.protein import annotation
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 pop_up_widgets
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.mapperwidgets import EnumComboBox
from schrodinger.ui.qt.mapperwidgets import MappableComboBox
from schrodinger.ui.qt.multi_combo_box import MultiComboBox
from schrodinger.ui.qt.standard_widgets import hyperlink
from schrodinger.ui.qt.utils import suppress_signals

maestro = schrodinger.get_maestro()

SEQ_ANNO = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
ALN_ANNO = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
ANNO_DEPENDENCIES = {
    SEQ_ANNO.antibody_cdr: set(
        [Dependency.Prime, Dependency.Clustal, Dependency.Bioluminate])
}

ICONS_PATH = ':/msv/icons/'

LAYOUT_ROW_PADDING = 6

SEPARATOR_COLOR = QtGui.QColor(210, 210, 210)  # Light gray

HAS_WORKSPACE = bool(maestro)

COLOR_SEQ_ALN_ITEMS = {
    "All residues on tab": viewconstants.ColorByAln.All,
    viewconstants.MATCHING_RESIDUES_ONLY: viewconstants.ColorByAln.Matching,
    viewconstants.DIFFERENT_RESIDUES_ONLY: viewconstants.ColorByAln.Different,
    "Residues matching consensus": viewconstants.ColorByAln.MatchingCons,
    "Residues differing from consensus": viewconstants.ColorByAln.DifferentCons,
    "No residues on tab": viewconstants.ColorByAln.Unset,
}  # yapf: disable

COLOR_SEQ_ALN_TOOLTIPS = {
    viewconstants.ColorByAln.All: "Color all residues on this tab",
    viewconstants.ColorByAln.Matching: "Color only residues matching the reference sequence",
    viewconstants.ColorByAln.Different: "Color only residues differing from the reference sequence",
    viewconstants.ColorByAln.MatchingCons: "Color only residues matching the consensus",
    viewconstants.ColorByAln.DifferentCons: "Color only residues differing from the consesus",
    viewconstants.ColorByAln.Unset: "Don't color residues on this tab"
} # yapf: disable

ANTIBODY_CDR_ITEMS = annotation.ANTIBODY_CDR_ITEMS

RESIDUE_PROPENSITY_ITEMS = {
    "Helix Propensity": SEQ_ANNO.helix_propensity,
    "Strand Propensity": SEQ_ANNO.beta_strand_propensity,
    "Turn Propensity": SEQ_ANNO.turn_propensity,
    "Helix Termination": SEQ_ANNO.helix_termination_tendency,
    "Exposure Tendency": SEQ_ANNO.exposure_tendency,
    "Steric Group": SEQ_ANNO.steric_group,
    "Side-Chain Chemistry": SEQ_ANNO.side_chain_chem,
}

ANTIBODY_CDR_TOOLTIPS = [
    "Use the %s numbering scheme for CDRs" % name
    for name in list(ANTIBODY_CDR_ITEMS)
]

HIGHLIGHT_COLORS = dict(red=(233, 65, 48),
                        red_orange=(234, 105, 53),
                        orange=(238, 154, 60),
                        yellow=(255, 253, 84),
                        yellow_green=(171, 250, 79),
                        green=(93, 200, 60),
                        green_blue=(88, 197, 198),
                        blue=(66, 145, 243),
                        indigo=(30, 44, 241),
                        purple=(137, 50, 242),
                        violet=(188, 58, 242),
                        pink=(230, 63, 243),
                        pink_red=(228, 63, 142))

OUTLINE_COLORS = {
    color: HIGHLIGHT_COLORS[color]
    for color in ['red', 'orange', 'green', 'indigo', 'purple', 'pink']
}


[docs]class ClickableLabel(QtWidgets.QLabel): """ A label that emits a 'clicked' signal when clicked :ivar clicked: emitted when the lable is clicked, :vartype clicked: `QtCore.pyqtSignal` """ clicked = QtCore.pyqtSignal()
[docs] def mousePressEvent(self, event): QtWidgets.QLabel.mousePressEvent(self, event) self.clicked.emit()
[docs]class DarkBackgroundLabel(ClickableLabel): """ A label that has dark background and text in white """
[docs] def __init__(self, text, parent=None, style=stylesheets.DARK_BG_STYLE): """ :param text: Label text :type text: string """ super().__init__(text, parent) self.setStyleSheet(style)
[docs]class GrayLabel(QtWidgets.QLabel): """ A label with smaller font and gray text, white background """
[docs] def __init__(self, text, parent=None): """ :param text: Label text :type text: string """ super().__init__(text, parent) self.setStyleSheet(stylesheets.GRAY_LABEL_STYLE)
[docs]class StyledButton(QtWidgets.QPushButton):
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet(stylesheets.BUTTON_STYLE)
[docs]class PlusMinusWidget(mappers.TargetMixin, QtWidgets.QFrame): """ A widget that uses two labels that have plus or minus on them, to change the value attribute. """ LAYOUT_SPACING = 5
[docs] def __init__(self, parent=None): super().__init__(parent) self.setStyleSheet(stylesheets.PLUS_MINUS_WIDGET) self.value = 0 self.plus = ClickableLabel("+", self) self.plus.setObjectName('plus_minus_lbl') minus = "\u2212" # unicode for "−" self.minus = ClickableLabel(minus, self) self.minus.setObjectName('plus_minus_lbl') self.plus.clicked.connect(self.increaseFont) self.minus.clicked.connect(self.decreaseFont) layout = QtWidgets.QHBoxLayout(self) layout.addWidget(GrayLabel("Size:", self)) layout.addSpacing(self.LAYOUT_SPACING) layout.addWidget(self.minus) layout.addWidget(self.plus) layout.setSpacing(self.LAYOUT_SPACING)
[docs] def increaseFont(self): if self.value < viewconstants.MAX_TEXT_SIZE: self.value += 1 self.targetValueChanged.emit()
[docs] def decreaseFont(self): if self.value > viewconstants.MIN_TEXT_SIZE: self.value -= 1 self.targetValueChanged.emit()
[docs] def targetGetValue(self): return self.value
[docs] def targetSetValue(self, value): self.value = value
[docs]class IconLabelToggle(QtWidgets.QCheckBox): """ Customized checkbox with an image icon on the left, which changes upon clicking. """ # TODO: Make this a QFrame widget with a label and a checkbox inside of it # instead of a checkbox with an icon. This will prevent the icon from # getting chopped off
[docs] def __init__(self, text, checked=False, parent=None): """ :param text: Text appear to the right of the icon :type text: string :param checked: default state of the toggle :type checked: bool """ super().__init__(text, parent) self.setStyleSheet(stylesheets.ICON_LABEL_TOGGLE) self.setChecked(checked)
[docs]class ToggleOption(QtCore.QObject): """ An instance handles one side of a SideBySideToggle """ clicked = QtCore.pyqtSignal(object)
[docs] def __init__(self, text, value, font, parent=None, is_terminal_opt=True, tooltip=None): """ :param text: The textual representation of the toggle option's value :type text: str :param value: The value represented by the toggle option :type value: object :param font: The font object to use to calculate label dimensions :type font: `QtGui.QFont` :param is_terminal_opt: Whether the option passed in is the last in the group :type is_terminal_opt: bool :param tooltip: An optional tooltip :type tooltip: None or str """ super().__init__(parent) if is_terminal_opt: self.selected_style = stylesheets.SELECTED_LABEL_STYLE self.unselected_style = stylesheets.UNSELECTED_LABEL_STYLE else: self.selected_style = stylesheets.SELECTED_CENTER_LABEL_STYLE self.unselected_style = stylesheets.UNSELECTED_CENTER_LABEL_STYLE self.text = text self.value = value self.font = font self.label = self._makeLabel(text) if tooltip is not None: self.label.setToolTip(tooltip) self.label.clicked.connect(self.onLabelClicked)
def _makeLabel(self, text): """ Makes a label with the specified text :param text: The text to set on the label :type text: str :rtype: ClickableLabel :return: A label with the appropriate text """ return ClickableLabel(text)
[docs] def select(self, selected=True): """ Changes the label's stylesheet to indicate whether it is selected :param selected: Whether to select the label :type selected: bool """ stylesheet = self.selected_style if selected else self.unselected_style self.label.setStyleSheet(stylesheet)
[docs] def onLabelClicked(self): """ Emits value when the label is clicked """ self.clicked.emit(self.value)
[docs]class BlankToggleOption(ToggleOption): """ Toggle option that displays an X to indicate that it is blank """ EXTRA_STYLESHEET = """ ClickableLabel { padding: 0px; } """ SIZE = 24
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.selected_style += self.EXTRA_STYLESHEET self.unselected_style += self.EXTRA_STYLESHEET off_pixmap = QtGui.QPixmap(":/msv/icons/no_text.png") self._off_pixmap = off_pixmap.scaled(self.SIZE, self.SIZE) on_pixmap = QtGui.QPixmap(":/msv/icons/no_text-ON.png") self._on_pixmap = on_pixmap.scaled(self.SIZE, self.SIZE) self.label.setPixmap(self._off_pixmap)
[docs] def select(self, selected=True): super().select(selected) pixmap = self._on_pixmap if selected else self._off_pixmap self.label.setPixmap(pixmap)
[docs]class SideBySideToggle(mappers.TargetMixin, QtWidgets.QWidget): """ A customized widget for switching between text options representing different values Emits a signal with the appropriate value when a new option is clicked. Changes background color when an option is selected """ LAYOUT_SPACE = 10
[docs] def __init__(self, label_text, text_value_mapping, initial_value, parent=None, margins=True, tooltips=None): """ :param label_text: Text that appears to the left of the options :type label_text: str :param text_value_mapping: An ordered mapping between the label texts presented to the user and the underlying values they represent :type text_value_mapping: list of tuples :param initial_value: The initial value for the widget :type initial_value: object :param margins: Whether the layout for this widget should have non-zero margins. :type margins: bool :param tooltips: An optional mapping of labels to tooltips :type tooltips: dict :raise ValueError: If the values in the map are not distinct """ super().__init__(parent) self._selected = None layout = QtWidgets.QHBoxLayout(self) if not margins: layout.setContentsMargins(0, 0, 0, 0) label = GrayLabel(label_text) layout.addWidget(label) layout.addSpacing(self.LAYOUT_SPACE) self._value_option_toggle_mapping = {} layout.setSpacing(0) tooltips = {} if tooltips is None else tooltips for idx, map_item in enumerate(text_value_mapping): text, value = map_item is_terminal_opt = idx == 0 or idx == len(text_value_mapping) - 1 ToggleCls = ToggleOption if text.strip() else BlankToggleOption tooltip = tooltips.get(text, None) option = ToggleCls(text, value, self.font(), self, is_terminal_opt=is_terminal_opt, tooltip=tooltip) layout.addWidget(option.label) option.clicked.connect(self.setValue) self._value_option_toggle_mapping[value] = option self.setValue(initial_value)
[docs] def labels(self): """ :rtype: list :return: The labels used in the side by side toggle widget This is useful for testing. """ return [ option.label for option in self._value_option_toggle_mapping.values() ]
[docs] def setValue(self, value): """ Sets the value for the widget, which will select the associated label and deselect the others. :param value: Any Python object that is among the possible values for the widget :type value: object :raise ValueError: If the value isn't among the possible values defined on instantiation of the widget """ try: selected_option = self._value_option_toggle_mapping[value] except KeyError: msg = "%s is not a possible value for this widget" % str(value) raise ValueError(msg) if self._selected == selected_option: # Don't bother to do anything if that option is already selected return self._selected = selected_option for option in self._value_option_toggle_mapping.values(): option.select(option == selected_option) self.targetValueChanged.emit()
[docs] def getValue(self): """ Returns the selected value for the widget :rtype: object :return: The selected value for the widget """ return self._selected.value
[docs] def targetGetValue(self): return self.getValue()
[docs] def targetSetValue(self, value): self.setValue(value)
[docs]class MsvComboBox(MappableComboBox):
[docs] def __init__(self, parent=None): super().__init__(parent) self.setStyleSheet(stylesheets.LIGHT_COMBOBOX) # MSV-1962: Workaround for a bug where the combobox background color # was being applied to the selected item background on Linux self.setItemDelegate(MsvComboBoxDelegate(self))
[docs] def setTooltips(self, tooltips): """ Sets tooltips for each item in the ComboBox as defined by a list. :param tooltips: Messages to display as tooltips. :type tooltips: list of str """ for i, tooltip in enumerate(tooltips): self.setItemData(i, tooltip, Qt.ToolTipRole)
[docs]class MsvComboBoxDelegate(QtWidgets.QStyledItemDelegate): """ Delegate for use with MsvComboBox that fixes bugs on Linux with background coloring (MSV-1962) and explicitly draws separators (MSV-2550) """
[docs] def paint(self, painter, option, index): """ It is necessary to explicitly implement separator painting when using a QStyledItemDelegate on a QComboBox """ if self.isSeparator(index): self.drawSeparator(painter, option) else: super().paint(painter, option, index)
[docs] def sizeHint(self, option, index): """ Half the row height of separator rows so there isnt too much padding """ size = super().sizeHint(option, index) if self.isSeparator(index): height = int(size.height() / 2) size.setHeight(height) return size
[docs] def isSeparator(self, index): return index.data(Qt.AccessibleDescriptionRole) == 'separator'
[docs] def drawSeparator(self, painter, option): painter.setPen(QtGui.QPen(SEPARATOR_COLOR)) painter.drawLine(option.rect.left() + 3, option.rect.center().y(), option.rect.right() - 3, option.rect.center().y())
[docs]class BindingSiteDistanceComboBox(EnumComboBox):
[docs] def __init__(self, parent=None): super().__init__(enum=annotation.BindingSiteDistance, parent=parent) self.setStyleSheet(stylesheets.LIGHT_COMBOBOX) # MSV-1962: Workaround for a bug where the combobox background color # was being applied to the selected item background on Linux self.setItemDelegate(MsvComboBoxDelegate(self)) display_texts = [(enum_member, f"{enum_member.value}Å") for enum_member in self._listified_enum] self.updateItemTexts(display_texts)
[docs] def setEnabled(self, enable): super().setEnabled(enable) self.setVisible(enable) self.targetValueChanged.emit()
[docs]class MappableMultiComboBox(mappers.TargetMixin, MultiComboBox): """ MultiComboBox that can be mapped to a SetParam representing the checked values. It must be passed a dictionary of all possible values in the SetParam, keyed by display name. """
[docs] def __init__(self, parent=None, itemdict=None): super().__init__(parent=parent) if itemdict is None: raise TypeError("itemdict must be provided") self.addItems(itemdict) self._item_values = list(itemdict.values()) self.selectionChanged.connect(self.targetValueChanged)
[docs] def targetSetValue(self, checked_values): # See parent class for documentation selected_indexes = [ i for i, val in enumerate(self._item_values) if val in checked_values ] self.setSelectedIndexes(selected_indexes)
[docs] def targetGetValue(self): # See parent class for documentation return { val for i, val in enumerate(self._item_values) if self.isIndexSelected(i) }
class _HideWhenDisabledMappableComboBox(MappableMultiComboBox): def __init__(self, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) self.setStyleSheet(stylesheets.DARK_COMBOBOX) def setEnabled(self, enable): """ Hide the combobox when it's not enabled. """ super().setEnabled(enable) self.setVisible(enable)
[docs]class ColorSchemeComboBox(color_widgets.ColorSchemeComboBoxMixin, MsvComboBox): """ A combo box that allows the user to select a color scheme or open the "Define Custom Color Scheme" dialog. :ivar defineCustomColorSchemeRequested: Signal emitted when the user selects "Define Custom Scheme...". :vartype defineCustomColorSchemeRequested: QtCore.pyqtSignal :ivar schemeChanged: Signal emitted when the user selects a scheme. :vartype schemeChanged: QtCore.pyqtSignal """ defineCustomColorSchemeRequested = QtCore.pyqtSignal() schemeChanged = QtCore.pyqtSignal() DEFINE_CUSTOM = "Define Custom Scheme..." _settingCustomVisibility = infra_util.flag_context_manager( "_setting_custom_visibility")
[docs] def __init__(self, parent=None): super().__init__(parent) added = self._populateComboBox(HAS_WORKSPACE, items_to_add=(self._SEPARATOR, self.DEFINE_CUSTOM)) self._define_custom_index = added.index(self.DEFINE_CUSTOM) self._setting_custom_visibility = False self._prev_index = 0 self.currentIndexChanged.connect(self._onCurIndexChanged)
@infra_util.skip_if("_setting_custom_visibility") def _onCurIndexChanged(self, idx): if idx == self._define_custom_index: with qt_utils.suppress_signals(self): # Make sure we don't leave "Define Custom Scheme..." selected self.setCurrentIndex(self._prev_index) self.defineCustomColorSchemeRequested.emit() else: self._prev_index = idx super()._onCurrentIndexChanged(idx) self.schemeChanged.emit()
[docs] def setCustomSchemeVisible(self, visible): """ Set whether the "Custom" entry is shown or hidden. :param visible: Whether the "Custom" entry should be shown. :type visible: bool """ if bool(visible) == self._custom_shown: return with self._settingCustomVisibility(): if visible: self._define_custom_index += 1 self._showCustom(self.CUSTOM) else: self._define_custom_index -= 1 self._hideCustom()
[docs]class BaseMsvPopUp(mappers.MapperMixin, pop_up_widgets.PopUp): """ Base class for MSV popup dialogs. Subclasses should define self._widget_to_model_map, the mapping between widgets and option model. """ model_class = gui_models.OptionsModel
[docs] def __init__(self, parent=None): super().__init__(parent) self.setObjectName('base_msv_pop_up') self.setStyleSheet(stylesheets.WHITE_BG_WIDGET_STYLE)
[docs] def setup(self): """ Sets up all components of the dialog. Should not be overridden by derived classes. Instead, they should override _setupWidgets. """ self.name_widget = {} self.main_layout = QtWidgets.QVBoxLayout(self) # MSV-1261 - Need to remove this padding outside of the stylesheets. See # https://stackoverflow.com/questions/24867987/qt-stylesheets-how-to-remove-dead-space self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.setSpacing(0) self._setupTitleWidget() self._setupWidgets() self._setupMapperMixin()
def _setupTitleWidget(self): """ Derived classes should override this function to set up the title. """ def _setupWidgets(self): """ Derived classes should override this function to set up widgets. """
[docs] def createCircleCheckBox(self, label_text, set_checked=False): """ Create and return a checkbox with the circle icon. :param label_text: Text for the checkbox label :type label_text: str :param set_checked: Whether to set the checkbox as checked by default :type set_checked: bool :return: The circle icon checkbox :rtype: `QtWidgets.QCheckBox` """ cb = QtWidgets.QCheckBox(label_text) cb.setStyleSheet(stylesheets.CIRCLE_CHECKBOX) cb.setChecked(set_checked) return cb
[docs] def createSubTitleLabel(self, label_text): """ Create and return a sub title label with the sub title style applied. :param label_text: text for the sub title label :type label_text: str :return: the newly created sub title label :rtype: `QtWidgets.QLabel` """ return DarkBackgroundLabel(label_text, style=stylesheets.DARK_BG_SUB_TITLE_STYLE)
[docs] def createVerticalLayout(self, indent=True): """ Create a vertical layout and set its margins. :return: The created vertical layout :rtype: `QtWidgets.QVBoxLayout` """ vlayout = QtWidgets.QVBoxLayout() self._setLayoutMargins(vlayout, indent=indent) return vlayout
[docs] def createHorizontalLayout(self, indent=True): """ Create a horizontal layout and set its margins. :return: The created horizontal layout :rtype: `QtWidgets.QHBoxLayout` """ hlayout = QtWidgets.QHBoxLayout() self._setLayoutMargins(hlayout, indent=indent) return hlayout
[docs] def createGridLayout(self, indent=True, items=None): """ Create a grid layout and set its margins. :param items: List of lists of items to add to the layout, or None to leave a gap in that position :type items: list[list[Union[QWidget, QLayout, None]]] :return: The grid layout :rtype: QtWidgets.QGridLayout """ glayout = QtWidgets.QGridLayout() self._setLayoutMargins(glayout, indent=indent) if items is not None: qt_utils.add_items_to_glayout(glayout, items) return glayout
def _setLayoutMargins(self, layout, indent=True): """ Sets the margins of the specified layout to the standard amounts for this widget. :param layout: Layout to set the margins of :type layout: `QtWidgets.QLayout` """ left = 10 if indent else 0 layout.setContentsMargins(left, LAYOUT_ROW_PADDING, 0, LAYOUT_ROW_PADDING)
[docs]class BaseMsvPopUpWithTitle(BaseMsvPopUp): """ Base class for MSV popup dialogs with the title bar. See BaseMsvPopUp for more information. """ def _setupTitleWidget(self): """ Helper method to setup the title area of the pop-up pane. """ self.pane_title_lbl = DarkBackgroundLabel( "TITLE", style=stylesheets.DARK_BG_TITLE_STYLE) self.main_layout.addWidget(self.pane_title_lbl) self.main_layout.addSpacing(7)
[docs] def setTitle(self, title): """ Set the title of the pop-up pane. :param title: to set at the top label of the pane. :type title: str """ self.pane_title_lbl.setText(title)
[docs]class ViewStylePopUp(widgetmixins.MessageBoxMixin, BaseMsvPopUpWithTitle): """ A popup dialog to provide options for viewing style """ openPropertyDialogRequested = QtCore.pyqtSignal() model_class = gui_models.PageModel GRID_LAYOUT_STRETCH_FACTOR = 1 GRID_LAYOUT_CONTENT_MARGINS = (15, 10, 15, 10) GRID_LAYOUT_HORIZONTAL_SPACING = 30 GRID_LAYOUT_VERTICAL_SPACING = 20 GRID_LAYOUT_COL_MIN_WIDTH = 50 H_LAYOUT_CONTENT_MARGINS = (5, 0, 5, 5)
[docs] def canToggleSplitChains(self): """ Check whether it's possible to toggle `split_chain_view`. If there are anchored residues or alignment sets, then prompt the user to confirm that they're okay with clearing them. If there are pairwise constraints, prompt the user to confirm that they're okay with clearing them, then clear them if okayed. :return: Whether it's possible to toggle `split_chain_view` :rtype: bool """ aln = self.model.aln title = "Toggle split chain view" if aln.getAnchoredResidues(): text = ("This change will cause anchors to be removed. Continue " "anyway?") if not self.question(text, title): return False if aln.hasAlnSets(): msg = ('This change will dissolve all Alignment Sets. ' 'Continue anyway?') if not self.question(msg, title): return False if aln.pairwise_constraints.hasConstraints(): msg = ('This change will clear pairwise constraints. ' 'Continue anyway?') if self.question(msg, title): aln.resetPairwiseConstraints() self.model.options.align_settings.pairwise.set_constraints = False else: return False if aln.anyHidden(): msg = ('This change will show all hidden seqs. Continue anyway?') if self.question(msg, title): aln.showAllSeqs() else: return False return True
def _setupWidgets(self): """ Set up the widgets for this dialog. """ self.setTitle('VIEW OPTIONS') # Alignment Calculations widgets aln_calc_label = self.createSubTitleLabel("ALIGNMENT CALCULATIONS") self.main_layout.addWidget(aln_calc_label) compute_for_columns_tooltips = { "All": "Compute for all columns defined by the Reference", "Selected": "Compute only for the columns defined by selection in the Reference" } self.col_toggle = SideBySideToggle( "Compute for columns:", (("All", viewconstants.ColumnMode.AllColumns), ("Selected", viewconstants.ColumnMode.SelectedColumns)), viewconstants.ColumnMode.SelectedColumns, tooltips=compute_for_columns_tooltips) self.col_toggle.layout().setContentsMargins(0, 10, 0, 10) self.gap_chb = self.createCircleCheckBox("Include Gaps") self.by_identity = self.createCircleCheckBox("Identity %") self.by_similarity = self.createCircleCheckBox("Similarity %") self.by_conservation = self.createCircleCheckBox("Conservation %") self.by_score = self.createCircleCheckBox("Overall Score") aln_layout = self.createGridLayout() show_lbl = GrayLabel('Show:') aln_layout.addWidget(show_lbl, 0, 0, Qt.AlignLeft) toggles = ((self.by_identity, self.by_similarity), (self.by_conservation, self.by_score)) for row, toggle_pair in enumerate(toggles): for col, toggle in enumerate(toggle_pair, 1): aln_layout.addWidget(toggle, row, col) aln_layout.addWidget(self.col_toggle, 2, 0, 1, 2, Qt.AlignLeft) aln_layout.addWidget(self.gap_chb, 2, 2, Qt.AlignVCenter) aln_layout.setColumnMinimumWidth(0, self.GRID_LAYOUT_COL_MIN_WIDTH) aln_layout.setColumnStretch(1, self.GRID_LAYOUT_STRETCH_FACTOR) aln_layout.setColumnStretch(2, self.GRID_LAYOUT_STRETCH_FACTOR) aln_layout.setHorizontalSpacing(self.GRID_LAYOUT_HORIZONTAL_SPACING) aln_layout.setVerticalSpacing(self.GRID_LAYOUT_VERTICAL_SPACING) aln_layout.setContentsMargins(*self.GRID_LAYOUT_CONTENT_MARGINS) self.main_layout.addLayout(aln_layout) # Sequence Display widgets seq_display_lbl = self.createSubTitleLabel("SEQUENCE DISPLAY") self.show_properties_lbl = hyperlink.SimpleLink("Show properties... ") self.show_properties_lbl.clicked.connect( self.openPropertyDialogRequested) seq_display_layout = self.createHorizontalLayout(indent=False) seq_display_layout.setContentsMargins(0, 0, 0, 0) seq_display_layout.addWidget(seq_display_lbl) seq_display_layout.addStretch() seq_display_layout.addWidget(self.show_properties_lbl) seq_display_frame = QtWidgets.QFrame() seq_display_frame.setStyleSheet("QFrame {border: none; " "background-color: #242424;}") seq_display_frame.setFixedHeight(25) seq_display_frame.setLayout(seq_display_layout) self.main_layout.addWidget(seq_display_frame) self.wrap_seq = self.createCircleCheckBox("Wrap Sequences", False) self.split_chain = self.createCircleCheckBox("Split Chains", True) self.split_chain.clicked.connect(self._splitChainClicked) wrap_layout = self.createGridLayout() wrap_layout.addWidget(self.wrap_seq, 0, 1, Qt.AlignVCenter) wrap_layout.addWidget(self.split_chain, 0, 2, Qt.AlignVCenter) wrap_layout.setColumnMinimumWidth( 0, (aln_layout.columnMinimumWidth(0) + show_lbl.minimumWidth() + aln_layout.horizontalSpacing())) wrap_layout.setColumnStretch(1, self.GRID_LAYOUT_STRETCH_FACTOR) wrap_layout.setColumnStretch(2, self.GRID_LAYOUT_STRETCH_FACTOR) wrap_layout.setHorizontalSpacing(self.GRID_LAYOUT_HORIZONTAL_SPACING) wrap_layout.setContentsMargins(*self.GRID_LAYOUT_CONTENT_MARGINS) self.main_layout.addLayout(wrap_layout) format_layout = self.createHorizontalLayout() format_layout.setContentsMargins(*self.H_LAYOUT_CONTENT_MARGINS) self.format_toggle = SideBySideToggle( "Format:", (("A", viewconstants.ResidueFormat.OneLetter), ("ALA", viewconstants.ResidueFormat.ThreeLetter), ("", viewconstants.ResidueFormat.HideLetters)), viewconstants.ResidueFormat.OneLetter) self.identity_toggle = SideBySideToggle( "Identities:", (("A", viewconstants.IdentityDisplayMode.Residue), (viewconstants.DEFAULT_GAP, viewconstants.IdentityDisplayMode.MidDot)), viewconstants.IdentityDisplayMode.Residue) self.font_widget = PlusMinusWidget(self) for widget in (self.format_toggle, self.identity_toggle, self.font_widget): format_layout.addWidget(widget) self.main_layout.addLayout(format_layout) def _setNotImplemented(self, widget): widget.setEnabled(False) widget.setToolTip("Not implemented") def _splitChainClicked(self, enabled): """ Respond to a click on the "Split Chains" checkbox. If there are no anchored residues, alignment sets, or pairwise constraints, then simply update the model's split chain setting. Otherwise, the user is prompted whether they are okay with clearing these in order to toggle `split_chain_view`. :param enabled: Whether the checkbox was checked or unchecked. :type enabled: bool """ if self.canToggleSplitChains(): # If the user is okay with it, update the model to match the view self.model.split_chain_view = enabled else: # Otherwise, reset the view to match the model. This will not # trigger this slot because `setChecked` does not cause `clicked` # to be emitted. self.split_chain.setChecked(self.model.split_chain_view)
[docs] def setModel(self, model): # See parent class for method documentation if self.model is not None: self.model.split_chain_viewChanged.disconnect( self.split_chain.setChecked) super().setModel(model) if model is not None: self.split_chain.setChecked(model.split_chain_view) model.split_chain_viewChanged.connect(self.split_chain.setChecked)
[docs] def defineMappings(self): # We intentionally don't connect split_chains_view here since we may # need to prompt the user about anchors before we actually change the # value. See _splitChainClicked above. M = gui_models.PageModel.options return [ (self.by_conservation, M.show_conservation_col), (self.by_identity, M.show_identity_col), (self.by_score, M.show_score_col), (self.by_similarity, M.show_similarity_col), (self.col_toggle, M.compute_for_columns), (self.font_widget, M.font_size), (self.format_toggle, M.res_format), (self.gap_chb, M.include_gaps), (self.identity_toggle, M.identity_display), (self.wrap_seq, M.wrap_sequences) ] # yapf:disable
[docs]class CheckBoxGroup(mappers.TargetMixin, QtCore.QObject): """ Class for mapping a set of checkboxes to a set where each checkbox represents some value. If the checkbox is checked, the value will be added to the set and vice-versa. Note that this class does not create any widgets itself and is simply a target for syncing a `SetParam` and a set of checkboxes. """
[docs] def __init__(self, value_cb_pairs): """ :param value_cb_pairs: A list of 2-tuples. The first value of each tuple is the value associated with the checkbox and the second value is the actual checkbox. :type value_cb_pairs: list[tuple(object, QCheckBox)] """ super().__init__() for _, cb in value_cb_pairs: cb.stateChanged.connect(self.targetValueChanged) self.value_cb_pairs = value_cb_pairs
[docs] def targetGetValue(self): return {value for value, cb in self.value_cb_pairs if cb.isChecked()}
[docs] def targetSetValue(self, new_value_set): with suppress_signals(self): for value, cb in self.value_cb_pairs: cb.setChecked(value in new_value_set) self.targetValueChanged.emit()
[docs]class EnumCheckBox(mappers.TargetMixin, QtWidgets.QCheckBox): """ Class for mapping a checkbox to an EnumParam with two values """
[docs] def __init__(self, label_text, enum, checked_index=1): """ :param label_text: Text to show next to the QCheckBox :param enum: 2-value enum to map to the checkbox state :param checked_index: Enum index corresponding to checked """ super().__init__(label_text) self.setStyleSheet(stylesheets.CIRCLE_CHECKBOX) assert len(enum) == 2 assert 0 <= checked_index <= 1 self._enum = enum self.checked_index = checked_index self.stateChanged.connect(self.targetValueChanged)
def _getEnumValue(self, index: int): return list(self._enum)[index]
[docs] def targetGetValue(self): if self.isChecked(): return self._getEnumValue(self.checked_index) else: return self._getEnumValue(0 if self.checked_index else 1)
[docs] def targetSetValue(self, new_value): assert isinstance(new_value, self._enum) enable = new_value is self._getEnumValue(self.checked_index) self.setChecked(enable)
[docs]class QuickAnnotationPopUp(BaseMsvPopUpWithTitle): """ Popup dialog for performing annotations. """ GRID_LAYOUT_SPACING = 15 GRID_LAYOUT_MARGINS = (15, 15, 15, 15) H_LAYOUT_MARGINS = GRID_LAYOUT_MARGINS def _setupWidgets(self): """ Set up the widgets for this popup. """ self.setTitle('ANNOTATIONS') # Sequence Annotations widgets self.sequence_label = self.createSubTitleLabel("SEQUENCE ANNOTATIONS") self.main_layout.addWidget(self.sequence_label) self.disulfide_cb = self.createCircleCheckBox("Disulfide Bonds") self.secondary_structure_cb = self.createCircleCheckBox( "Secondary Structure Assignment") self.b_factor_cb = self.createCircleCheckBox("B Factor") self.binding_sites_cb = self.createCircleCheckBox("Binding Site") self.binding_sites_combo = BindingSiteDistanceComboBox() self.binding_sites_combo.setVisible(False) self.hydrophobicity_cb = self.createCircleCheckBox("Hydrophobicity") self.iso_point_cb = self.createCircleCheckBox("Isoelectric Point") self.res_num_cb = self.createCircleCheckBox("Residue Numbers") self.domain_cb = self.createCircleCheckBox("Domains") self.kinase_feat_cb = self.createCircleCheckBox("Kinase Features") self.kinase_cons_cb = self.createCircleCheckBox( "Kinase Binding Site Conservation") self.antibody_cdr_cb = self.createCircleCheckBox("Antibody CDRs") self.antibody_cdr_combo = MsvComboBox(self) self.antibody_cdr_combo.addItems(ANTIBODY_CDR_ITEMS) self.antibody_cdr_combo.setVisible(False) self.antibody_cdr_combo.setTooltips(ANTIBODY_CDR_TOOLTIPS) self.res_propensity_cb = self.createCircleCheckBox("Residue Propensity") self.res_propensity_combo = _HideWhenDisabledMappableComboBox( itemdict=RESIDUE_PROPENSITY_ITEMS) self.res_propensity_combo.setVisible(False) binding_sites_layout = QtWidgets.QHBoxLayout() binding_sites_layout.addWidget(self.binding_sites_cb) binding_sites_layout.addWidget(self.binding_sites_combo) seq_grid = ( (self.disulfide_cb, self.secondary_structure_cb), (self.b_factor_cb, binding_sites_layout), (self.hydrophobicity_cb, self.iso_point_cb), (self.res_num_cb, self.domain_cb), (self.kinase_feat_cb, self.kinase_cons_cb), (self.antibody_cdr_cb, self.antibody_cdr_combo), (self.res_propensity_cb, self.res_propensity_combo), ) # yapf: disable seq_layout = self.createGridLayout(items=seq_grid) # Stretch out the last column so that all toggles are tightly bound seq_layout.setColumnStretch(2, 2) self.main_layout.addLayout(seq_layout) anno_cb_pairs = [ (SEQ_ANNO.disulfide_bonds, self.disulfide_cb), (SEQ_ANNO.secondary_structure, self.secondary_structure_cb), (SEQ_ANNO.b_factor, self.b_factor_cb), (SEQ_ANNO.binding_sites, self.binding_sites_cb), (SEQ_ANNO.window_hydrophobicity, self.hydrophobicity_cb), (SEQ_ANNO.resnum, self.res_num_cb), (SEQ_ANNO.window_isoelectric_point, self.iso_point_cb), (SEQ_ANNO.antibody_cdr, self.antibody_cdr_cb), (SEQ_ANNO.domains, self.domain_cb), (SEQ_ANNO.kinase_conservation, self.kinase_cons_cb), ] # yapf: disable self.seq_anno_cbgroup = CheckBoxGroup(anno_cb_pairs) # Alignment Annotations widgets self.alignment_label = self.createSubTitleLabel("ALIGNMENT ANNOTATIONS") self.main_layout.addWidget(self.alignment_label) self.cons_seq_cb = self.createCircleCheckBox("Consensus Sequence") self.mean_hydro_cb = self.createCircleCheckBox("Mean Hydrophobicity") self.seq_logo_cb = self.createCircleCheckBox("Sequence Logo") self.cons_symbols_cb = self.createCircleCheckBox("Consensus Symbols") self.mean_iso_cb = self.createCircleCheckBox("Mean Isoelectric Point") aln_grid = ( (self.cons_seq_cb, self.mean_hydro_cb, self.seq_logo_cb), (self.cons_symbols_cb, self.mean_iso_cb), ) # yapf: disable aln_layout = self.createGridLayout(items=aln_grid) aln_anno_cb_pairs = [ (ALN_ANNO.mean_hydrophobicity, self.mean_hydro_cb), (ALN_ANNO.mean_isoelectric_point, self.mean_iso_cb), (ALN_ANNO.consensus_symbols, self.cons_symbols_cb), (ALN_ANNO.consensus_seq, self.cons_seq_cb), (ALN_ANNO.sequence_logo, self.seq_logo_cb), ] self.aln_anno_cbgroup = CheckBoxGroup(aln_anno_cb_pairs) self.main_layout.addLayout(aln_layout) # Annotation Settings widgets self.settings_label = self.createSubTitleLabel("ANNOTATION SETTINGS") self.main_layout.addWidget(self.settings_label) settings_layout = self.createHorizontalLayout() self.group_by_type_cb = EnumCheckBox("Group by Type", viewconstants.GroupBy) settings_layout.addWidget(self.group_by_type_cb) hspacer = QtWidgets.QSpacerItem(40, 0, QtWidgets.QSizePolicy.MinimumExpanding) settings_layout.addSpacerItem(hspacer) self.reset_all_btn = StyledButton("Clear All") self.reset_all_btn.clicked.connect(self.resetMappedParams) settings_layout.addWidget(self.reset_all_btn) self.main_layout.addLayout(settings_layout) self.antibody_cdr_enabled = True # If we're in debug mode, we only hookup antibody CDR if the required # dependencies are installed if utils.DEBUG_MODE: missing_dependencies = (ANNO_DEPENDENCIES[SEQ_ANNO.antibody_cdr] - INSTALLED_DEPENDENCIES) if missing_dependencies: self.antibody_cdr_enabled = False
[docs] def defineMappings(self): M = gui_models.OptionsModel res_prop_target = mappers.TargetSpec(self.res_propensity_cb, slot=self._updateResiduePropensity) # FIXME: After PANEL-17924 QCheckBox and the param will be in sync, we # should then use the infra code. kinase_feat_target = mappers.TargetSpec( self.kinase_feat_cb, signal=self.kinase_feat_cb.toggled) return [ (self.group_by_type_cb, M.group_by), (self.binding_sites_combo, M.binding_site_distance), (res_prop_target, M.residue_propensity_enabled), (self.res_propensity_combo, M.residue_propensity_annotations), (self.antibody_cdr_cb, M.antibody_cdr), (self.antibody_cdr_combo, M.antibody_cdr_scheme), (self.seq_anno_cbgroup, M.sequence_annotations), (self.aln_anno_cbgroup, M.alignment_annotations), (kinase_feat_target, M.kinase_features), ] # yapf: disable
[docs] def getSignalsAndSlots(self, model): ss = [ (self.binding_sites_cb.toggled, self._updateBindingSites), (model.sequence_annotationsChanged, self._enableAnnotations), (model.alignment_annotationsChanged, self._enableAnnotations), (model.predicted_annotationsChanged, self._enableAnnotations), (model.residue_propensity_enabledChanged, self._enableAnnotations), (model.residue_propensity_annotationsChanged, self._onResPropAnnosChanged), (model.kinase_features_enabledChanged, self._enableAnnotations), (model.kinase_featuresChanged, self._onKinaseFeaturesChanged), ] # yapf: disable if self.antibody_cdr_enabled: ss.append((self.antibody_cdr_cb.toggled, self._updateAntibodyCDR)) else: ss.append( (self.antibody_cdr_cb.toggled, self._disabledAntibodyCDRSlot)) return ss
[docs] def createGridLayout(self, **kwargs): """ Add the appropriate spacing and content margins for annotations pane. See parent `BaseMsvPopUpWithTitle` for documentation. """ layout = super().createGridLayout(**kwargs) layout.setSpacing(self.GRID_LAYOUT_SPACING) layout.setContentsMargins(*self.GRID_LAYOUT_MARGINS) return layout
[docs] def createHorizontalLayout(self, indent=True): """ Add the appropriate content margins for annotations pane. See parent `BaseMsvPopUpWithTitle` for documentation. """ layout = super().createHorizontalLayout(indent) layout.setContentsMargins(*self.H_LAYOUT_MARGINS) return layout
@QtCore.pyqtSlot() def _enableAnnotations(self): self.model.annotations_enabled = True @QtCore.pyqtSlot(object) @QtCore.pyqtSlot(list) def _onResPropAnnosChanged(self, res_prop_annos): if res_prop_annos: # Note: residue_propensity_annotationsChanged is # connected to _enableAnnotations, so we don't explicitly call # _enableAnnotations here to avoid multiple signals self.model.residue_propensity_enabled = True else: self._enableAnnotations() @QtCore.pyqtSlot() def _updateBindingSites(self): self.binding_sites_combo.setEnabled(self.binding_sites_cb.isChecked()) def _updateResiduePropensity(self): show_res_propensity = self.model.residue_propensity_enabled self.res_propensity_combo.setEnabled(show_res_propensity) @QtCore.pyqtSlot() def _updateAntibodyCDR(self): show_antibody_cdr = self.antibody_cdr_cb.isChecked() self.antibody_cdr_combo.setVisible(show_antibody_cdr) def _disabledAntibodyCDRSlot(self): feature = 'antibody cdr annotations' missing_dependencies = (ANNO_DEPENDENCIES[SEQ_ANNO.antibody_cdr] - INSTALLED_DEPENDENCIES) with suppress_signals(self.antibody_cdr_cb): self.antibody_cdr_cb.setCheckState(False) self._showMissingDependencyWarning(feature, missing_dependencies) def _onKinaseFeaturesChanged(self): """ Check whether prime is installed before enabling kinase annotations. """ if not self.model.kinase_features: self.model.kinase_features_enabled = False return if not is_prime_installed(): self.model.kinase_features = False msg = "Prime not installed, kinase features cannot be found." messagebox.show_warning(self, msg, title='Kinase Feature Annotation') return accepted = True self.model.kinase_features = accepted self.model.kinase_features_enabled = accepted def _showMissingDependencyWarning(self, feature, dependencies): """ Method that shows a warning message telling the developer what missing dependencies they need to install to use `feature`. :param feature: The feature that is missing dependencies :type feature: string :param dependencies: The missing dependencies :type dependencies: set(Dependency) """ warning_msg = ('You must install the following missing packages before ' 'using %s: ' % feature + ', '.join([d.name for d in dependencies])) QtWidgets.QMessageBox.warning(self.parent(), 'Dependency not found', warning_msg)
[docs]class ColorPopUp(BaseMsvPopUpWithTitle): """ A popup dialog to provide options for color settings. :ivar colorSelResiduesRequested: Signal emitted when one of the "Paint Selected" color buttons is clicked. Emits the RGB color to color the residues. :ivar clearHighlightsRequested: Signal emitted when the "Clear All Highlights" lbl is clicked. :ivar applyColorsToWorkspaceRequested: Signal emitted when the "Color Carbons" or "Color Residues" button is clicked. Emitted with whether to color all atoms. :ivar defineCustomColorSchemeRequested: Signal emitted when the user selects "Define Custom Scheme...". """ colorSelResRequested = QtCore.pyqtSignal(tuple) clearHighlightsRequested = QtCore.pyqtSignal() applyColorsToWorkspaceRequested = QtCore.pyqtSignal(bool) defineCustomColorSchemeRequested = QtCore.pyqtSignal() COLOR_WS_TEXT = "Color Carbons" COLOR_WS_RES_TEXT = "Color Residues" GRID_LAYOUT_VERTICAL_SPACING = 10 COLOR_PIXMAP_SIZE = 16 COLOR_IMAGE_SIZE = 256 def _setupWidgets(self): self.setTitle("COLOR SEQUENCES") ### Settings section ### settings_lbl = self.createSubTitleLabel("COLOR SCHEMES & SETTINGS") settings_sublayout = self.createHorizontalLayout(indent=False) self.reset_colors_lbl = self.createSubTitleLabel("Reset") self.reset_colors_lbl.setObjectName("reset_colors_lbl") self.reset_colors_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) settings_sublayout.addWidget(settings_lbl) settings_sublayout.addWidget(self.reset_colors_lbl) apply_lbl = GrayLabel("Apply to:") self.color_seq_aln_combo = MsvComboBox(self) self.color_seq_aln_combo.addItems(COLOR_SEQ_ALN_ITEMS) for idx in range(self.color_seq_aln_combo.count()): item = self.color_seq_aln_combo.itemData(idx) tooltip = COLOR_SEQ_ALN_TOOLTIPS[item] self.color_seq_aln_combo.setItemData(idx, tooltip, Qt.ToolTipRole) separator_positions = [ self.color_seq_aln_combo.findData(data) for data in (viewconstants.ColorByAln.MatchingCons, viewconstants.ColorByAln.Unset) ] for sep_idx, before_position in enumerate(sorted(separator_positions)): position = before_position + sep_idx self.color_seq_aln_combo.insertSeparator(position) color_by_lbl = GrayLabel("Color by:") # TODO MSV-1800 Add new color schemes and separators self.color_seq_combo = ColorSchemeComboBox(self) self.color_seq_combo.defineCustomColorSchemeRequested.connect( self.defineCustomColorSchemeRequested) self.color_scheme_info_btn = make_pop_up_tool_button( parent=self, pop_up_class=ColorSchemeInfoPopUp, obj_name="color_scheme_info_btn", indicator=False) self.color_info_popup = self.color_scheme_info_btn.popup_dialog self.weight_by_quality_cb = self.createCircleCheckBox( "Weight colors by alignment quality", False) self.average_in_cols_cb = self.createCircleCheckBox( "Average colors in columns", False) self.reset_colors_lbl.clicked.connect(self._resetColorOptions) settings_grid = ( (apply_lbl, self.color_seq_aln_combo), (color_by_lbl, self.color_seq_combo, self.color_scheme_info_btn), (None, self.weight_by_quality_cb), (None, self.average_in_cols_cb) ) # yapf: disable settings_layout = self.createGridLayout(items=settings_grid) settings_layout.setVerticalSpacing(self.GRID_LAYOUT_VERTICAL_SPACING) ### Highlight section ### highlight_lbl = self.createSubTitleLabel("HIGHLIGHT SELECTED RESIDUES") self.clear_highlight_lbl = self.createSubTitleLabel( "Clear All Highlights") self.clear_highlight_lbl.setObjectName("clear_all_highlights_lbl") self.clear_highlight_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) highlight_sublayout = self.createHorizontalLayout(indent=False) highlight_sublayout.addWidget(highlight_lbl) highlight_sublayout.addWidget(self.clear_highlight_lbl) paint_lbl = GrayLabel("Paint selected:") self.paint_widget = self._createColorBtnWidget(HIGHLIGHT_COLORS, custom=True) paint_layout = self.createHorizontalLayout(indent=False) paint_layout.addWidget(paint_lbl) paint_layout.addSpacing(20) paint_layout.addWidget(self.paint_widget) paint_layout.addSpacing(10) # MSV-3357: Hiding unimplemented features for beta #outline_lbl = GrayLabel("Outline selected blocks:") #outline_widget = self._createColorBtnWidget(OUTLINE_COLORS) #outline_layout = self.createHorizontalLayout(indent=False) #outline_layout.addWidget(outline_lbl) #outline_layout.addSpacing(20) #outline_layout.addWidget(outline_widget) #outline_layout.addStretch() highlight_layout = self.createVerticalLayout() highlight_layout.addLayout(paint_layout) #highlight_layout.addLayout(outline_layout) # Add everything to main layout self.main_layout.addLayout(settings_sublayout) self.main_layout.addLayout(settings_layout) self.main_layout.addLayout(highlight_sublayout) self.main_layout.addLayout(highlight_layout) ### Workspace section ### if HAS_WORKSPACE: workspace_lbl = self.createSubTitleLabel("COLOR LINKED STRUCTURES") apply_lbl = GrayLabel("Apply colors to structures:") self.whenever_cb = self.createCircleCheckBox("Whenever they change") self.entire_cb = self.createCircleCheckBox("Color entire residues") self.color_ws_btn = StyledButton(self.COLOR_WS_TEXT) self.color_ws_btn.clicked.connect(self._requestColorWorkspace) workspace_sublayout = self.createHorizontalLayout(indent=False) workspace_sublayout.addWidget(self.whenever_cb) workspace_sublayout.addWidget(self.entire_cb) workspace_sublayout.addWidget(self.color_ws_btn) workspace_layout = self.createVerticalLayout() workspace_layout.addWidget(apply_lbl) workspace_layout.addLayout(workspace_sublayout) self.main_layout.addWidget(workspace_lbl) self.main_layout.addLayout(workspace_layout) # TODO Fix disabled parts # TODO MSV-1438 implement user-defined annotations #self._setNotImplemented(outline_lbl) #self._setNotImplemented(outline_widget)
[docs] def defineMappings(self): M = gui_models.OptionsModel color_scheme_target = mappers.TargetSpec( self.color_seq_combo, getter=self._getCurrentScheme, setter=self._setCurrentScheme, signal=self.color_seq_combo.schemeChanged) mappings = [ (self.color_seq_aln_combo, M.color_by_aln), (self.weight_by_quality_cb, M.weight_by_quality), (self.average_in_cols_cb, M.average_in_cols), (color_scheme_target, M.seq_color_scheme), (self.color_info_popup, M.seq_color_scheme), (self._onCustomColorSchemeChanged, M.custom_color_scheme) ] # yapf: disable if HAS_WORKSPACE: mappings.extend([ (self.whenever_cb, M.ws_color_sync), (self.entire_cb, M.ws_color_all_atoms), ]) return mappings
[docs] def getSignalsAndSlots(self, model): ss = [ (self.clear_highlight_lbl.clicked, self.clearHighlightsRequested), (model.color_by_alnChanged, self._onColorOptionChanged), (model.weight_by_qualityChanged, self._onColorOptionChanged), (model.average_in_colsChanged, self._onColorOptionChanged), (model.seq_color_schemeChanged, self._onColorOptionChanged) ] # yapf: disable if HAS_WORKSPACE: ss.append((model.ws_color_all_atomsChanged, self._updateColorWorkspaceButton)) for signal in ( model.seq_color_schemeChanged, model.custom_color_schemeChanged, self.colorSelResRequested, self.clearHighlightsRequested, ): ss.append((signal, self._updateWorkspaceColorsIfAutoSyncing)) return ss
def _getCurrentScheme(self): combo = self.color_seq_combo cur_item = combo.currentItem() if cur_item == combo.CUSTOM: return self.model.custom_color_scheme elif cur_item == combo.DEFINE_CUSTOM: return combo.DEFINE_CUSTOM else: scheme = cur_item() return scheme def _setCurrentScheme(self, scheme): combo = self.color_seq_combo if scheme.custom: # This setCustomSchemeVisible may be necessary when we're in the # process of loading a new OptionsModel, since we don't know what # order seq_color_scheme and custom_color_scheme will be loaded in. combo.setCustomSchemeVisible(True) combo.setCurrentItem(combo.CUSTOM) else: combo.setCurrentItem(type(scheme)) def _onCustomColorSchemeChanged(self): visible = self.model.custom_color_scheme is not None self.color_seq_combo.setCustomSchemeVisible(visible) @QtCore.pyqtSlot() def _onColorOptionChanged(self): """ When any color options are changed, automatically enable colors and update reset """ self.model.colors_enabled = True self.updateResetColorLabel() def _resetColorOptions(self): self.resetMappedParams() self.updateResetColorLabel() def _mappedParamsAreDefault(self): if self.model is None: return True for param in self.mappedParams(): v = param.getParamValue(self.model) dv = param.defaultValue() if v != dv: return False return True def _createColorBtnWidget(self, colors, custom=False): layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(10, 8, 10, 8) layout.setSpacing(0) for rgb_color in colors.values(): pixmap = self.createColorPixmap(rgb_color) color_btn = self._createColorBtnFromPixmap(pixmap) # TODO MSV-1438 need different signal for outline color_btn.clicked.connect( partial(self.colorSelResRequested.emit, rgb_color)) layout.addWidget(color_btn) if custom: custom_color_btn = QtWidgets.QToolButton() icon = QtGui.QIcon(":/msv/icons/ellipsis.png") custom_color_btn.setIcon(icon) custom_color_btn.clicked.connect(self.onCustomColorClicked) custom_color_btn.setToolTip( "Select custom color for selected residues") layout.addWidget(custom_color_btn) layout.addSpacing(10) clear_color_btn = self._createClearColorBtn() clear_color_btn.setToolTip("Clear highlight from selected residues") layout.addWidget(clear_color_btn) widget = QtWidgets.QWidget() widget.setStyleSheet(stylesheets.COLOR_BUTTON_STYLE) widget.setLayout(layout) return widget
[docs] @classmethod def createColorPixmap(cls, rgb_color): pixmap = QtGui.QPixmap(cls.COLOR_PIXMAP_SIZE, cls.COLOR_PIXMAP_SIZE) pixmap.fill(QtGui.QColor(*rgb_color)) return pixmap
def _createColorBtnFromPixmap(self, pixmap): icon = QtGui.QIcon(pixmap) color_btn = QtWidgets.QToolButton() color_btn.setIcon(icon) return color_btn def _createClearColorBtn(self): image = QtGui.QImage(self.COLOR_IMAGE_SIZE, self.COLOR_IMAGE_SIZE, QtGui.QImage.Format_RGB32) image.fill(Qt.white) painter = QtGui.QPainter() painter.begin(image) painter.setRenderHint(QtGui.QPainter.Antialiasing) pen = QtGui.QPen(QtGui.QColor(186, 41, 30)) pen.setWidth(self.COLOR_IMAGE_SIZE // 8) painter.setPen(pen) painter.drawLine(self.COLOR_IMAGE_SIZE, 0, 0, self.COLOR_IMAGE_SIZE) painter.end() pixmap = QtGui.QPixmap.fromImage(image) color_btn = self._createColorBtnFromPixmap(pixmap) color_btn.clicked.connect(partial(self.colorSelResRequested.emit, ())) return color_btn
[docs] def onCustomColorClicked(self): """ Show a color dialog and emit the colorSelResRequested signal with the selected custom color. """ dlg = QtWidgets.QColorDialog(self) if dlg.exec(): color = dlg.currentColor() rgb = tuple(color.getRgb()[:3]) self.colorSelResRequested.emit(rgb)
[docs] def setEnableColorPicker(self, enable): """ Enables picking colors for selected residues :param enable: Whether to enable color picking :type enable: bool """ self.paint_widget.setEnabled(enable)
[docs] def setClearHighlightStatus(self, status): """ Set the dynamic property "highlight" on the "Clear All Highlights" button :param status: Value to set for "highlight" :type status: bool """ widget = self.clear_highlight_lbl widget.setProperty("highlight", status) qt_utils.update_widget_style(widget)
[docs] def updateResetColorLabel(self): widget = self.reset_colors_lbl mapped_params_are_default = self._mappedParamsAreDefault() widget.setProperty("highlight", not mapped_params_are_default) qt_utils.update_widget_style(widget)
def _requestColorWorkspace(self): """ Request applying colors to the workspace """ color_all = self.entire_cb.isChecked() self.applyColorsToWorkspaceRequested.emit(color_all) def _updateColorWorkspaceButton(self, entire_residues): """ Update the text of the color workspace button """ text = self.COLOR_WS_RES_TEXT if entire_residues else self.COLOR_WS_TEXT self.color_ws_btn.setText(text) def _updateWorkspaceColorsIfAutoSyncing(self): if not self.model.ws_color_sync: return self._requestColorWorkspace() def _setNotImplemented(self, w, disable=True): if disable: w.setEnabled(False) w.setToolTip("Not implemented")
[docs]class ColorSchemeInfoPopUp(mappers.TargetMixin, widgetmixins.InitMixin, pop_up_widgets.PopUp):
[docs] def initSetOptions(self): super().initSetOptions() self.setObjectName("color_scheme_info_popup") self.setStyleSheet(""" QFrame#color_scheme_info_popup { background-color: #343434; border: 1px #444444; margin: 2px; } QFrame#color_scheme_info_popup QLabel { color: white; } """)
[docs] def initLayOut(self): super().initLayOut() self._grid_layout = QtWidgets.QGridLayout() self.main_layout.addLayout(self._grid_layout)
[docs] def setup(self): # No-op override of PopUp pass
[docs] def targetSetValue(self, value): self._value = value self._updateDisplay(value)
def _updateDisplay(self, scheme): desc_dict = scheme.getSchemeSummary() self._clearDisplay() if isinstance(scheme, AbstractResiduePropertyScheme): top_texts = [scheme._seq_prop.display_name] else: top_texts = [scheme.NAME] if scheme.custom: top_texts.append("(custom)") for row_idx, text in enumerate(top_texts): lbl = QtWidgets.QLabel(text) self._grid_layout.addWidget(lbl, row_idx, 0, 1, 2) offset = len(top_texts) for idx, (color_tuple, description) in enumerate(desc_dict.items()): pixmap = ColorPopUp.createColorPixmap(color_tuple) color_lbl = QtWidgets.QLabel() color_lbl.setPixmap(pixmap) desc_lbl = QtWidgets.QLabel(description) row_idx = idx + offset self._grid_layout.addWidget(color_lbl, row_idx, 0) self._grid_layout.addWidget(desc_lbl, row_idx, 1) def _clearDisplay(self): while self._grid_layout.count(): widget_item = self._grid_layout.takeAt(0) widget = widget_item.widget() widget.deleteLater()
[docs]class OtherTasksPopUp(BaseMsvPopUp): """ A popup dialog to provide other task options. :ivar runPredictions: Signal emitted when the corresponding task is selected. :vartype runPredictions: `QtCore.pyqtSignal` :ivar secondaryStructure: Signal emitted when the corresponding task is selected. :vartype secondaryStructure: `QtCore.pyqtSignal` :ivar solventAccessibility: Signal emitted when the corresponding task is selected. :vartype solventAccessibility: `QtCore.pyqtSignal` :ivar domainArrangement: Signal emitted when the corresponding task is selected. :vartype domainArrangement: `QtCore.pyqtSignal` :ivar disorderedRegions: Signal emitted when the corresponding task is selected. :vartype disorderedRegions: `QtCore.pyqtSignal` :ivar disulfideBridges: Signal emitted when the corresponding task is selected. :vartype disulfideBridges: `QtCore.pyqtSignal` :ivar findHomologs: Signal emitted when the corresponding task is selected. :vartype findHomologs: `QtCore.pyqtSignal` :ivar homologResults: Signal emitted when the corresponding task is selected. :vartype homologResults: `QtCore.pyqtSignal` :ivar findFamily: Signal emitted when the corresponding task is selected. :vartype findFamily: `QtCore.pyqtSignal` :ivar buildHomologyModel: Signal emitted when the corresponding task is selected. :vartype buildHomologyModel: `QtCore.pyqtSignal` :ivar analyzeBindingSite: Signal emitted when the corresponding task is selected. :vartype analyzeBindingSite: `QtCore.pyqtSignal` :ivar compareSequences: Signal emitted when the corresponding task is selected. :vartype compareSequences: `QtCore.pyqtSignal` :ivar computeSequenceDescriptors: Signal emitted when the corresponding task is selected. :vartype computeSequenceDescriptors: `QtCore.pyqtSignal` """ model_class = gui_models.PageModel _SEPARATOR = object() LINE_SEPARATOR_WIDTH = 175 LINE_SEPARATOR_HEIGHT = 2 TASK_LAYOUT_CONTENT_MARGINS = (10, 5, 10, 0) TASK_LAYOUT_SPACING = 20 SUB_TASK_LAYOUT_CONTENT_MARGINS = (10, 10, 10, 10) SUB_TASK_LAYOUT_SPACING = 8 SUB_TASK_LAYOUT_SEPARATOR_SPACING = 2 runPredictions = QtCore.pyqtSignal() secondaryStructure = QtCore.pyqtSignal() solventAccessibility = QtCore.pyqtSignal() domainArrangement = QtCore.pyqtSignal() disorderedRegions = QtCore.pyqtSignal() disulfideBonds = QtCore.pyqtSignal() findHomologs = QtCore.pyqtSignal() homologResults = QtCore.pyqtSignal() findFamily = QtCore.pyqtSignal() buildHomologyModel = QtCore.pyqtSignal() analyzeBindingSite = QtCore.pyqtSignal() copySeqsToNewTab = QtCore.pyqtSignal(list) computeSequenceDescriptors = QtCore.pyqtSignal() BLAST_RESULTS_TEXT = "Homologs Search Results..."
[docs] def __init__(self, parent=None): """ See parent class for more information. """ super().__init__(parent) self.setObjectName('other_tasks_pop_up') self.setStyleSheet(stylesheets.OTHER_TASKS_POP_UP_DIALOG)
[docs] def defineMappings(self): M = self.model_class analyze_dialog_spec = mappers.TargetSpec( setter=self._analyze_dialog.updateDialog) return [ (analyze_dialog_spec, M), (self._updateCompareSeqsDialog, M), ] # yapf: disable
[docs] def getSignalsAndSlots(self, model): return [ (model.options.include_gapsChanged, self._updateCompareSeqsDialog), (model.alnChanged, self._updateCompareSeqsDialog), ] # yapf: disable
def _getStructureAndPropertyPredictionsMapping(self): return [ ('Run All Predictions', self.runPredictions), self._SEPARATOR, ('Secondary Structure', self.secondaryStructure), ('Solvent Accessibility', self.solventAccessibility), ('Domain Arrangement', self.domainArrangement), ('Disordered Regions', self.disorderedRegions), ('Disulfide Bonds', self.disulfideBonds), ] def _getHomologyModelingAndAnalysisMapping(self): return [ ('Find Homologs (BLAST)...', self.findHomologs), (self.BLAST_RESULTS_TEXT, self.homologResults), ('Find Family (Pfam)', self.findFamily), self._SEPARATOR, ('Build Homology Model...', self.buildHomologyModel), self._SEPARATOR, ('Analyze Binding Sites...', self._openAnalyzeBindingSitesDialog), ('Compare Sequences...', self.openCompareSequencesDialog), ] def _getPropertyCalculationsMapping(self): return [ ('Compute Sequence Descriptors...', self.computeSequenceDescriptors), ('Aggregate Residue Property...', self._openAggregateResiduePropertyDialog), ] # yapf:disable def _openAggregateResiduePropertyDialog(self): """ Open the Aggregate Residue Property dialog. """ dlg = dialogs.AggregatePropertyDialog(self.model.aln) dlg.seqDescriptorsUpdated.connect(self._onSeqPropsUpdated) dlg.run(modal=True) def _onSeqPropsUpdated(self, seq_prop): """ Update the 'sequence_proeprties' list with the newly calculated ( aggregate of residue property) property. If the property is already in the list, remove it first in order to include the value for new sequences,if any. :param seq_prop: The new sequence property calculated. :type seq_prop: properties.SequenceProperty """ try: self.model.options.sequence_properties.remove(seq_prop) except ValueError: pass self.model.options.sequence_properties.append(seq_prop) def _setupWidgets(self): """ See parent class for more information. """ self._analyze_dialog = dialogs.AnalyzeBindingSiteDialog(parent=self) self._compare_seqs_dlg = None # main horizontal layout for tasks tasks_layout = self.createHorizontalLayout() tasks_layout.setContentsMargins(*self.TASK_LAYOUT_CONTENT_MARGINS) tasks_layout.setSpacing(self.TASK_LAYOUT_SPACING) tasks_layout.setAlignment(Qt.AlignTop) # Left list of tasks left_layout = self.createVerticalLayout(indent=False) left_layout.setSpacing(0) left_layout.setAlignment(Qt.AlignTop) tasks = self._getStructureAndPropertyPredictionsMapping() left_layout = self._makeTaskSection( 'Structure and Property Predictions', tasks, left_layout) tasks = self._getPropertyCalculationsMapping() left_layout = self._makeTaskSection('Property Calculations', tasks, left_layout) tasks_layout.addLayout(left_layout) # Right lsit of tasks right_layout = self.createVerticalLayout(indent=False) right_layout.setSpacing(0) right_layout.setAlignment(Qt.AlignTop) tasks = self._getHomologyModelingAndAnalysisMapping() right_layout = self._makeTaskSection('Homology Modeling and Analysis', tasks, right_layout) tasks_layout.addLayout(right_layout) self.main_layout.addLayout(tasks_layout) def _makeTaskSection(self, task_header, tasks_mapping, layout): """ Create a section of tasks, each represented by a tuple with a string and a slot that the label should trigger. :param task_header: The header for this section of tasks :type task_header: str :param tasks_mapping: List of tuples, or self._SEPARATOR to represent that a line should be used to separate two items. Tuples should consist of the string to use for the label and a slot that should be called when the label is clicked. :type tasks_mapping: list[tuple or self._SEPARATOR] :param layout: The layout to insert the new task section into. :type layout: QtWidgets.QLayout """ header = QtWidgets.QLabel(task_header) # A strong focus object is required for the popup dialog to # behave correctly, see note in parent class. header.setFocusPolicy(Qt.StrongFocus) header.setObjectName('other_tasks_header_lbl') layout.addWidget(header) # sub tasks_mapping sub layout sub_tasks_layout = self.createVerticalLayout() sub_tasks_layout.setContentsMargins( *self.SUB_TASK_LAYOUT_CONTENT_MARGINS) sub_tasks_layout.setSpacing(self.SUB_TASK_LAYOUT_SPACING) for sub_task in tasks_mapping: if sub_task is self._SEPARATOR: # add spacing after the separator sub_tasks_layout.addWidget(self._makeSeparator()) sub_tasks_layout.addSpacing( self.SUB_TASK_LAYOUT_SEPARATOR_SPACING) continue sub_task_name, sub_task_slot = sub_task sub_task_label = ClickableLabel(sub_task_name) sub_task_label.setObjectName('other_tasks_sub_task_lbl') sub_task_label.clicked.connect(self.close) sub_task_label.clicked.connect(sub_task_slot) if sub_task_name == self.BLAST_RESULTS_TEXT: sub_task_label.setToolTip("Displays a BLAST results dialog") sub_tasks_layout.addWidget(sub_task_label) layout.addLayout(sub_tasks_layout) return layout def _makeSeparator(self): """ Helper method to create a separator widget. """ line_separator = QtWidgets.QFrame() line_separator.setObjectName('other_tasks_separator') line_separator.setFrameShape(QtWidgets.QFrame.HLine) line_separator.setLineWidth(self.LINE_SEPARATOR_HEIGHT) line_separator.setFixedWidth(self.LINE_SEPARATOR_WIDTH) return line_separator
[docs] def openCompareSequencesDialog(self): aln = self.model.aln if self._compare_seqs_dlg is None: dialog = dialogs.CompareSeqsDialog(aln, parent=self.parent()) dialog.copySeqsToNewTab.connect(self.copySeqsToNewTab) self._compare_seqs_dlg = dialog self._compare_seqs_dlg.run() return self._compare_seqs_dlg
def _updateCompareSeqsDialog(self): if self._compare_seqs_dlg is not None: self._compare_seqs_dlg.update(self.model.aln, self.model.options.include_gaps) def _openAnalyzeBindingSitesDialog(self): aln = self.model.aln has_hidden_seqs = dialogs.prompt_for_hidden_seqs(self, aln) if has_hidden_seqs: return self._analyze_dialog.run()
[docs]class PopUpDialogButton(pop_up_widgets.ToolButtonWithPopUp): """ A checkable button that brings up a popup dialog when toggled. """
[docs] def __init__(self, parent, pop_up_class, text): super().__init__(parent, pop_up_class, text=text) self.setFocusPolicy(QtCore.Qt.ClickFocus) self.setPopupHalign(self.ALIGN_LEFT) self.setPopupValign(self.ALIGN_TOP)
@property def popup_dialog(self): return self._pop_up
[docs]class PopUpDialogButtonWithIndicator(PopUpDialogButton): """ A popup button that draws the menu indicator, which adds an arrow to the toolbutton """ INDICATOR_WIDTH = 8
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.indicator = QtGui.QPixmap( ':/msv/icons/arrow-open.png').scaledToWidth( self.INDICATOR_WIDTH, QtCore.Qt.SmoothTransformation)
[docs] def sizeHint(self): """ Make some extra room for the indicator arrow """ return super().sizeHint() + QtCore.QSize(self.indicator.width(), 0)
[docs] def paintEvent(self, ev): opt = QtWidgets.QStyleOptionToolButton() self.initStyleOption(opt) painter = QtWidgets.QStylePainter(self) painter.drawComplexControl(QtWidgets.QStyle.CC_ToolButton, opt) target = QtCore.QPoint() # Give indicator a bit of padding to the right padding_right = 8 target.setX(opt.rect.width() - self.indicator.width() - padding_right) # Put the indicator in the center, shifted down a bit target.setY((opt.rect.height() - self.indicator.height()) // 2 + 1) painter.drawPixmap(target, self.indicator)
[docs]class EllipsisPopUpDialogToolButton(QtWidgets.QToolButton): """ An ellipsis options tool button is a checkable widget, which shows an ellipsis button on top to indicate that a pop-up dialog is available whenever the user hovers over the widget. """ POPUP_VISIBLE_PROPERTY = 'popup_visible'
[docs] def __init__(self, pop_up_class, parent=None): """ Sets up the tool button and the ellipsis button on top. :param pop_up_class: The class of the pop up widget to show when the ellipsis button is clicked or the checkable tool button is right-clicked. :type pop_up_class: subclass of `PopUp` See parent `QtWidgets.QToolButton` for further documentation. """ super().__init__(parent) self.setCheckable(True) # ellipsis tool button that opens a popup dialog self.ellipsis_btn = _EllipsisPopUpButton(self, pop_up_class) self.ellipsis_btn.popUpClosing.connect(self.onPopUpClosed) # the ellipsis button is only shown during hover events or when the # popup dialog is open self._updateWidgetStyle(False) self.setStyleSheet(stylesheets.ELLIPSIS_POP_UP_DIALOG_TOOL_BUTTON)
@property def popup_dialog(self): return self.ellipsis_btn.popup_dialog
[docs] def showPopUpDialog(self): """ Shows the popup dialog. """ self._updateWidgetStyle(True) self.ellipsis_btn.click()
[docs] def onPopUpClosed(self): """ Update the widget style once the popup closes, only if the user is not actively hovering over the widget. """ mouse_pos = QtGui.QCursor.pos() mouse_pos = self.mapFromGlobal(mouse_pos) if not self.rect().contains(mouse_pos): self._updateWidgetStyle(False)
[docs] def enterEvent(self, event): # See parent `QtWidgets.QToolButton` for documentation self._updateWidgetStyle(True) super().enterEvent(event)
[docs] def leaveEvent(self, event): # See parent `QtWidgets.QToolButton` for documentation e_btn = self.ellipsis_btn mouse_pos = e_btn.mapFromGlobal(QtGui.QCursor.pos()) mouse_over_e_btn = e_btn.rect().contains(mouse_pos) should_hide = not (e_btn.isChecked() or mouse_over_e_btn) # Update the widget style for a mouse leave event only if the popup # dialog is not currently open and the mouse is not over the ellipsis # button. if should_hide: self._updateWidgetStyle(False) super().leaveEvent(event)
[docs] def mousePressEvent(self, event): # See parent `QtWidgets.QToolButton` for documentation # Open the popup dialog if the main tool button is right-clicked if event.button() == QtCore.Qt.RightButton: self.showPopUpDialog() else: super().mousePressEvent(event)
def _updateWidgetStyle(self, state): """ Update the widget style according to given state. This updates the ellipsis button to be visible or hidden, and sets the hover style of the tool button while the popup dialog is open. :param state: whether the styling should be applied or not. :type state: bool """ if state: self.ellipsis_btn.show() else: self.ellipsis_btn.hide() self.setProperty(self.POPUP_VISIBLE_PROPERTY, state) qt_utils.update_widget_style(self)
class _EllipsisPopUpButton(PopUpDialogButton): """ :cvar BUTTON_OFFSET: Vertical overlap between the ellipsis button and the tool button :vartype BUTTON_OFFSET: int """ BUTTON_OFFSET = 5 def __init__(self, parent, pop_up_class): """ :param parent: Toolbutton to show ellipsis button over :type parent: EllipsisPopUpDialogToolButton :param pop_up_class: The class of pop up widget to show when the ellipsis button is clicked :type pop_up_class: PopUp """ super().__init__(parent, pop_up_class, text=None) self.setObjectName('ellipsis_btn') self.setArrowType(QtCore.Qt.NoArrow) self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) if sys.platform == 'darwin': self.popup_dialog.visibilityChanged.connect( self._macActiveWindowWorkaround) def show(self): """ Show the ellipsis button if neither it nor the popup widget are visible """ if not self.isVisible() and not self.popup_dialog.isVisible(): self._moveAboveParent() super().show() def leaveEvent(self, event): """ Hide the ellipsis button on mouse leave if the mouse isn't over the tool button """ btn = self.parent() mouse_pos = btn.mapFromGlobal(QtGui.QCursor.pos()) mouse_over_btn = btn.rect().contains(mouse_pos) if not mouse_over_btn: self.hide() super().leaveEvent(event) def _moveAboveParent(self): """ Move the ellipsis button above and slightly overlapping the tool button """ self.adjustSize() # Force btn to update height e_height = self.height() btn = self.parent() btn_pos = btn.mapToGlobal(btn.rect().topLeft()) x, y = btn_pos.x(), btn_pos.y() ellipsis_pos = QtCore.QPoint(x, y - e_height + self.BUTTON_OFFSET) self.move(ellipsis_pos) if sys.platform == 'darwin': def _macActiveWindowWorkaround(self, visible): """ On Mac, the main window does not become active if the popup gets hidden while the ellipsis button is hidden. If the main window is not active, its tooltips don't work. This explicitly activates the main window whenever the popup gets hidden. """ if not visible: app = QtWidgets.QApplication.instance() app.setActiveWindow(self.popup_dialog.parent())
[docs]def make_pop_up_tool_button(parent, pop_up_class, tooltip="", obj_name="", text="", icon="", indicator=False): """ Helper function to setup a pop-up QToolButton (popups.PopUpDialogButton). :param parent: the parent for this button. This is very important to set, as the pop-up will require correct parentage to propagate the pop-up dialog all the way to the top to display properly. :type parent: `QtWidgets.QWidget` :param pop_up_class: the class type of the popup. :type pop_up_class: subclass of `pop_up_widgets.PopUp` :param tooltip: tooltip the toolbutton should display. :type tooltip: str :param obj_name: object name for access from style sheet. :type obj_name: str :param text: to be shown beside the icon. :type text: str :param icon: the filename of the icon, which should live in msv/icons. :type icon: str :param bool indicator: Whether to draw a menu indicator. Only recommended if text is set (otherwise arrow will appear over the icon). :return: the created toolbutton with the given args. :rtype: `popups.PopUpDialogButton` """ BtnCls = PopUpDialogButtonWithIndicator if indicator else PopUpDialogButton btn = BtnCls(parent=parent, pop_up_class=pop_up_class, text=text) btn.setToolTip(tooltip) btn.setObjectName(obj_name) # Hide built-in indicator arrow btn.setArrowType(QtCore.Qt.NoArrow) if icon: q_icon = QtGui.QIcon(ICONS_PATH + icon) btn.setIcon(q_icon) return btn