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

"""
Widgets for the "Define Custom Color Scheme" dialog and color popup.
"""

import copy

from schrodinger.application.msv.gui import color
from schrodinger.infra import util as infra_util
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 delegates
from schrodinger.ui.qt import pop_up_widgets
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import table_helper


[docs]class ColorSchemeComboBoxMixin: """ A mixin for QComboBoxes that list color schemes. """ _SEPARATOR = object() CUSTOM = "Custom" _COLOR_SEQ_OPTS = ( color.SideChainChemistryScheme, color.ResidueTypeScheme, color.SimilarityScheme, color.HydrophobicityKDScheme, color.HydrophobicityHWScheme, color.TaylorScheme, color.ClustalXScheme, color.SecondaryStructureScheme, color.BFactorScheme, _SEPARATOR, color.HelixPropensityScheme, color.StrandPropensityScheme, color.TurnPropensityScheme, color.ExposureTendencyScheme, color.StericGroupScheme, _SEPARATOR, color.WorkspaceScheme, color.ChainNameScheme, color.PositionScheme, color.ResidueChargeScheme, CUSTOM ) # yapf: disable def _populateComboBox(self, show_workspace, items_to_remove=(), items_to_add=()): """ Populate the combo box using `self._COLOR_SEQ_OPTS`. Should be called in `__init__`. :param show_workspace: Whether to include the workspace color scheme in the combo box. :type show_workspace: bool :param items_to_remove: Items from `self._COLOR_SEQ_OPTS` that should not be included in the combo box. :type items_to_remove: Iterable :param items_to_add: Additional items to include in the combo box. :type items_to_add: Iterable """ to_add = list(self._COLOR_SEQ_OPTS) if not show_workspace: to_add.remove(color.WorkspaceScheme) for item in items_to_remove: to_add.remove(item) to_add.extend(items_to_add) self._custom_index = to_add.index(self.CUSTOM) self._custom_shown = False to_add.remove(self.CUSTOM) self._addItemsToComboBox(to_add) return to_add def _addItemsToComboBox(self, to_add): """ Add items to the combo box. The list passed in should be based on `self._COLOR_SEQ_OPTS`. :param to_add: A list of items to add. Each item may be either a color scheme, a string, or `_SEPARATOR`. :type to_add: Iterable(color.AbstractRowColorScheme or str or object) """ for i, item in enumerate(to_add): if item is self._SEPARATOR: self.insertSeparator(i) elif isinstance(item, str): # "Custom" or "Define Custom Scheme..." self.addItem(item, item) elif issubclass(item, color.AbstractRowColorScheme): self.addItem(item.NAME, item) else: raise RuntimeError("Unrecognized option") def _showCustom(self, value): """ Show the "Custom" option in the combo box. :param value: The Qt.UserRole data to set for the custom option. :type value: object """ if self._custom_shown: self.setItemData(self._custom_index, value) else: self.insertItem(self._custom_index, self.CUSTOM, value) self._custom_shown = True def _hideCustom(self): """ Hide the custom option in the combo box. """ if self._custom_shown: self.removeItem(self._custom_index) self._custom_shown = False
[docs]class ColorSchemeComboBox(ColorSchemeComboBoxMixin, swidgets.SComboBox): """ The "Start with:" combo box in the "Define Custom Color Scheme" dialog. """
[docs] def __init__(self, parent=None): super().__init__(parent) # We can't customize the workspace scheme, so that's always omitted. self._populateComboBox(show_workspace=False)
[docs] def currentScheme(self, ramp=None): """ Return an instance of the currently selected color scheme. :param ramp: The color ramp to use for the scheme. Only has an effect for the B-factor and Residue Position schemes. Will be ignored otherwise. If not given, the existing color ramp will be used (i.e. for custom schemes, the ramp the scheme was defined with; for non- custom schemes, the default ramp). :type ramp: schrodinger.structutils.ColorRamp :return: The current color scheme :rtype: color.AbstractRowColorScheme """ if self.currentText() == self.CUSTOM: custom_scheme = self.currentData() if (isinstance(custom_scheme, color.AbstractColorRampOnlyScheme) and ramp is not None): scheme_cls = type(custom_scheme) return scheme_cls(ramp, custom=True) else: return copy.deepcopy(self.currentData()) else: scheme_cls = self.currentData() if issubclass(scheme_cls, color.AbstractColorRampOnlyScheme): return scheme_cls(ramp, custom=True) else: return scheme_cls(custom=True)
[docs] def setCurrentScheme(self, scheme): """ Set the current item to be the specified color scheme. If a custom color scheme is passed in, the custom scheme must have been set via `setCustomScheme`. :param scheme: The color scheme :type scheme: color.AbstractRowColorScheme """ if scheme.custom: if not self._custom_shown: raise ValueError("No custom scheme set") self.setCurrentIndex(self._custom_index) else: self.setCurrentData(type(scheme))
[docs] def clearCustomScheme(self): """ Remove the custom color scheme option from the combo box. """ self._hideCustom()
[docs] def setCustomScheme(self, scheme): """ Add a custom color scheme option to the combo box. Calling `currentScheme` while "Custom" is selected in the combo box will return a copy of this scheme. :param scheme: The custom color scheme :type scheme: color.AbstractRowColorScheme """ self._showCustom(scheme)
class ColorSchemeColumns(table_helper.TableColumns): Key = table_helper.Column("Value/Range") Color = table_helper.Column("Color", editable=True)
[docs]class ColorSchemeTableModel(table_helper.RowBasedTableModel): """ :cvar schemeChanged: A signal emitted when the color scheme changes. Emitted with the new scheme. :vartype schemeChanged: QtCore.pyqtSignal(color.AbstractRowColorScheme) """ Column = ColorSchemeColumns schemeChanged = QtCore.pyqtSignal(color.AbstractRowColorScheme)
[docs] def __init__(self, parent=None): super().__init__(parent) self._keys = [] self._scheme = None
[docs] def getScheme(self): """ Return the current color scheme. """ return self._scheme
[docs] @table_helper.model_reset_method def loadData(self, scheme): """ Load in a copy of the specified scheme. :param scheme: The color scheme to load. :type scheme: color.AbstractRowColorScheme """ scheme = copy.deepcopy(scheme) self._keys = scheme.getKeys() self._rows = self._scheme = scheme self.schemeChanged.emit(copy.deepcopy(scheme))
[docs] @table_helper.model_reset_method def clearData(self): """ Clear all data from this model. """ self._keys = [] self._rows = [] self._scheme = None
[docs] def rowCount(self, parent=None): # See Qt documentation for method documentation return len(self._keys)
def _getRowFromIndex(self, index): # See RowBasedModelMixin for method documentation. return self._keys[index.row()] @table_helper.data_method(Qt.DisplayRole) def _displayData(self, col, key): if col == self.Column.Key: return self._scheme.prettyKeyName(key) @table_helper.data_method(Qt.DecorationRole) def _decorationData(self, col, key): if col == self.Column.Color: return self._scheme.getBrushByKey(key).color() def _setData(self, col, key, value, role, row_num): # See RowBasedModelMixin for method documentation. if col != self.Column.Color or role != Qt.EditRole: return False self._scheme.setColor(key, value.red(), value.green(), value.blue()) self.schemeChanged.emit(copy.deepcopy(self._scheme)) return True
[docs]class ColorDelegate(delegates.AbstractCustomDelegate):
[docs] def __init__(self, parent=None): super().__init__(parent) self._outline_pen = QtGui.QPen(Qt.darkGray, 1)
[docs] def initStyleOption(self, option, index): super().initStyleOption(option, index) decoration_data = index.data(Qt.DecorationRole) if decoration_data is not None: # QTreeView::initStyleOption will convert Qt::DecorationRole colors # into icons, but we want an outline around the icon so we generate # it ourselves size = option.decorationSize pixmap = QtGui.QPixmap(option.decorationSize) painter = QtGui.QPainter(pixmap) painter.setPen(self._outline_pen) painter.setBrush(decoration_data) rect = QtCore.QRect(0, 0, size.width() - 1, size.height() - 1) painter.drawRect(rect) painter.end() option.icon = QtGui.QIcon(pixmap)
[docs] def paint(self, painter, option, index): option, style = self.paintItemBackground(painter, option, index) rect = style.alignedRect(Qt.LeftToRight, Qt.AlignCenter, option.decorationSize, option.rect) icon = option.icon selected = option.state & style.State_Selected mode = icon.Selected if selected else icon.Normal icon.paint(painter, rect, mode=mode)
[docs] def createEditor(self, parent, option, index): return QtWidgets.QColorDialog(parent)
[docs] def updateEditorGeometry(self, editor, option, index): # our editor is a separate dialog, not a widget within the same window, # so we don't want to set its geometry pass
[docs] def setEditorData(self, editor, index): cur_color = index.data(Qt.DecorationRole) editor.setCurrentColor(cur_color)
[docs] def setModelData(self, editor, model, index): if editor.result() == editor.Accepted: model.setData(index, editor.selectedColor())
[docs]class ColorSchemeTableView(QtWidgets.QTableView): Column = ColorSchemeColumns
[docs] def __init__(self, parent=None): super().__init__(parent) self._color_delegate = ColorDelegate(self) self.setSelectionBehavior(self.SelectRows) self.setEditTriggers(self.AllEditTriggers) self.setAlternatingRowColors(True) self.verticalHeader().hide()
[docs] def setModel(self, model): super().setModel(model) self.setItemDelegateForColumn(self.Column.Color, self._color_delegate) header = self.horizontalHeader() header.setSectionResizeMode(self.Column.Key, header.Stretch)
[docs]class EditMinMaxPopUp(pop_up_widgets.PopUp): """ A pop up for editing the minimum and maximum colored values for a residue property color scheme color ramp. Appears when the user clicks on `EditMinMaxToolButton`. """ dataChanged = QtCore.pyqtSignal(float, float)
[docs] def setup(self): # We do this import here to avoid a circular import that fails if and # only if color_widgets gets imported before anything from the dialogs # directory from schrodinger.application.msv.gui.dialogs import edit_min_max_ui self._aln_min = 0 self._aln_max = 0 self._cur_min = 0 self._cur_max = 0 self.ui = edit_min_max_ui.Ui_Form() self.ui.setupUi(self) self.ui.accept_btn.clicked.connect(self._onAccepted) self.ui.detect_lbl.linkActivated.connect(self.restoreAlnValues) self._validator = QtGui.QDoubleValidator(self) self.ui.min_le.setValidator(self._validator) self.ui.max_le.setValidator(self._validator) self.ui.min_le.textEdited.connect(self._updateButtonsEnabled) self.ui.max_le.textEdited.connect(self._updateButtonsEnabled) self.ui.min_le.returnPressed.connect(self._onAccepted) self.ui.max_le.returnPressed.connect(self._onAccepted)
def _onAccepted(self): """ Update the color scheme when the user clicks on the accept button or hits enter on one of the line edits. Has no effect if either of the line edits are empty. """ try: min_val = float(self.ui.min_le.text()) max_val = float(self.ui.max_le.text()) except ValueError: return if min_val > max_val: min_val, max_val = max_val, min_val self.dataChanged.emit(min_val, max_val) self.close() def _updateButtonsEnabled(self): """ Update whether the accept button and "Detect" link are enabled. The accept button is only enabled if the current values are valid (i.e. non-empty) and different from the current color ramp settings. The "Detect" link is only enabled if the current values are different from the alignment. """ try: min_val = float(self.ui.min_le.text()) max_val = float(self.ui.max_le.text()) except ValueError: self.ui.accept_btn.setEnabled(False) self.ui.detect_lbl.setEnabled(True) return matches_cur_vals = (abs(min_val - self._cur_min) < 10e-10 and abs(max_val - self._cur_max) < 10e-10) self.ui.accept_btn.setEnabled(not matches_cur_vals) matches_aln_vals = (abs(min_val - self._aln_min) < 10e-10 and abs(max_val - self._aln_max) < 10e-10) self.ui.detect_lbl.setEnabled(not matches_aln_vals) # Setting the label to disabled doesn't have any effect on the # appearence of links (at least on Windows), so we remove the hyperlink # markup when we disable the label if matches_aln_vals: self.ui.detect_lbl.setText("Detect") else: self.ui.detect_lbl.setText('<a href>Detect</a>') def _setLineEditVals(self, min_val, max_val): """ Set the minimum and maximum values to display in the line edits. :param min_val: The minimum value :type min_val: float or int :param max_val: The maximum value :type max_val: float or int """ self.ui.min_le.setText(f"{min_val:.8g}") self.ui.max_le.setText(f"{max_val:.8g}")
[docs] def setMinMax(self, min_val, max_val): """ Set the minimum and maximum values to display in the line edits based on the current color ramp settings. The accept button will be enabled when the user-entered values difer from these values. :param min_val: The minimum value :type min_val: float or int :param max_val: The maximum value :type max_val: float or int """ self._setLineEditVals(min_val, max_val) self._cur_min = min_val self._cur_max = max_val self._updateButtonsEnabled()
[docs] def setAlnMinMax(self, min_val, max_val): """ Set the minimum and maximum values from the alignment. The "Detect" link will be enabled when the user-entered values differ from the alignment values. :param min_val: The minimum value :type min_val: float or int :param max_val: The maximum value :type max_val: float or int """ self._aln_min = min_val self._aln_max = max_val self._updateButtonsEnabled()
[docs] def restoreAlnValues(self): """ Put the minimum and maximum values from the alignment into the line edits. """ self._setLineEditVals(self._aln_min, self._aln_max) self._updateButtonsEnabled()
[docs]class EditMinMaxToolButton(pop_up_widgets.ToolButtonWithPopUp): """ A toolbutton for editing the minimum and maximum colored values for a residue property color scheme color ramp. Launches the `EditMinMaxPopUp` when clicked. .. note:: `setAlnMinMax` and `setMinMax` must be called before showing the pop up. """ valuesChanged = QtCore.pyqtSignal(float, float) _changingValues = infra_util.flag_context_manager("_changing_values")
[docs] def __init__(self, parent): super().__init__(parent, EditMinMaxPopUp, Qt.NoArrow) self._changing_values = False
[docs] @infra_util.skip_if("_changing_values") def popUpUpdated(self, min_val, max_val): with self._changingValues(): self.valuesChanged.emit(min_val, max_val)
[docs] def setMinMax(self, min_val, max_val): """ Set the minimum and maximum values to display in the line edits based on the current color ramp settings. The accept button will be enabled when the user-entered values difer from these values. :param min_val: The minimum value :type min_val: float or int :param max_val: The maximum value :type max_val: float or int """ self._pop_up.setMinMax(min_val, max_val)
[docs] def setAlnMinMax(self, min_val, max_val): """ Set the minimum and maximum values from the alignment. The "Detect" link will be enabled when the user-entered values differ from the alignment values. :param min_val: The minimum value :type min_val: float or int :param max_val: The maximum value :type max_val: float or int """ self._pop_up.setAlnMinMax(min_val, max_val)