Source code for schrodinger.application.msv.gui.toolbar
import itertools
import sys
import weakref
from functools import partial
import inflect
import schrodinger
from schrodinger.application.msv import seqio
from schrodinger.application.msv.gui import alignment_pane
from schrodinger.application.msv.gui import dialogs
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui import popups
from schrodinger.application.msv.gui import stylesheets
from schrodinger.application.msv.gui.homology_modeling import hm_models
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.models import mappers
from schrodinger.models import parameters
from schrodinger.protein import predictors
from schrodinger.protein.tasks import blast
from schrodinger.protein.tasks import pfam
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.tasks import queue
from schrodinger.tasks.tasks import AbstractTask
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.swidgets import StyleMixin
from schrodinger.utils import scollections
from schrodinger.ui.qt import utils as qt_utils
from . import toolbar_ui
LINE_SEPARATOR_WIDTH = 1
LINE_SEPARATOR_HEIGHT = 40
[docs]class MSVToolbar(widgetmixins.InitMixin, StyleMixin, QtWidgets.QToolBar):
    """
    The main toolbar for the MSV GUI.
    :cvar importFromWorkspace: Signal emitted when import from workspace is
        requested.
    :vartype importFromWorkspace: QtCore.pyqtSignal
    :cvar importSelectedEntries: Signal emitted when import from selected
        entries is requested.
    :vartype importSelectedEntries: QtCore.pyqtSignal
    :cvar importFile: Signal emitted when import from file is requested.
    :vartype importFile: QtCore.pyqtSignal
    :cvar requestFind: Signal emitted for request to run a substring or pattern
        search on the sequences.
    :vartype requestFind: QtCore.pyqtSignal
    :cvar requestFetch: Signal emitted for request to fetch a PDB or sequence
        given the search terms.
    :vartype requestFetch: QtCore.pyqtSignal
    :cvar nextPatternMatch: Signal emitted when the next occurrence of the
        pattern match is requested during a find search.
    :vartype nextPatternMatch: QtCore.pyqtSignal
    :cvar prevPatternMatch: Signal emitted when the prev occurrence of the
        pattern match is requested during a find search.
    :vartype nextPatternMatch: QtCore.pyqtSignal
    :ivar resetPickRequested: Signal emitted when a pick banner "Reset" button
        is clicked. Emitted with a `picking.PickMode`.
    :ivar pickClosed: Signal emitted when a pick banner is closed. Emitted with
        a `picking.PickMode`.
    :cvar FETCH_VALIDATORS: List of methods that check whether a string is a
        valid ID for Fetch
    :vartype FETCH_VALIDATORS: list(callable)
    """
    importFromWorkspace = QtCore.pyqtSignal()
    importSelectedEntries = QtCore.pyqtSignal()
    importFile = QtCore.pyqtSignal()
    requestFind = QtCore.pyqtSignal(str)
    requestFetch = QtCore.pyqtSignal(str)
    nextPatternMatch = QtCore.pyqtSignal()
    prevPatternMatch = QtCore.pyqtSignal()
    resetPickRequested = QtCore.pyqtSignal(object)
    pickClosed = QtCore.pyqtSignal(object)
    ui_module = toolbar_ui
    # tooltips
    FIND_SEQUENCE_SUBSTRING_TT = """
    Type in a sequence of single-letter residues and press Enter to locate all
    matching substrings.
    """
    FIND_PROSITE_PATTERN_TT = """
    Choose a saved PROSITE pattern from the Bookmark menu at right or type
    in a pattern and press Enter to locate all matching sequences. For help
    with syntax and examples, select Manage Saved Patterns... from
    the menu.
    """
    FETCH_PDB_STRUCTURES_TT = """
    To fetch a PDB structure, type the PDB ID and press Enter.
    To fetch multiple PDB structures, list the PDB IDs separated by commas.
    To fetch a single chain, append the chain ID to the PDB ID (e.g.2hbaA).
    """
    FETCH_UNIPROT_OR_ENTREZ_SEQUENCE_TT = """
    To fetch a protein sequence from either the UniProt or Entrez database, 
    type the appropriate code (e.g. P17787 for UniProt, NP_778203 for Entrez)
    and press Enter. The UniProt name may also be used (e.g. ACHB2_HUMAN).
    """
    FIND_TT = "Search"
    FETCH_TT = "Download"
    INVALID_FF_TT = "Text is not a valid residue pattern or sequence code"
    FETCH_VALIDATORS = [
        seqio.valid_pdb_id, seqio.valid_entrez_id, seqio.valid_uniprot_id,
        seqio.valid_swiss_prot_name
    ]
    FIND_SEQUENCE_SUBSTRING_PT = 'enter sequence substring'
    FIND_PROSITE_PATTERN_PT = 'pick / enter pattern'
    FETCH_PDB_STRUCTURES_PT = ' or PDB IDs'
    FETCH_UNIPROT_OR_ENTREZ_SEQUENCE_PT = ' or sequence code / name'
    # Toolbar states
    WS_PAGE, LOAD_PAGE, LOAD_FROM_FILE_PAGE, FIND_SEQ_PAGE = range(4)
    DEFAULT_PAGE, EDIT_MODE_PAGE = range(2)
    SEARCH_PAGE, DOWNLOAD_PAGE = range(2)
    # Content Load Toolbar
    LOAD_FROM_OPTIONS = ['Workspace', 'Selected Entries', 'File']
    # Tasks Toolbar
    ALIGN_TXT = 'Align'
    OTHER_TASKS_TXT = 'Other Tasks'
    ALIGN_TOOLTIP = ''
    OTHER_TASKS_TOOLTIP = ''
[docs]    def initSetUp(self):
        """
        See parent class docstring for more details.
        """
        super().initSetUp()
        self.setObjectName('msv_toolbar')
        self.setStyleSheet(stylesheets.MSV_TOOLBAR)
        self.setFloatable(False)
        self.setMovable(False)
        # add any remaining widgets to toolbar
        self._setUpContentLoadToolbar()
        self._setUpFindToolbar()
        self._setUpTaskToolbar()
        # set up each sub toolbar's signals
        self._setUpContentLoadToolbarSignals()
        self._setUpFindToolbarSignals()
        self._setUpTaskToolbarSignals()
        self.ui.view_load_from_cb.currentIndexChanged.connect(
            self._updateViewDownloadBtn)
[docs]    def initLayOut(self):
        """
        Set toolbar layout.
        See parent class docstring for more details.
        """
        super().initLayOut()
        self.widget_layout.setContentsMargins(0, 0, 0, 0)
        self.main_layout.setContentsMargins(0, 0, 0, 0)
        sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred,
                                   QtWidgets.QSizePolicy.Fixed)
        sp.setHorizontalStretch(0)
        sp.setVerticalStretch(0)
        self.setSizePolicy(sp)
[docs]    def initSetDefaults(self):
        """
        Set toolbar defaults.
        See parent class docstring for more details.
        """
        super().initSetDefaults()
        self._setFindToolbarDefaults()
