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 QtGui
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 QWidgetStyled(StyleMixin, QtWidgets.QWidget): pass
[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(QtGui.QAction) def _onBookMarkedPatternSelection(self, action): """ Slot for when a bookmarked pattern is selected. :param action: selected action :type action: QtGui.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 = QtGui.QAction('Sequence Substring') self.find_prosite_pattern = QtGui.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 = QtGui.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 = QtGui.QAction('PDB Structures') self.fetch_uniprot_or_entrez = QtGui.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 = QtGui.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] def initSetUp(self): super().initSetUp() self.banner.setObjectName("BindingSitePickBanner")
[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] def initSetUp(self): super().initSetUp() self.banner.setObjectName("ProximityPickBanner")
[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] def initSetUp(self): super().initSetUp() self.banner.setObjectName("PairwisePickBanner")
[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