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