[docs]    def setWidgetLayout(self):
        """
        Set the widget layout. A QToolBar's layout does not allow nested
        layouts so we create a container widget to hold the widget layout.
        """
        self.setContentsMargins(0, 0, 0, 0)
        top_level_widget = QtWidgets.QWidget()
        top_level_widget.setContentsMargins(0, 0, 0, 0)
        top_level_widget.setLayout(self.widget_layout)
        self._top_level_widget = top_level_widget
        self.addWidget(top_level_widget)
    def _setUpContentLoadToolbar(self):
        """
        Helper method to initialize the content load toolbar.
        The header content area split between showing a tip for Workspace tab
        and input source load for View tabs.
        """
        load_combo = self.ui.view_load_from_cb
        # MSV-1962: Workaround for a bug where the combobox background color
        # was being applied to the selected item background on Linux
        load_combo.setItemDelegate(QtWidgets.QStyledItemDelegate(load_combo))
        # add options for import combo box
        import_signals = (self.importFromWorkspace, self.importSelectedEntries,
                          self.importFile)
        for text, signal in itertools.zip_longest(self.LOAD_FROM_OPTIONS,
                                                  import_signals):
            load_combo.addItem(text, signal)
        load_combo.setFixedWidth(load_combo.sizeHint().width() + 10)
    def _setUpContentLoadToolbarSignals(self):
        """
        Connect all content load toolbar signals to equivalent menu option.
        """
        self.ui.view_download_btn.clicked.connect(self._importRequested)
        self.ui.view_load_from_file_btn.clicked.connect(self.importFile)
    @QtCore.pyqtSlot()
    def _importRequested(self):
        """
        Emit the appropriate import type request signal. Update the icon on the
        button to default state.
        """
        self._setDownloadBtnHighlight(False)
        self.ui.view_load_from_cb.currentData().emit()
    @QtCore.pyqtSlot(int)
    def _updateViewDownloadBtn(self, idx):
        """
        Update the icon on download button when 'File' is selected in the combo.
        """
        highlight_value = self.ui.view_load_from_cb.itemText(idx) == 'File'
        self._setDownloadBtnHighlight(highlight_value)
    def _setDownloadBtnHighlight(self, highlight):
        """
        Set a property on view_download_btn so that the but icon can be updated
        based on it's value
        :param highlight: Whether to use highlighted icon on the button or not.
        :type highlight: bool
        """
        self.ui.view_download_btn.setProperty('highlight', highlight)
        qt_utils.update_widget_style(self.ui.view_download_btn)
    def _setUpFindToolbar(self):
        """
        Helper method to initialize the find toolbar.
        The Find / Fetch area of the MSV Toolbar.
        """
        self._setUpSettingsMenu()
        self._setUpPickBanners()
        self.pattern_edit_dialog = dialogs.PatternEditDialog(self)
        # update patterns if edit dialog changes bookmarked patterns
        self.pattern_edit_dialog.patternListChanged.connect(
            self._setUpBookmarkMenu)
        # set up pattern bookmarks menu
        self.pattern_edit_dialog.emitPatternList()
    def _setUpFindToolbarSignals(self):
        """
        Connect appropriate signals for Find Toolbar.
        """
        ui = self.ui
        sig_slot_list = [
            (self._settings_menu.triggered, self._updateFindToolbarLineEditToolTipAndPlaceHolderText),
            (self.find_prosite_pattern.toggled, ui.find_fetch_bookmark_btn.setVisible),
            (ui.find_fetch_search_btn.clicked, ui.fetch_find_le.returnPressed.emit),
            (ui.find_fetch_download_btn.clicked, ui.fetch_find_le.returnPressed.emit),
            (ui.find_fetch_prev_pattern_btn.clicked, self.prevPatternMatch.emit),
            (ui.find_fetch_next_pattern_btn.clicked, self.nextPatternMatch.emit),
            (ui.fetch_find_le.textChanged, self._updateFindToolbarSearchBtns),
            (ui.fetch_find_le.returnPressed, self._onFetchFindRequested),
        ]  # yapf: disable
        for signal, slot in sig_slot_list:
            signal.connect(slot)
    def _setFindToolbarDefaults(self):
        """
        Sets Find Toolbar Defaults:
        - settings are reset to Find Sequence Substring & Fetch PDB
        - all buttons except settings is disabled
        """
        ui = self.ui
        self.find_sequence_substr.setChecked(True)
        self.fetch_pdb.setChecked(True)
        ui.fetch_find_le.clear()
        ui.find_fetch_bookmark_btn.setVisible(False)
        ui.search_btns_sw.setCurrentIndex(self.SEARCH_PAGE)
        self._enableFindToolbarSearchBtns(False)
        ui.find_fetch_prev_pattern_btn.setDisabled(True)
        ui.find_fetch_next_pattern_btn.setDisabled(True)
        self._updateFindToolbarLineEditToolTipAndPlaceHolderText()
[docs]    def createSavedPatternsMenu(self, pattern_list):
        """
        Set up the pattern bookmark menu with the given saved patterns.
        :param pattern_list: List of pattern 4-tuples
            (name, pattern, hotspot, color)
        :type pattern_list: tuple(str, str, str, str)
        """
        bookmark_menu = QtWidgets.QMenu(self)
        saved_patterns_action = bookmark_menu.addAction('Saved Patterns')
        saved_patterns_action.setEnabled(False)
        for name, pattern, *_ in pattern_list:
            action = bookmark_menu.addAction(name)
            action.setData(pattern)
        bookmark_menu.addSeparator()
        bookmark_menu.addAction('Manage Saved Patterns...',
                                self._onManagePatternsSelection)
        return bookmark_menu
    @QtCore.pyqtSlot(list)
    def _setUpBookmarkMenu(self, pattern_list):
        """
        Create pattern bookmark menu and set on bookmark button.
        See `createSavedPatternsMenu` for parameter documentation.
        """
        bookmark_menu = self.createSavedPatternsMenu(pattern_list)
        bookmark_menu.triggered.connect(self._onBookMarkedPatternSelection,
                                        Qt.QueuedConnection)
        self.ui.find_fetch_bookmark_btn.setMenu(bookmark_menu)
    @QtCore.pyqtSlot(QtWidgets.QAction)
    def _onBookMarkedPatternSelection(self, action):
        """
        Slot for when a bookmarked pattern is selected.
        :param action: selected action
        :type action: QtWidgets.QAction
        """
        pattern = action.data()
        if pattern is None:
            # If the action is not associated with a pattern, its data is None
            return
        self.ui.fetch_find_le.setText(pattern)
        # execute the search without expecting user to click
        self.ui.find_fetch_search_btn.click()
    def _onManagePatternsSelection(self):
        """
        Show the edit patterns dialog.
        """
        self.pattern_edit_dialog.setPatterns()
        self.pattern_edit_dialog.display()
    def _setUpSettingsMenu(self):
        """
        Set up Find Toolbar's settings menu by adding QActions for menu item.
        """
        self._settings_menu = QtWidgets.QMenu()
        settings_menu = self._settings_menu
        self.find_sequence_substr = QtWidgets.QAction('Sequence Substring')
        self.find_prosite_pattern = QtWidgets.QAction('PROSITE Pattern')
        title = settings_menu.addAction('Find in Sequence')
        title.setEnabled(False)
        find_menu_items = (self.find_sequence_substr, self.find_prosite_pattern)
        find_action_group = QtWidgets.QActionGroup(self)
        find_action_group.setExclusive(True)
        for find_item in find_menu_items:
            settings_menu.addAction(find_item)
            find_action_group.addAction(find_item)
            find_item.setCheckable(True)
        settings_menu.addSeparator()
        self.fetch_pdb = QtWidgets.QAction('PDB Structures')
        self.fetch_uniprot_or_entrez = QtWidgets.QAction(
            'UniProt or Entrez Sequence')
        title = settings_menu.addAction('Fetch')
        title.setEnabled(False)
        fetch_menu_items = (self.fetch_pdb, self.fetch_uniprot_or_entrez)
        fetch_action_group = QtWidgets.QActionGroup(self)
        fetch_action_group.setExclusive(True)
        for fetch_item in fetch_menu_items:
            settings_menu.addAction(fetch_item)
            fetch_action_group.addAction(fetch_item)
            fetch_item.setCheckable(True)
        self.ui.find_fetch_settings_btn.setMenu(settings_menu)
    def _updateFindToolbarLineEditToolTipAndPlaceHolderText(self):
        """
        Set customized tooltip and placeholder text according to the chosen
        settings for the find / fetch line edit.
        """
        if self.find_sequence_substr.isChecked():
            tooltip = self.FIND_SEQUENCE_SUBSTRING_TT
            placeholder_text = self.FIND_SEQUENCE_SUBSTRING_PT
        else:
            tooltip = self.FIND_PROSITE_PATTERN_TT
            placeholder_text = self.FIND_PROSITE_PATTERN_PT
        if self.fetch_pdb.isChecked():
            tooltip += self.FETCH_PDB_STRUCTURES_TT
            placeholder_text += self.FETCH_PDB_STRUCTURES_PT
        else:
            tooltip += self.FETCH_UNIPROT_OR_ENTREZ_SEQUENCE_TT
            placeholder_text += self.FETCH_UNIPROT_OR_ENTREZ_SEQUENCE_PT
        le = self.ui.fetch_find_le
        le.setToolTip(tooltip)
        le.setPlaceholderText(placeholder_text)
    def _onFetchFindRequested(self):
        """
        Emits a request to Find or Fetch given the requested search terms.
        """
        search_text = self.ui.fetch_find_le.text()
        if self.isFindRequest(search_text):
            self.requestFind.emit(search_text)
        elif self.isFetchRequest(search_text):
            try:
                self._enableFindToolbarSearchBtns(enable=False,
                                                  tooltip="Fetching...")
                self.requestFetch.emit(search_text)
            finally:
                self._enableFindToolbarSearchBtns(enable=True)
    def _updateFindToolbarSearchBtns(self, search_text):
        """
        Only enable the Find Toolbar search / download button whenever there is
        text in the search bar. Also determine if it's a fetch or find action,
        and show appropriate button.
        :param search_text: current text entered in the search bar.
        :type search_text: str
        """
        is_find = self.isFindRequest(search_text)
        is_fetch = self.isFetchRequest(search_text)
        valid = is_find or is_fetch
        tooltip = self.INVALID_FF_TT if search_text and not valid else None
        self._enableFindToolbarSearchBtns(valid, tooltip)
        if is_find or not search_text:
            index = self.SEARCH_PAGE
        else:
            index = self.DOWNLOAD_PAGE
        self.ui.search_btns_sw.setCurrentIndex(index)
        self._updatePrevAndNextFindToolbarBtns(False)
    def _enableFindToolbarSearchBtns(self, enable, tooltip=None):
        self.ui.find_fetch_search_btn.setEnabled(enable)
        self.ui.find_fetch_download_btn.setEnabled(enable)
        if tooltip is None:
            search_tt = self.FIND_TT
            download_tt = self.FETCH_TT
        else:
            search_tt = download_tt = tooltip
        self.ui.find_fetch_search_btn.setToolTip(search_tt)
        self.ui.find_fetch_download_btn.setToolTip(download_tt)
[docs]    def setPatternFound(self):
        """
        Callback for when pattern matches are found.
        Enables the prev and next pattern buttons.
        """
        self._updatePrevAndNextFindToolbarBtns(True)
    def _updatePrevAndNextFindToolbarBtns(self, enable):
        """
        Helper method to enable / disable the next & prev search buttons.
        :param enable: whether to enable / disable
        :type enable: bool
        """
        self.ui.find_fetch_prev_pattern_btn.setEnabled(enable)
        self.ui.find_fetch_next_pattern_btn.setEnabled(enable)
    def _setUpPickBanners(self):
        # Lambda slots with references to self cause problems with garbage
        # collection.  To avoid this, we replace self with a weakref.
        self = weakref.proxy(self)
        stacked_layout = self.ui.find_toolbar_sw.layout()
        # Allow widget pages underneath to show up
        stacked_layout.setStackingMode(stacked_layout.StackAll)
        chimera_pick_banner = HMChimeraPickingBanner()
        chimera_pick_banner.closeClicked.connect(self._onChimeraBannerClosed)
        chimera_pick_banner.resetClicked.connect(
            lambda: self.resetPickRequested.emit(PickMode.HMChimera))
        pairwise_pick_banner = PairwisePickingBanner()
        pairwise_pick_banner.closeClicked.connect(self._onPairwiseBannerClosed)
        binding_pick_banner = HMBindingSitePickingBanner()
        binding_pick_banner.closeClicked.connect(self._onBindingBannerClosed)
        proximity_pick_banner = HMProximityPickingBanner()
        proximity_pick_banner.closeClicked.connect(
            self._onProximityBannerClosed)
        self._pick_banners = {
            PickMode.HMChimera: chimera_pick_banner,
            PickMode.Pairwise: pairwise_pick_banner,
            PickMode.HMBindingSite: binding_pick_banner,
            PickMode.HMProximity: proximity_pick_banner,
        }
        for banner in self._pick_banners.values():
            banner.hide()
            self.ui.find_toolbar_sw.addWidget(banner)
    def _setUpTaskToolbar(self):
        """
        Helper method to initialize the task toolbar.
        The Tasks area consists of the Homology / Align / Other Tasks buttons
        as well as the Edit mode tools.
        """
        # align task
        align_dialog = alignment_pane.AlignmentPane
        align_btn = popups.make_pop_up_tool_button(
            parent=self,
            pop_up_class=align_dialog,
            tooltip=self.ALIGN_TOOLTIP,
            obj_name='align_btn',
            text=self.ALIGN_TXT,
            icon='empty_icon.png',
            indicator=True,
        )
        align_btn.setPopupValign(align_btn.ALIGN_BOTTOM)
        align_btn.setPopupHalign(align_btn.ALIGN_LEFT)
        self.ui.default_toolbar.layout().addWidget(align_btn)
        self.quick_align_dialog = align_btn.popup_dialog
        # other tasks
        other_tasks_dialog = popups.OtherTasksPopUp
        other_tasks_btn = popups.make_pop_up_tool_button(
            parent=self,
            pop_up_class=other_tasks_dialog,
            tooltip=self.OTHER_TASKS_TOOLTIP,
            obj_name='other_tasks_btn',
            text=self.OTHER_TASKS_TXT,
            icon='empty_icon.png',
            indicator=True,
        )
        other_tasks_btn.setPopupValign(other_tasks_btn.ALIGN_BOTTOM)
        other_tasks_btn.setPopupHalign(other_tasks_btn.ALIGN_LEFT)
        self.ui.task_toolbar_base_layout.addWidget(other_tasks_btn)
        self.other_tasks_dialog = other_tasks_btn.popup_dialog
    def _setUpTaskToolbarSignals(self):
        """
        Set up all task / edit toolbar signals.
        """
        ui = self.ui
        self.buildHomologyModelRequested = self.other_tasks_dialog.buildHomologyModel
        self.allPredictionsRequested = self.other_tasks_dialog.runPredictions
        self.disulfideBondPredictionRequested = self.other_tasks_dialog.disulfideBonds
        self.secondaryStructurePredictionRequested = self.other_tasks_dialog.secondaryStructure
        self.solventAccessibilityPredictionRequested = self.other_tasks_dialog.solventAccessibility
        self.disorderedRegionsPredictionRequested = self.other_tasks_dialog.disorderedRegions
        self.domainArrangementPredictionRequested = self.other_tasks_dialog.domainArrangement
        # create signals for homologs task
        self.findHomologs = ui.find_homologs_btn.clicked
        # create signals for the edit toolbar buttons
        self.insertGaps = ui.insert_gap_btn.clicked
        self.deleteGaps = ui.delete_gaps_btn.clicked
        self.insertResidues = ui.insert_residues_btn.clicked
        self.deleteSelection = ui.delete_residues_btn.clicked
        self.changeResidues = ui.change_residue_or_gap_btn.clicked
        self.replaceSelection = ui.replace_selection_btn.clicked
        self.exitEditMode = ui.exit_edit_mode_btn.clicked
        self.edit_toolbar_manager = EditToolbarManager(self.ui)
[docs]    def enterWorkspaceToolbarMode(self):
        """
        Switches the content toolbar to accommodate the workspace tab.
        """
        self.ui.content_load_sw.setCurrentIndex(self.WS_PAGE)
[docs]    def enterViewToolbarMode(self):
        """
        Switches the content toolbar to accommodate the view tab.
        """
        self.ui.content_load_sw.setCurrentIndex(self.LOAD_PAGE)
[docs]    def enterStandaloneToolbarMode(self):
        """
        Switches the content toolbar to accomodate standalone MSV.
        """
        self.ui.content_load_sw.setCurrentIndex(self.LOAD_FROM_FILE_PAGE)
[docs]    def enterFindSeqToolbarMode(self):
        """
        Switches the content toolbar to show the Find Sequence in List widget
        """
        self.ui.content_load_sw.setCurrentIndex(self.FIND_SEQ_PAGE)
[docs]    def enterPickMode(self, pick_mode):
        pick_banner = self._pick_banners.get(pick_mode)
        if pick_banner is None:
            return
        self.ui.find_toolbar_sw.setCurrentWidget(pick_banner)
        pick_banner.show()
        pick_banner.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                                  QtWidgets.QSizePolicy.Preferred)
        for banner in self._pick_banners.values():
            if banner is not pick_banner:
                banner.hide()
[docs]    def exitPickMode(self, pick_mode):
        for banner in self._pick_banners.values():
            banner.setSizePolicy(QtWidgets.QSizePolicy.Ignored,
                                 QtWidgets.QSizePolicy.Preferred)
            banner.hide()
        self.ui.find_toolbar_sw.setCurrentIndex(0)
    @QtCore.pyqtSlot()
    def _onChimeraBannerClosed(self):
        pick_mode = PickMode.HMChimera
        self.exitPickMode(pick_mode)
        self.pickClosed.emit(pick_mode)
    @QtCore.pyqtSlot()
    def _onPairwiseBannerClosed(self):
        pick_mode = PickMode.Pairwise
        self.exitPickMode(pick_mode)
        self.pickClosed.emit(pick_mode)
    @QtCore.pyqtSlot()
    def _onBindingBannerClosed(self):
        pick_mode = PickMode.HMBindingSite
        self.exitPickMode(pick_mode)
        self.pickClosed.emit(pick_mode)
    @QtCore.pyqtSlot()
    def _onProximityBannerClosed(self):
        pick_mode = PickMode.HMProximity
        self.exitPickMode(pick_mode)
        self.pickClosed.emit(pick_mode)
[docs]    def enterEditToolbarMode(self):
        """
        Switches to Edit mode and shows the appropriate task tools.
        """
        self.ui.task_bar_stacked_widget.setCurrentIndex(self.EDIT_MODE_PAGE)
[docs]    def exitEditToolbarMode(self):
        """
        Switches to default mode and removes all Edit related tools.
        """
        self.ui.task_bar_stacked_widget.setCurrentIndex(self.DEFAULT_PAGE)
[docs]    def setEditMode(self, enable=True):
        """
        Enable or disable edit mode.
        :param enable: Whether to enable edit mode.
        :type enable: bool
        """
        if enable:
            self.enterEditToolbarMode()
        else:
            self.exitEditToolbarMode()
[docs]    def isFindRequest(self, request: str) -> bool:
        """
        A find request is determined by checking if the request only contains
        alphabets or special characters, but no commas.
        :param request: the user entered search text.
        """
        # For "Find":
        # necessary: cannot contain comma
        # sufficient: contains "-"
        # sufficient: all alpha
        return ("," not in request and
                (request.isalpha() or any(s in request for s in "-[]{}@")))
[docs]    def isFetchRequest(self, request: str) -> bool:
        """
        Return whether any of the entered IDs are a valid ID for PDB, Entrez,
        or UniProt
        :param request: the user entered search text.
        """
        request_ids = request.split(",")
        for db_id in request_ids:
            if any(valid(db_id) for valid in self.FETCH_VALIDATORS):
                return True
        return False
[docs]class EditToolbarManager(mappers.MapperMixin, QtCore.QObject):
    """
    Class to encapsulate managing edit toolbar button enabled states
    """
    model_class = gui_models.PageModel
[docs]    def __init__(self, ui, *args, **kwargs):
        """
        :param ui: The ui object containing the toolbar widgets
        """
        super().__init__(*args, **kwargs)
        self.ui = ui
        self._is_workspace = False
        self._aln_set_selected = False
        # tooltips for the edit toolbar
        sel_residues = "requires selected residues"
        single_block = ("requires single block of selected residues from one "
                        "sequence only")
        single_res = "requires single selected residue"
        # dictionary of {button: (button tooltip,
        #                         reason why button would be disabled)}
        self.TOOLTIPS = {
            ui.insert_gap_btn: ("Insert gaps", sel_residues),
            ui.delete_gaps_btn: ("Delete gaps", "requires selected gaps"),
            ui.insert_residues_btn: ("Insert residues", single_block),
            ui.delete_residues_btn: ("Delete selection", sel_residues),
            ui.change_residue_or_gap_btn: ("Change residue or gap", single_res),
            ui.replace_selection_btn: ("Replace selection", single_block),
            ui.exit_edit_mode_btn: ("Exit edit mode", None)
        }
        for btn, (tooltip, _) in self.TOOLTIPS.items():
            btn.setToolTip(tooltip)
        # methods for toggling the enabled state of buttons in the edit toolbar
        self.setInsertGapsEnabled = partial(self._setButtonEnabled,
                                            ui.insert_gap_btn)
        self.setDeleteGapsEnabled = partial(self._setButtonEnabled,
                                            ui.delete_gaps_btn)
        self.setInsertResiduesEnabled = partial(self._setButtonEnabled,
                                                ui.insert_residues_btn)
        self.setDeleteSelectionEnabled = partial(self._setButtonEnabled,
                                                 ui.delete_residues_btn)
        self.setChangeResiduesEnabled = partial(self._setButtonEnabled,
                                                ui.change_residue_or_gap_btn)
        self.setReplaceSelectionEnabled = partial(self._setButtonEnabled,
                                                  ui.replace_selection_btn)
        self._setupMapperMixin()
[docs]    def setModel(self, model):
        super().setModel(model)
        if model is not None:
            # Need to update tooltips when the page changes
            self._is_workspace = model.is_workspace
            self._updateDisabledButtonTooltipsForAlnSet()
[docs]    def defineMappings(self):
        M = self.model_class
        menu_statuses = M.menu_statuses
        return [
            (mappers.TargetSpec(setter=self.setInsertGapsEnabled), menu_statuses.can_insert_gap),
            (mappers.TargetSpec(setter=self.setDeleteGapsEnabled), menu_statuses.can_delete_gaps),
            (mappers.TargetSpec(setter=self.setInsertResiduesEnabled), menu_statuses.can_insert_residues),
            (mappers.TargetSpec(setter=self.setDeleteSelectionEnabled), menu_statuses.can_delete_residues),
            (mappers.TargetSpec(setter=self.setChangeResiduesEnabled), menu_statuses.can_change_elem),
            (mappers.TargetSpec(setter=self.setReplaceSelectionEnabled), menu_statuses.can_replace_selected_elems),
        ]  # yapf: disable
[docs]    def getSignalsAndSlots(self, model):
        return [
            (model.aln_signals.resSelectionChanged, self._updateDisabledButtonTooltipsForAlnSet),
            (model.aln_signals.alnSetChanged, self._updateDisabledButtonTooltipsForAlnSet),
        ]  # yapf: disable
    def _setButtonEnabled(self, btn, enabled):
        """
        Enable or disable the specified button.  If disabled, the tooltip will
        be updated to explain the requirements for enabling the button.
        :param btn: The button to enable or disable
        :type btn: QtWidgets.QAbstractButton
        :param enabled: Whether to enable the button.
        :type enabled: bool
        """
        btn.setEnabled(enabled)
        if enabled:
            self._setEnabledButtonTooltip(btn)
        else:
            self._setDisabledButtonTooltip(btn)
    def _setEnabledButtonTooltip(self, btn):
        """
        Set the tooltip for an enabled button
        """
        tooltip, _ = self.TOOLTIPS[btn]
        btn.setToolTip(tooltip)
    def _setDisabledButtonTooltip(self, btn):
        """
        Set the tooltip for a disabled button, specifying the requirements to
        enable the button.
        """
        tooltip, requirements = self.TOOLTIPS[btn]
        if self._is_workspace and btn not in {
                self.ui.insert_gap_btn, self.ui.delete_gaps_btn
        }:
            requirements = "cannot edit residues on the Workspace Tab"
        elif self._aln_set_selected:
            requirements = "cannot edit sequences in alignment sets"
        tooltip += f" ({requirements})"
        btn.setToolTip(tooltip)
    @QtCore.pyqtSlot()
    def _updateDisabledButtonTooltipsForAlnSet(self):
        """
        Update disabled button tooltips based on whether residues in an
        alignment set are selected
        """
        # Cache whether alignment set residues are selected
        self._aln_set_selected = self.model.aln.alnSetResSelected()
        for btn in self.TOOLTIPS.keys():
            if not btn.isEnabled():
                self._setDisabledButtonTooltip(btn)
[docs]    def makeInitialModel(self):
        """
        Intentionally left blank to avoid creating an empty model
        """
class _AbstractPickingBanner(widgetmixins.InitMixin, QWidgetStyled):
    closeClicked = QtCore.pyqtSignal()
    TEXT = NotImplemented
    def initSetUp(self):
        super().initSetUp()
        self.setObjectName("PickBannerContainer")
        self.banner = QtWidgets.QWidget()
        self.lbl = QtWidgets.QLabel(self.TEXT)
        self.lbl.setObjectName("lbl")
        self.close_btn = QtWidgets.QToolButton()
        self.close_btn.setObjectName("close_btn")
        self.close_btn.setText("x")
        self.close_btn.clicked.connect(self.closeClicked)
    def initLayOut(self):
        super().initLayOut()
        glayout = QtWidgets.QGridLayout()
        self.banner.setLayout(glayout)
        glayout.setContentsMargins(5, 2, 1, 2)
        glayout.setSpacing(0)
        glayout.addWidget(self.lbl, 0, 0, -1, 1)
        glayout.addWidget(self.close_btn, 0, 1, Qt.AlignTop)
        # Center the widget so it shrinks
        self.main_layout.addWidget(self.banner, 0, Qt.AlignCenter)
[docs]class HMChimeraPickingBanner(_AbstractPickingBanner):
    resetClicked = QtCore.pyqtSignal()
    TEXT = '''<b>Define template regions.</b> Select regions on the alternate
sequences to <br/>override the default. Selections on sequences may not overlap.
<a href="reset" style="color: #60b0dc">Reset</a>'''
[docs]    def initSetUp(self):
        super().initSetUp()
        self.banner.setObjectName("ChimeraPickBanner")
        self.lbl.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.lbl.linkActivated.connect(self.resetClicked)
[docs]class HMBindingSitePickingBanner(_AbstractPickingBanner):
    TEXT = '''<b>Constrain Binding Site Residues.</b> Pick binding site
    annotation squares <br/>to add them to the constraints list.
    Click again to clear them.'''
[docs]class HMProximityPickingBanner(_AbstractPickingBanner):
    TEXT = '''<b>Set Proximity Constraints.</b> Click pairs of residues in the
Reference <br/> sequence to keep them together. Clicking again removes the
constraint.'''
[docs]class PairwisePickingBanner(_AbstractPickingBanner):
    TEXT = '''<b>Set Constraints.</b> Click a residue in the Reference
sequence, then click one in <br/>the other sequence to keep them
together. Clicking again removes a constraint.
'''
[docs]class StructStatus(parameters.CompoundParam):
    num_in_ws = parameters.IntParam()
    num_total = parameters.IntParam()
[docs]class AlnStatusModel(parameters.CompoundParam):
    num_selected = parameters.IntParam()
    num_total = parameters.IntParam()
    num_hidden = parameters.IntParam()
    num_structs = StructStatus()
    ref_seq = parameters.StringParam()
[docs]class PanelStatus(parameters.CompoundParam):
    num_seqs_in_other_tabs = parameters.IntParam()
    num_tabs = parameters.IntParam()
[docs]class SetOnlyTarget(mappers.TargetSpec):
    """
    A Target that only allows a setter. This allows one-way syncing between the
    model and the target.
    """
[docs]    def __init__(self, setter):
        super().__init__(obj=None,
                         getter=None,
                         setter=setter,
                         signal=None,
                         slot=None)
[docs]class MSVStatusBar(mappers.MapperMixin, widgetmixins.InitMixin, QWidgetStyled):
    """
    A status bar that displays:
         - Total number of sequences and the number that are selected
         - The title of the reference sequence
         - Number of structures in the workspace and in total
         - Number of sequences in other tabs
         - Number of other tabs
    """
    model_class = AlnStatusModel
    SEQUENCES_LABEL = 'SEQUENCES'
    STRUCTURES_LABEL = 'STRUCTURES'
    SELECTED_SEQS_TXT = '{num} selected'
    TOTAL_SEQS_TXT = '{num} total'
    HIDDEN_SEQS_TXT = '{num} hidden'
    NUM_SEQS_WITH_STRUCTURES_TXT = '{num} in Workspace ({num_total} total)'
    RESIDUE_LABEL = 'RESIDUE'
    CHAIN_LABEL = 'CHAIN'
    SEQUENCE_LABEL = 'SEQUENCE'
    REFERENCE_SEQ_LABEL = 'REFERENCE'
    SEQS_IN_OTHER_TABS_LABEL = 'OTHER TABS'
    SEQS_IN_OTHER_TABS_TXT = '{num_sequences} {seq_txt} ({num_tabs} {tab_txt})'
    SUB_LABEL_SEQUENCE_TXT = 'sequence'
    SUB_LABEL_TAB_TXT = 'tab'
    HIGHLIGHT_PROPERTY = 'highlight'
    DEFAULT_SECONDARY_PAGE = 0
    HOVER_SECONDARY_PAGE = 1
    MAIN_LAYOUT_SPACING = 20
    CONTENT_MARGINS = (0, 0, 0, 0)
    OVERALL_GRID_SPACING = 0
    GRID_HORIZONTAL_SPACING = 10
[docs]    def initSetUp(self):
        """
        Sets up the status bar layout and sub widgets.
        """
        super().initSetUp()
        self.setObjectName('msv_status_bar')
        self._primary_info_layout = self._setupPrimaryInfoBar()
        self._secondary_bar = self._setupSecondaryInfoBar()
        self.panel_status = None
[docs]    def initLayOut(self):
        super().initLayOut()
        status_layout = self.bottom_layout
        # current tab primary info bar
        status_layout.addLayout(self._primary_info_layout)
        status_layout.addWidget(
            _make_vertical_line_separator(LINE_SEPARATOR_WIDTH,
                                          LINE_SEPARATOR_HEIGHT))
        # current tab secondary info / hover-over residue bar
        status_layout.addWidget(self._secondary_bar)
        status_layout.addWidget(
            _make_vertical_line_separator(LINE_SEPARATOR_WIDTH,
                                          LINE_SEPARATOR_HEIGHT))
        status_layout.setContentsMargins(10, 0, 0, 0)
        status_layout.setSpacing(self.MAIN_LAYOUT_SPACING)
        sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,
                                   QtWidgets.QSizePolicy.Fixed)
        sp.setHorizontalStretch(0)
        sp.setVerticalStretch(0)
        sp.setHeightForWidth(
            self._secondary_bar.sizePolicy().hasHeightForWidth())
        self.setSizePolicy(sp)
[docs]    def defineMappings(self):
        # MapperMixin
        model_class = self.model_class
        return [
            (SetOnlyTarget(self.setSelectedSeqs), model_class.num_selected),
            (SetOnlyTarget(self.setTotalSeqs), model_class.num_total),
            (SetOnlyTarget(self.setHiddenSeqs), model_class.num_hidden),
            (SetOnlyTarget(self.setRefSequence), model_class.ref_seq),
            (SetOnlyTarget(lambda v: self.setSeqsWithStructures(**v.toDict())),
             model_class.num_structs),
        ]
[docs]    def setModel(self, model):
        super().setModel(model)
        # Set up param for panel-wide status
        if self.panel_status is not None:
            self.panel_status.disconnect()
        self.panel_status = PanelStatus()
        self.panel_status.valueChanged.connect(
            lambda: self.setSeqsInOtherTabs(**self.panel_status.toDict()))
        self.panel_status.valueChanged.emit(
            self.panel_status)  # Force initial update
    def _setupPrimaryInfoBar(self):
        """
        Helper method to setup the primary info related labels of the status
        bar.
        :return: the layout for the primary info bar
        :rtype: `QtWidgets.QGridLayout`
        """
        grid_layout = QtWidgets.QGridLayout()
        grid_layout.setSpacing(self.OVERALL_GRID_SPACING)
        grid_layout.setHorizontalSpacing(self.GRID_HORIZONTAL_SPACING)
        # Number of selected, total, and hidden sequences
        seq_lbl = _make_label(self.SEQUENCES_LABEL, 'sb_sequences_display_lbl')
        self._selected_seqs_lbl = _make_label('', 'sb_selected_seqs_lbl')
        self._total_seqs_lbl = _make_label('', 'sb_total_seqs_lbl')
        self._hidden_seqs_lbl = _make_label('', 'sb_hidden_seqs_lbl')
        # Number of sequences with structures - in Workspace & total
        st_lbl = _make_label(self.STRUCTURES_LABEL, 'sb_structures_display_lbl')
        self._num_seqs_with_structures_lbl = _make_label(
            '', 'sb_num_seqs_with_structures_lbl')
        lbls = (seq_lbl, self._selected_seqs_lbl, self._total_seqs_lbl,
                self._hidden_seqs_lbl)
        for idx, lbl in enumerate(lbls):
            grid_layout.addWidget(lbl, 0, idx)
        grid_layout.addWidget(st_lbl, 1, 0)
        grid_layout.addWidget(self._num_seqs_with_structures_lbl, 1, 1, 1, 2)
        return grid_layout
    def _setupSecondaryInfoBar(self):
        """
        Helper method to setup the secondary info / hovered-over residue
        related labels of the status bar.
        :return: the stacked widget for the secondary info bar
        :rtype: `QtWidgets.QStackedWidget`
        """
        sw = QtWidgets.QStackedWidget()
        sw.setContentsMargins(*self.CONTENT_MARGINS)
        # secondary info widget
        secondary_info_page = QtWidgets.QWidget()
        secondary_info_layout = QtWidgets.QGridLayout(secondary_info_page)
        secondary_info_layout.setContentsMargins(*self.CONTENT_MARGINS)
        secondary_info_layout.setSpacing(self.OVERALL_GRID_SPACING)
        secondary_info_layout.setHorizontalSpacing(self.GRID_HORIZONTAL_SPACING)
        # Reference sequence
        ref_display_lbl = _make_label(self.REFERENCE_SEQ_LABEL,
                                      'sb_reference_seq_display_lbl')
        self._reference_seq_lbl = _make_label('', 'sb_reference_seq_lbl')
        # Total number of sequences on other tabs & number of other tabs
        seqs_in_other_tabs_display_lbl = _make_label(
            self.SEQS_IN_OTHER_TABS_LABEL, 'sb_other_tabs_display_lbl')
        self._seqs_in_other_tabs_lbl = _make_label('', 'sb_other_tabs_lbl')
        lbls = ((ref_display_lbl, self._reference_seq_lbl),
                (seqs_in_other_tabs_display_lbl, self._seqs_in_other_tabs_lbl))
        for row, lbl_grp in enumerate(lbls):
            for col, lbl in enumerate(lbl_grp):
                secondary_info_layout.addWidget(lbl, row, col)
        sw.addWidget(secondary_info_page)
        # hovered-over residue widget
        hover_page = QtWidgets.QWidget()
        hover_layout = QtWidgets.QGridLayout(hover_page)
        hover_layout.setContentsMargins(*self.CONTENT_MARGINS)
        hover_layout.setSpacing(self.OVERALL_GRID_SPACING)
        # Hovered-over residue
        self._residue_lbl = _make_label('', 'sb_residue_lbl')
        residue_display_lbl = _make_label(self.RESIDUE_LABEL,
                                          'sb_residue_display_lbl')
        # Hovered-over chain
        self._chain_lbl = _make_label('', 'sb_chain_lbl')
        chain_display_lbl = _make_label(self.CHAIN_LABEL,
                                        'sb_chain_display_lbl')
        # Hovered-over sequence
        self._sequence_lbl = _make_label('', 'sb_sequence_lbl')
        seq_display_lbl = _make_label(self.SEQUENCE_LABEL,
                                      'sb_sequence_display_lbl')
        lbls = ((self._residue_lbl, residue_display_lbl),
                (self._chain_lbl, chain_display_lbl), (self._sequence_lbl,
                                                       seq_display_lbl))
        for col, lbl_grp in enumerate(lbls):
            for row, lbl in enumerate(lbl_grp):
                hover_layout.addWidget(lbl, row, col, QtCore.Qt.AlignHCenter)
        sw.addWidget(hover_page)
        return sw
[docs]    def enterHoverMode(self):
        """
        Show the status bar's hover mode.
        """
        self._secondary_bar.setCurrentIndex(self.HOVER_SECONDARY_PAGE)
[docs]    def exitHoverMode(self):
        """
        Hide the status bar's hover mode and show default information.
        """
        self._secondary_bar.setCurrentIndex(self.DEFAULT_SECONDARY_PAGE)
[docs]    def setSelectedSeqs(self, num_selected):
        """
        Set the current tab's number of selected sequences.
        :param num_selected: number of selected sequences in current tab.
        :type num_selected: int
        """
        lbl = self._selected_seqs_lbl
        highlight_status = bool(num_selected > 0)
        self._setHighlightProperty(lbl, highlight_status)
        lbl.setText(self.SELECTED_SEQS_TXT.format(num=num_selected))
    def _setHighlightProperty(self, lbl, status):
        """
        Given a label, set its highlight property to status.
        :param lbl: the label to update the highlight property for.
        :type lbl: `QtWidgets.QLabel`
        :param status: whether the highlighting should be applied or not.
        :type status: bool
        """
        lbl.setProperty(self.HIGHLIGHT_PROPERTY, status)
        qt_utils.update_widget_style(lbl)
[docs]    def setTotalSeqs(self, num_total):
        """
        Set the current tab's total number of sequences.
        :param num_total: total number of sequences in current tab.
        :type num_total: int
        """
        self._total_seqs_lbl.setText(self.TOTAL_SEQS_TXT.format(num=num_total))
[docs]    def setHiddenSeqs(self, num_hidden):
        """
        Set the current tab's number of hidden sequences. Note that if there
        are currently no hidden sequences, the label will be hidden.
        :param num_hidden: number of hidden sequences in current tab.
        :type num_hidden: int
        """
        txt = ''
        if num_hidden > 0:
            txt = self.HIDDEN_SEQS_TXT.format(num=num_hidden)
        self._hidden_seqs_lbl.setText(txt)
[docs]    def setSeqsWithStructures(self, num_in_ws, num_total):
        """
        Set the number of sequences with structures in Workspace and total.
        :param num_in_ws: number of sequences with structures in workspace.
        :type num_in_ws: int
        :param num_total: total number of sequences with structures.
        :type num_total: int
        """
        self._num_seqs_with_structures_lbl.setText(
            self.NUM_SEQS_WITH_STRUCTURES_TXT.format(num=num_in_ws,
                                                     num_total=num_total))
[docs]    def setSeqsInOtherTabs(self, num_seqs_in_other_tabs, num_tabs):
        """
        Sets the total number of sequences inn other tabs and the number of
        other tabs.
        :param num_seqs_in_other_tabs: total number of sequences in other tabs.
        :type num_seqs_in_other_tabs: int
        :param num_tabs: total number of tabs.
        :type num_tabs: int
        """
        seq_txt = inflect.engine().plural(self.SUB_LABEL_SEQUENCE_TXT,
                                          num_seqs_in_other_tabs)
        tab_txt = inflect.engine().plural(self.SUB_LABEL_TAB_TXT, num_tabs)
        self._seqs_in_other_tabs_lbl.setText(
            self.SEQS_IN_OTHER_TABS_TXT.format(
                num_sequences=num_seqs_in_other_tabs,
                seq_txt=seq_txt,
                num_tabs=num_tabs,
                tab_txt=tab_txt))
[docs]    def setResidue(self, residue):
        """
        Set the currently hovered-over residue name.
        :param residue: current hovered-over residue name.
        :type residue: str
        """
        self._residue_lbl.setText(residue)
[docs]    def setChain(self, chain):
        """
        Set the currently hovered-over chain name.
        :param chain: current hovered-over chain name.
        :type chain: str
        """
        self._chain_lbl.setText(chain)
[docs]    def setSequence(self, seq):
        """
        Set the currently hovered-over sequence name.
        :param seq: current hovered-over sequence name.
        :type seq: str
        """
        self._sequence_lbl.setText(seq)
[docs]    def setRefSequence(self, ref_seq):
        """
        Set current tab's reference sequence.
        :param ref_seq: current tab's reference sequence name.
        :type ref_seq: str
        """
        self._reference_seq_lbl.setText(ref_seq)
[docs]class ResStatusModel(parameters.CompoundParam):
    num_res_selected = parameters.IntParam()
    num_gaps_selected = parameters.IntParam()
    num_seqs_with_selected_elems = parameters.IntParam()
    num_blocks_selected = parameters.IntParam()
[docs]class MSVResStatusBar(mappers.MapperMixin, widgetmixins.InitMixin,
                      QWidgetStyled):
    """
    A status bar that displays:
        - Number of residues and gaps currenty selected
        - Number of sequences with selected residues
        - Number of contiguous selected blocks (TODO: MSV-2177)
    """
    model_class = ResStatusModel
    clearResSelectionRequested = QtCore.pyqtSignal()
    NUM_RESIDUES_TEXT = "{0} residue"
    NUM_GAPS_TEXT = "{0} gap"
    NUM_SEQS_SELECTED_TEXT = "{0} sequence"
    NUM_BLOCKS_SELECTED_TEXT = ", {0} block"
    ALL_TEXT = "{elem_sel_text} selected"
    SECONDARY_TEXT = "(in {seq_sel_text})"
    MAIN_LAYOUT_SPACING = 15
[docs]    def initSetUp(self):
        super().initSetUp()
        self.setObjectName('msv_residue_status_bar')
        self.res_status_lbl = QtWidgets.QLabel()
        self.res_status_lbl.setObjectName("sb_res_status_lbl")
        self.res_status_lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.res_status2_lbl = QtWidgets.QLabel()
        self.res_status2_lbl.setObjectName("sb_res_status2_lbl")
        self.res_status2_lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.clear_res_sel_lbl = popups.ClickableLabel("Clear")
        self.clear_res_sel_lbl.setObjectName("sb_clear_res_sel_lbl")
        self.clear_res_sel_lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.clear_res_sel_lbl.clicked.connect(self.clearResSelectionRequested)
[docs]    def initLayOut(self):
        super().initLayOut()
        # First line
        hlayout = QtWidgets.QHBoxLayout()
        hlayout.addWidget(self.res_status_lbl)
        hlayout.addWidget(self.clear_res_sel_lbl)
        hlayout.setSpacing(self.MAIN_LAYOUT_SPACING)
        vlayout = QtWidgets.QVBoxLayout()
        vlayout.addLayout(hlayout)
        # Second line
        vlayout.addWidget(self.res_status2_lbl)
        self.bottom_layout.addLayout(vlayout)
[docs]    def defineMappings(self):
        # MapperMixin
        M = self.model_class
        return [
            (SetOnlyTarget(self.updateText), M),
        ]
[docs]    def updateText(self, model):
        """
        Update the text and visibility of the labels
        """
        any_selection = model.num_res_selected + model.num_gaps_selected > 0
        self.clear_res_sel_lbl.setVisible(any_selection)
        if not any_selection:
            text = ""
            text2 = ""
        else:
            elem_words = []
            if model.num_res_selected:
                res_sel_text = self._formatText(self.NUM_RESIDUES_TEXT,
                                                model.num_res_selected)
                elem_words.append(res_sel_text)
            if model.num_gaps_selected:
                gap_sel_text = self._formatText(self.NUM_GAPS_TEXT,
                                                model.num_gaps_selected)
                elem_words.append(gap_sel_text)
            elem_sel_text = ", ".join(elem_words)
            seq_sel_text = self._formatText(self.NUM_SEQS_SELECTED_TEXT,
                                            model.num_seqs_with_selected_elems)
            if model.num_blocks_selected:
                seq_sel_text += self._formatText(self.NUM_BLOCKS_SELECTED_TEXT,
                                                 model.num_blocks_selected)
            text = self.ALL_TEXT.format(elem_sel_text=elem_sel_text)
            text2 = self.SECONDARY_TEXT.format(seq_sel_text=seq_sel_text)
        self.res_status_lbl.setText(text)
        self.res_status2_lbl.setText(text2)
    def _formatText(self, fmt_str, count):
        """
        Given a format string that takes a single int and has a following noun, insert the int and add the proper ending to the noun.
        e.g. if `fmt_str` is "{} apple",
        `count` 0: "0 apples"
        `count` 1: "1 apple"
        `count` 2: "2 apples"
        """
        return inflect.engine().plural(fmt_str.format(count), count)
[docs]class ConfigurationTogglesBar(mappers.MapperMixin, widgetmixins.InitMixin,
                              QWidgetStyled):
    model_class = gui_models.OptionsModel
    MAIN_LAYOUT_SPACING = 0
    CONTENT_MARGINS = (0, 0, MAIN_LAYOUT_SPACING, 0)
[docs]    def initSetUp(self):
        super().initSetUp()
        self.setObjectName('msv_configuration_toggles_bar')
        # Edit tool button
        self.edit_btn = QtWidgets.QToolButton()
        self.edit_btn.setCheckable(True)
        self.edit_btn.setObjectName('edit_btn')
        self.edit_btn.setToolTip("Toggle edit mode")
        # Annotations tool button
        annotations_dialog = popups.QuickAnnotationPopUp
        self.annotations_btn = popups.EllipsisPopUpDialogToolButton(
            pop_up_class=annotations_dialog, parent=self)
        self.annotations_btn.setObjectName('annotations_btn')
        ann_tooltip = "Annotations toggle (right-click to choose annotations)"
        self.annotations_btn.setToolTip(ann_tooltip)
        # Sequence colors tool button
        sequence_colors_dialog = popups.ColorPopUp
        self.sequence_colors_btn = popups.EllipsisPopUpDialogToolButton(
            pop_up_class=sequence_colors_dialog, parent=self)
        self.sequence_colors_btn.setObjectName('sequence_colors_btn')
        color_tooltip = ("Coloring toggle (right-click to change or apply "
                         "colors)")
        self.sequence_colors_btn.setToolTip(color_tooltip)
        # Options tool button
        options_dialog = popups.ViewStylePopUp
        self.options_btn = popups.PopUpDialogButton(parent=self,
                                                    pop_up_class=options_dialog,
                                                    text=None)
        self.options_btn.setArrowType(QtCore.Qt.NoArrow)
        self.options_btn.setObjectName('options_btn')
        options_tooltip = "Show View Options pane"
        self.options_btn.setToolTip(options_tooltip)
[docs]    def initLayOut(self):
        super().initLayOut()
        layout = self.bottom_layout
        layout.setContentsMargins(*self.CONTENT_MARGINS)
        layout.setSpacing(self.MAIN_LAYOUT_SPACING)
        separator = _make_vertical_line_separator(LINE_SEPARATOR_WIDTH,
                                                  LINE_SEPARATOR_HEIGHT)
        widgets = (self.edit_btn, separator, self.annotations_btn,
                   self.sequence_colors_btn, self.options_btn)
        for w in widgets:
            layout.addWidget(w)
        sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed,
                                   QtWidgets.QSizePolicy.Fixed)
        sp.setHorizontalStretch(0)
        sp.setVerticalStretch(0)
        sp.setHeightForWidth(self.options_btn.sizePolicy().hasHeightForWidth())
        self.setSizePolicy(sp)
[docs]    def defineMappings(self):
        # MapperMixin
        model_class = self.model_class
        return [(self.sequence_colors_btn, model_class.colors_enabled),
                (self.annotations_btn, model_class.annotations_enabled)]
[docs]class TaskStatusBar(basewidgets.BaseWidget):
    """
    Status bar that displays a status label when monitored tasks are running.
    Automatically hides itself if all monitored tasks are completed.
    """
    PRETTY_TASK_NAMES = {
        blast.BlastTask: "homology search",
        pfam.PfamTask: "pfam job",
        hm_models.HomologyModelingTask: 'homology modeling job',
        hm_models.ChimeraHomologyModelingTask: 'homology modeling job',
        hm_models.BatchHomologyModelingTask: 'batch homology modeling job',
        hm_models.ConsensusHomologyModelingTask: 'homology modeling job',
        predictors.PredictorWrapperTask: 'prediction job'
    }
[docs]    def initSetUp(self):
        super().initSetUp()
        self._watched_tasks = []
        self.status_lbl = QtWidgets.QLabel("")
        self.status_lbl.setObjectName("status_lbl")
        self.hide()
[docs]    def initLayOut(self):
        super().initLayOut()
        h_layout = QtWidgets.QHBoxLayout()
        h_layout.addWidget(self.status_lbl)
        h_layout.addStretch()
        self.main_layout.addLayout(h_layout)
[docs]    @QtCore.pyqtSlot(AbstractTask)
    def watchTask(self, new_task):
        if not any(task is new_task for task in self._watched_tasks):
            self._watched_tasks.append(new_task)
            new_task.statusChanged.connect(self._updateLabel)
        self._updateLabel()
    def _updateLabel(self):
        label_string = ''
        task_type_to_tasks = scollections.DefaultIdDict(list)
        for task in self._watched_tasks:
            if task.status != task.RUNNING:
                continue
            if isinstance(task, queue.TaskQueue):
                # We expect 1 task queue per task type
                key = task
            else:
                key = type(task)
            task_type_to_tasks[key].append(task)
        for task_type, tasks in task_type_to_tasks.items():
            if isinstance(task_type, queue.TaskQueue):
                label_part = getattr(task_type, "description", None)
                if label_part is None:
                    label_part = f"{task_type.name} in progress..."
                    if schrodinger.in_dev_env():
                        print(f"### {task_type.name} has no description",
                              file=sys.stderr)
                label_string += label_part
            else:
                pretty_task_name = self._getPrettyTaskName(
                    task_type, len(tasks))
                label_string += (f'{len(tasks)} '
                                 f'{pretty_task_name} '
                                 'in progress...   ')
        if label_string:
            label_string = f'<i>{label_string}</i>'
        self.status_lbl.setText(label_string)
        if not task_type_to_tasks:
            self.hide()
        else:
            self.show()
    def _getPrettyTaskName(self, task_type, num_tasks):
        if task_type in self.PRETTY_TASK_NAMES:
            name = self.PRETTY_TASK_NAMES[task_type]
        else:
            name = task_type.__name__
            if schrodinger.in_dev_env():
                print(
                    f"{task_type} has no task description, please add one to PRETTY_TASK_NAMES",
                    file=sys.stderr)
        return inflect.engine().plural(name, num_tasks)
def _make_vertical_line_separator(width, height):
    """
    Helper function to setup a custom vertical line separator.
    :param width: of the line separator
    :type width: int
    :param height: of the line separator
    :type height: int
    :return: the created vertical line separator.
    :rtype: `QtWidgets.QFrame`
    """
    line_separator = QtWidgets.QFrame()
    line_separator.setObjectName('toolbar_separator')
    line_separator.setFrameShape(QtWidgets.QFrame.VLine)
    line_separator.setLineWidth(width)
    line_separator.setFixedHeight(height)
    return line_separator
def _make_label(text, object_name):
    """
    Helper function to setup a label with the given initial text and object
    name.
    :param text: to show in the created label
    :type text: str
    :param object_name: of the created label to refer to in stylesheet
    :type object_name: str
    :return: created label
    :rtype: `QtWidgets.QLabel`
    """
    lbl = QtWidgets.QLabel(text)
    lbl.setObjectName(object_name)
    return lbl