Source code for schrodinger.ui.qt.forcefield

"""
Contains Force Field selector widgets
"""
import enum
import os
from contextlib import contextmanager
from typing import List
from typing import Optional

import schrodinger
from schrodinger.forcefield import OPLS_DIR_ENV
from schrodinger.forcefield import OPLSVersion
from schrodinger.forcefield.custom_params import upgrade_oplsdir
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui import maestro_ui
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt.appframework2 import application
from schrodinger.utils import fileutils
from schrodinger.utils import license
from schrodinger.utils import mmutil
from schrodinger.utils import preferences
from schrodinger.utils.env import EnvironmentContext
from schrodinger.utils.env import prepend_sys_path

from . import swidgets

maestro = schrodinger.get_maestro()
maestro_hub = maestro_ui.MaestroHub.instance()

MMFFS_INT = 10
MMFFS_NAME = 'MMFFs'

MIGRATED_MESSAGE = (
    'Your custom force field parameters have been automatically migrated to'
    ' the customizations file associated with the current release.\n\n'
    'Note that any new customizations you define will only be available with'
    ' this version of the Schrodinger Suite, as older versions of Maestro will'
    ' continue to use the older parameter files.')

SAVE_RESPONSE_KEY = 'use_default_ff'

_opls_available = None


def _is_opls_available():
    """
    Determine if S-OPLS (F16) is available.
    """
    # This is run in a function to avoid calling at import and cached to avoid
    # duplicate license feature checks
    global _opls_available
    if _opls_available is None:
        _opls_available = license.is_opls2_available()
    return _opls_available


FORCEFIELD_PATH_TYPE_DEFAULT = 0
FORCEFIELD_PATH_TYPE_CUSTOM = 1

F14_DISP_NAME = mm.opls_version_to_name(mm.OPLSVersion.F14, mm.OPLSName_DISPLAY)
F16_DISP_NAME = mm.opls_version_to_name(mm.OPLSVersion.F16, mm.OPLSName_DISPLAY)
FALLBACK_NAME = F14_DISP_NAME

FORCE_FIELD_NUMBER_TO_NAME = {
    OPLSVersion.F14: F14_DISP_NAME,
    OPLSVersion.F16: F16_DISP_NAME,
    MMFFS_INT: MMFFS_NAME
}

FFS_INCOMPATIBLE_PARAMS_MSG = f"""<html><p>Your customizations
        <a style="text-decoration:none;color:#3D3DF9;" href="opls_path">(path)</a>
        are either unavailable or incompatible with the {F16_DISP_NAME} force
        field currently in use in Schrödinger suite. New custom parameters are
        needed to accurately model your structures.</p>
        <p>You may run this job without customizations, but we recommend
        that you cancel now and generate the new parameters. Use the Force
        Field Builder, available from a Customize button on your panel or
        from the Task Tool.</p>"""

prefs = preferences.Preferences(preferences.MAESTRO)

# These are used to look up preferences the first time Maestro is opened,
# and hasn't stored its default preferences yet.  This will be fixed in 14-1.
# See: SHARED-2705
default_WET_opls_dir = os.path.join(
    fileutils.get_directory_path(fileutils.APPDATA), 'opls_dir')
pref_WET_dict = {
    (mm.MM_PREFER_FORCEFIELD_PATH_TYPE_KEY, None): FORCEFIELD_PATH_TYPE_DEFAULT,
    (mm.MM_PREFER_DEFAULT_CUSTOM_FORCEFIELD_PATH_KEY, None): default_WET_opls_dir,
    (mm.MM_PREFER_DEFAULT_OPLS_FORCEFIELD_KEY, OPLSVersion.F14): OPLSVersion.F14,
    (mm.MM_PREFER_USE_CUSTOM_FORCEFIELD_KEY, False): False
}  # yapf: disable


[docs]def get_pref_with_fallback(key, opt): """ This mimics prefs.get, except will try to look it up from a default dictionary if the preferences hasn't been set yet. See comment above about SHARED-2705 for more details. :param key: The key we're looking up the preference for. :type key: mmpref key :param opt: Optional argument when looking up key :type opt: bool/None/int """ pref = prefs.get(key, opt) if not pref: try: pref = pref_WET_dict[(key, opt)] except KeyError: raise KeyError(("Could not find preference '%s' in either stored " "preferences or default preferences.") % key) return pref
[docs]def has_valid_custom_OPLS_preference(): """ :return: whether the custom OPLS directory is a valid S-OPLS path :rtype: bool """ opls_dir = get_custom_opls_dir() return opls_dir is None or mm.is_valid_opls_directory(opls_dir)
[docs]class OPLSDirResult(enum.IntEnum): """ Return object for validate_opls_dir function """ ABORT = 0 # Cancel VALID = 1 # opls_dir is fine, use it DEFAULT = 2 # opls_dir is incompatible, use the default opls dir
[docs]class OPLSValidator: """ Validates OPLS-related settings. Meant for use by GUIs that may need to block other code while performing validation. """ INCOMPATIBLE_PARAMS_MSG = FFS_INCOMPATIBLE_PARAMS_MSG
[docs] def __init__(self, parent: QtWidgets.QWidget): """ :ivar parent: The parent object from which to display messageboxes :vartype parent: QWidget """ self.parent = parent self._validating = False
@property def validating(self) -> bool: """ Return whether or not the validator is currently performing validation. """ return self._validating
[docs] def validateOPLSDir(self, opls_dir: Optional[str] = None, allow_default_dialog: bool = True) -> OPLSDirResult: """ Return validation results of OPLS_DIR. If invalid, attempt to update the OPLS_DIR. If invalid and can't update OPLS_DIR, optionally prompt `ValidateOplsDirMessageBox`. See validate_opls_dir() for full documentation. :param opls_dir: Path to the custom OPLS directory. Use to avoid calling get_custom_opls_dir() twice in the event that the function is already called outside of this method. If not given or `None` the validation will be done for the user's preferences custom OPLS directory. :param allow_default_dialog: whether the user may be presented with the dialog allowing them to run with the default OPLS dir. """ if opls_dir is None: opls_dir = get_custom_opls_dir() self._validating = True try: valid = validate_opls_dir( opls_dir, parent=self.parent, incompatible_params_msg=self.INCOMPATIBLE_PARAMS_MSG, allow_default_dialog=allow_default_dialog) finally: self._validating = False return valid
[docs]class ForceFieldSelector(QtWidgets.QFrame): TOOLTIP = f""" The specified customizations are either unavailable or incompatible with the {F16_DISP_NAME} force field. New custom parameters are needed to accurately model your structures. This job may run without customizations, but we do not recommend it. Use the Force Field Builder, available from a Customize button on your panel or from the Task Tool. """ STYLE = """ QCheckBox {color:#aa6d09;} QCheckBox QToolTip {color:#444444; border-width:1px; border-style:solid; border-color:#aa6d09; border-radius:4px; background-color:#FDF8E7;} QLabel QToolTip {color:#444444; border-width:1px; border-style:solid; border-color:#aa6d09; border-radius:4px; background-color:#FDF8E7;} """
[docs] def __init__(self, layout=None, check_ffld_opls=False, show_when_no_choice=False, parent=None, stretch=True, add_customize_force_field=True): """Initialize the force field selector widget :param layout: The layout to add this widget to :type layout: `PyQt5.QtWidgets.QLayout` :param check_ffld_opls: If True, OPLS 2.1 will be shown if the user has the FFLD_OPLS licensing token. If False, the SOLUTIONS token will be used instead. This should be True for Desmond panels and False for all other panels. :type check_ffld_opls: bool :param show_when_no_choice: Whether to show the ffs even if the user has no choice of force fields :type show_when_no_choice: bool :param parent: The Qt parent :type parent: `PyQt5.QtWidgets.QWidget` :type stretch: bool :param stretch: If layout is supplied, the default behavior is to add a stretch to the layout after this Frame is added to it. Setting stretch to False prevents the stretch from being added to the layout. It has no effect if layout is not provided. :type add_customize_force_field: bool :param add_customize_force_field: Whether or not to add the customize force field widgets. """ QtWidgets.QFrame.__init__(self, parent) self._opls_dir_validator = OPLSValidator(parent=self) self._show_when_no_choice = show_when_no_choice self._check_ffld_opls = check_ffld_opls if layout is not None: if not isinstance(layout, QtWidgets.QLayout): raise ValueError("layout is not a QLayout") layout.addWidget(self) if stretch: layout.addStretch(1) self.layout = swidgets.SHBoxLayout(self) self.force_field_menu = ffComboBox( "Force field:", layout=self.layout, command=self._selectionChanged) self._populateMenu() if add_customize_force_field: self._addCustomize() maestro_hub.preferenceChanged.connect(self._onPreferenceChanged) self.update() self._hideIfNoChoice()
def _onPreferenceChanged(self, pref_name): """ Updates the widget state whenever Maestro preferences are changed """ if "forcefield_path" in pref_name: self._updateUseCustomizeForceFieldCheckbox()
[docs] def event(self, e): """ Handle the window activation. :param e: the event object :rtype: QTCore.QEvent """ # update the selector based on possible change to the contents of the # S-OPLS preferences directory, but only if the panel is not activated # due to message shown while the OPLS dir is being validated. allow_update = not self._opls_dir_validator.validating if e.type() == QtCore.QEvent.WindowActivate and allow_update: # ensure that the checkbox update happens after the activate event # is finished so we can display message boxes if necessary QtCore.QTimer.singleShot(0, self._updateUseCustomizeForceFieldCheckbox) return super().event(e)
@property def has_valid_opls_dir(self): """ Checks whether Maestro preferences point to a valid S-OPLS directory. :return: whether the OPLS dir is valid :rtype: bool """ return has_valid_custom_OPLS_preference() def _updateUseCustomizeForceFieldCheckbox(self): """ Updates the UI to reflect whether the current custom OPLS dir is valid. """ if hasattr(self, 'customize_button'): checked = self.use_custom_ffld_cb.isChecked() self._onUseCustomFFcbToggled(checked) def _hideIfNoChoice(self): """ Hides the entire FFS if there are fewer than two force field choices available, unless show_when_no_choice is True (False by default). """ if self._show_when_no_choice: return num_options = len(self.force_field_menu) if num_options < 2: self.hide() self.setEnabled(False) def _addCustomize(self): """ Add a Customize button if there is a license for the Force Field builder panel """ if _is_opls_available() and mm.mmcommon_display_ffbuilder(): self.customize_button = swidgets.SPushButton( "Customize...", command=self._customize_force_field, layout=self.layout) self.ff_warning_lbl = swidgets.SLabel() self.ff_warning_lbl.setTextFormat(QtCore.Qt.RichText) self.ff_warning_lbl.setText( "<img src=':/schrodinger/ui/qt/icons_dir/validation-warning'>") self.ff_warning_lbl.setStyleSheet(self.STYLE) self.use_custom_ffld_cb = swidgets.SCheckBoxWithSubWidget( "Use customized version", subwidget=self.ff_warning_lbl, layout=self.layout) self.use_custom_ffld_cb.toggled.connect( self._onUseCustomFFcbToggled) def _onUseCustomFFcbToggled(self, checked): """ Change the color and tooltip of the checkbox text, when the selected forcefield directory is incompatible after trying to sanitize it. Also displays the warning icon if necessary. :param checked: whether the check box is on or off. : type checked: bool """ style = 'color:black' tooltip = '' visible = False if not self.isVisible(): # prevent repeated dialogs if default custom S-OPLS is on and points # to a folder that can't be upgraded (sanitized) return f16_selected = (self.getSelectionForceFieldInt() == OPLSVersion.F16) if checked and f16_selected and not self.sanitizeCustomOPLSDir( allow_default_dialog=False): visible = True style = ForceFieldSelector.STYLE tooltip = ForceFieldSelector.TOOLTIP self.ff_warning_lbl.setToolTip(tooltip) self.ff_warning_lbl.setVisible(visible) self.use_custom_ffld_cb.setStyleSheet(style) self.use_custom_ffld_cb.setToolTip(tooltip) @QtCore.pyqtSlot(str) def _selectionChanged(self): """ Hooked up to the forcefield menu currentIndexChanged[str] signal. Shows or hides customize related widgets. """ if not hasattr(self, 'customize_button'): return if self.getSelectionForceFieldInt() == OPLSVersion.F16: hide_custom_widgets = False else: hide_custom_widgets = True for item in [self.customize_button, self.use_custom_ffld_cb]: item.setHidden(hide_custom_widgets) visible = (self.use_custom_ffld_cb.isChecked() and not self.has_valid_opls_dir) self.ff_warning_lbl.setVisible(visible and not hide_custom_widgets) def _customize_force_field(self): """ Slot launches maestro command to bring up FFLD toolkit. This is used as a slot. """ launch_ffbuilder_gui() def _populateMenu(self, additional_ff_items=None): """ Populate menu items based on licensing. Prepends the menu items with additional_ff_items as needed. """ menu_items = [F14_DISP_NAME] if self._includeOpls(): menu_items.append(F16_DISP_NAME) if additional_ff_items is not None: menu_items[:0] = additional_ff_items for menu_item in menu_items: self.force_field_menu.addItem(menu_item) def _includeOpls(self): """ Should the OPLS force field be included in the menu? :return: True if the OPLS force field should be included in the menu, False otherwise. :rtype: bool """ if self._check_ffld_opls: return mm.mmcommon_display_opls2() else: return mm.mmcommon_display_scisol()
[docs] def getSelectionForceField(self, index: Optional[int] = None) -> str: """ Return the string representation of the selected force field. If index is passed, the force field name at that index is returned. Because Maestro panels using this selector set options based on the common OPLS names, extracted names are regularized to be compatible. :param index: combo box index :return: the name corresponding to the selected force field """ if index is None: index = self.force_field_menu.currentIndex() name = self.force_field_menu.itemText(index) try: version_int = mm.opls_name_to_version(name) except IndexError: return name # leave non-OPLS alone return mm.opls_version_to_name(version_int, mm.OPLSName_COMMON)
[docs] def setSelectionForceField(self, force_field_int): """Set the force_field_menu's current text to the name matching the force field int. Will raise a key error if the int does not correspond to a possible selection and a value error if the int does not correspond to an available selection. :param force_field_int the integer corresponding to the force field :type force_field int :return: None """ force_field_name = FORCE_FIELD_NUMBER_TO_NAME[force_field_int] index = self.force_field_menu.findText(force_field_name) if index == -1: raise ValueError("{0} is not an available force field" \ .format(force_field_name)) self.force_field_menu.setCurrentIndex(index) self._selectionChanged()
[docs] def getSelectionForceFieldInt(self): """ Return the integer representation of the current force field. For names without mappable integers (i.e. those loaded by the MaestroForceFieldSelector) -1 is returned. :rtype: int """ text = self.force_field_menu.currentText() for version_int, name in FORCE_FIELD_NUMBER_TO_NAME.items(): if name == text: return version_int return -1
[docs] def getCustomOPLSDIR(self): """ Return OPLSDIR suitable for passing to jobcontrol through -OPLSDIR or None if usage of a custom directory is not selected. :rtype: str :returns: string of OPLS dir or None custom forcefield not selected """ # If the widget controlling the usage of a custom directory is # hidden we treat it as if it was not selected. if (hasattr(self, 'use_custom_ffld_cb') and self.use_custom_ffld_cb.isChecked() and not self.use_custom_ffld_cb.isHidden()): return get_custom_opls_dir() else: return None
[docs] def sanitizeCustomOPLSDir(self, allow_default_dialog=True): """ Sanitize the custom OPLS directory if a custom OPLS dir is used. If a custom OPLS directory is used that is not acceptable a dialog will be presented to allow the user to abort, or use the default parameters, see also `validate_opls_dir`. Note: A side-effect of this method call is that the custom force field checkbox may be unchecked. This means that `self.getCustomOPLSDir()` has to be called (again) if this method returns True in order to determine what the actual opls directory is that should be used. :param allow_default_dialog: whether the user may be presented with the dialog allowing them to run with the default OPLS dir. :type allow_default_dialog: bool :return: False if the custom OPLS dir choice is not acceptable. True in all other cases :rtype: bool """ opls_dir = self.getCustomOPLSDIR() if opls_dir: valid = self._opls_dir_validator.validateOPLSDir( opls_dir=opls_dir, allow_default_dialog=allow_default_dialog) if valid == OPLSDirResult.DEFAULT: self.use_custom_ffld_cb.setChecked(False) return valid != OPLSDirResult.ABORT return True
[docs] def update(self): """ Synchronize the maestro preferences with the selector. """ force_field_int = get_opls_preference() force_field_name = FORCE_FIELD_NUMBER_TO_NAME[force_field_int] index = self.force_field_menu.findText(force_field_name) # If not found, e.g. S-OPLS is not available but set in preferences if index == -1: # Fall back to OPLS_2005 (F14) index = self.force_field_menu.findText(FALLBACK_NAME) if index == -1: raise ValueError("Could not find fallback forcefield " f"{FALLBACK_NAME} in {self.force_field_menu}") self.force_field_menu.setCurrentIndex(index) if hasattr(self, 'use_custom_ffld_cb'): use_custom_pref = get_use_custom_forcefield_preference() self.use_custom_ffld_cb.setChecked(use_custom_pref) self._onUseCustomFFcbToggled(use_custom_pref) self._selectionChanged()
[docs] def hide(self): """ Hide all the children of the QFrame object and then hide the QFrame itself. """ self.force_field_menu.hide() self.force_field_menu.label.hide() if hasattr(self, 'customize_button'): self.customize_button.hide() if hasattr(self, 'use_custom_ffld_cb'): self.use_custom_ffld_cb.hide() super(ForceFieldSelector, self).hide()
[docs]class ForceFieldSelectorUi(ForceFieldSelector): """ A forcefield selector that accepts the standard QFrame initialization arguments so it can be used directly in Qt Designer by promoting a QFrame. """
[docs] def __init__(self, parent=None): super(ForceFieldSelectorUi, self).__init__(parent=parent)
[docs]class MacrocycleForceFieldSelector(ForceFieldSelector): """ A forcefield widget that contains an entry for MMFFs """ def _populateMenu(self): """ Populate the force field dropdown with the standard force fields plus MMFFs """ super(MacrocycleForceFieldSelector, self)._populateMenu() self.force_field_menu.addItem(MMFFS_NAME)
[docs]class ffComboBox(swidgets.SLabeledComboBox):
[docs] def showEvent(self, event): """ Override show event to update the default value in case the user has updated maestro preferences. """ self.update() super(ffComboBox, self).showEvent(event)
[docs]def get_custom_opls_dir(): """ Returns the custom OPLS directory defined in maestro or mmpref. This is used to specify OPLS_DIR to the job launch process. This is not local to the individual panel. Note: the path is not guaranteed to be a valid OPLS directory :rtype: str :return: path to custom OPLS dir """ if maestro: return _get_custom_opls_dir_maestro() else: return _get_custom_opls_dir_no_maestro()
[docs]@contextmanager def use_custom_opls_dir_preference(): """ If specified, sets the custom OPLSDIR Maestro preference into the env in a context managed scope. """ if get_use_custom_forcefield_preference(): with EnvironmentContext(OPLS_DIR_ENV, get_custom_opls_dir()): yield else: # do not set OPLS_DIR_ENV yield
def _get_custom_opls_dir_maestro(): """ Returns the custom OPLS directory defined in maestro. This is used to specify OPLS_DIR to the job launch process. This is not local to the individual panel. :rtype: str :return: path to custom OPLS dir """ path_type = maestro.get_command_option('prefer', 'forcefield_path_type') # path_type should be either 'custom' or 'default' maestro_pref = "%s_forcefield_path" % path_type directory = maestro.get_command_option('prefer', maestro_pref) return directory def _get_custom_opls_dir_no_maestro(): """ Returns the custom OPLS directory defined in the mmpref preferences. This is used to specify OPLS_DIR to the job launch process. This is not local to the individual panel. :rtype: str :return: path to custom OPLS dir """ key = mm.MM_PREFER_FORCEFIELD_PATH_TYPE_KEY path_type = get_pref_with_fallback(key, None) if path_type is None: raise KeyError('schrodinger.ui.qt.forcefield: custom opls dir type ' 'not found in preferences') if path_type == FORCEFIELD_PATH_TYPE_DEFAULT: key = mm.MM_PREFER_DEFAULT_CUSTOM_FORCEFIELD_PATH_KEY elif path_type == FORCEFIELD_PATH_TYPE_CUSTOM: key = mm.MM_PREFER_CUSTOM_FORCEFIELD_PATH_KEY directory = get_pref_with_fallback(key, None) if directory is None: raise KeyError('schrodinger.ui.qt.forcefield: custom opls dir not ' 'found in preferences') return directory
[docs]def get_opls_preference(): if maestro: return get_opls_preference_maestro() else: return get_opls_preference_no_maestro()
[docs]def get_opls_preference_maestro(): """ Returns default maestro OPLS preference. Raises a ValueError if maestro returns a value that is not compatible with this module. This is to ease addition, and testing maestro integer. :rtype: int :return: integer representation of forcefield """ maestro_pref = "default_opls_forcefield" force_field_int = int(maestro.get_command_option('prefer', maestro_pref)) if force_field_int not in FORCE_FIELD_NUMBER_TO_NAME: raise ValueError("Maestro value of {0} for {1} is not recognized by " "schrodinger.ui.qt.forcefield".format( force_field_int, maestro_pref)) return force_field_int
[docs]def get_opls_preference_no_maestro(): """ Returns default maestro OPLS preference. Raises a ValueError if maestro returns a value that is not compatible with this module. This is to ease addition, and testing maestro integer. :rtype: int :return: integer representation of forcefield """ key = mm.MM_PREFER_DEFAULT_OPLS_FORCEFIELD_KEY force_field_int = get_pref_with_fallback(key, OPLSVersion.F14) if force_field_int not in FORCE_FIELD_NUMBER_TO_NAME: raise ValueError("Maestro value of {0} for {1} is not recognized by " "schrodinger.ui.qt.forcefield".format( force_field_int, key)) return force_field_int
[docs]def get_use_custom_forcefield_preference(): if maestro: return get_use_custom_forcefield_preference_maestro() else: return get_use_custom_forcefield_preference_no_maestro()
[docs]def get_use_custom_forcefield_preference_maestro(): """ Returns use custom forcefield maestro preference. :rtype: bool :return: True if use custom forcefield is set in maestro preferences """ maestro_pref = "use_custom_forcefield" use_custom_pref = maestro.get_command_option('prefer', maestro_pref) if use_custom_pref == "True": return True elif use_custom_pref == "False": return False else: raise ValueError("Maestro value of {0} for {1} is not recognized by " "schrodinger.ui.qt.forcefield".format( use_custom_pref, maestro_pref))
[docs]def get_use_custom_forcefield_preference_no_maestro(): """ Returns use custom forcefield maestro preference. :rtype: bool :return: True if use custom forcefield is set in maestro preferences """ key = mm.MM_PREFER_USE_CUSTOM_FORCEFIELD_KEY use_custom_pref = get_pref_with_fallback(key, False) return use_custom_pref
[docs]class ValidateOplsDirMessageBox(messagebox.MessageBox): """ A message box allowing the user to cancel when the custom opls dir is not valid, or run with the default opls dir. A check box exists to allow unset the Maestro preference for using the custom opls dir. :cvar title: dialog window title :vartype title: str :cvar text: the message text :vartype text: str :ivar cancel_btn: the cancel button :vartype cancel_btn: QtWidgets.QPushButton :ivar run_defaults_btn: the 'Run with Defaults' button :vartype run_defaults_btn: QtWidgets.QPushButton :ivar incompatible_params_msg: the message to display when incompatible parameters are found. :vartype incompatible_params_msg: str """ TITLE = "Custom Parameters Incompatible" CB_TEXT = 'Do not show this message again'
[docs] def __init__( self, opls_dir, parent=None, incompatible_params_msg=FFS_INCOMPATIBLE_PARAMS_MSG, ): super().__init__( parent=parent, save_response_key=SAVE_RESPONSE_KEY, add_prefix_to_key=False) # Set add_prefix_to_key to False to ensure that the "Do not show" # state is shared between all force field selector subclasses. self.save_response_chk.setText(self.CB_TEXT) self.opls_dir = opls_dir self.setIcon(QtWidgets.QMessageBox.Question) self.setWindowTitle(self.TITLE) self._incompatible_params_msg = incompatible_params_msg self.setText(self._incompatible_params_msg) # find the QLabel that has our text and hook up the link call back for label in (lbl for lbl in self.children() if isinstance(lbl, QtWidgets.QLabel)): if label.text() == self._incompatible_params_msg: break label.setOpenExternalLinks(False) label.linkActivated.connect(self._showPath) label.linkHovered.connect(self._showPath) self.run_defaults_btn = self.addButton('Run with Defaults', QtWidgets.QMessageBox.AcceptRole) cancel_btn = self.addButton('Cancel', QtWidgets.QMessageBox.RejectRole) self.setDefaultButton(cancel_btn) self.setEscapeButton(cancel_btn) self.layout().setVerticalSpacing(20) self.run_defaults_btn.setMinimumWidth(100)
def _showPath(self): """ Callback method to show the tool tip for the link in the text """ QtWidgets.QToolTip.showText(self.cursor().pos(), self.opls_dir)
[docs] def getResponse(self): if self.clickedButton() == self.run_defaults_btn: return True return None
[docs]def validate_opls_dir(opls_dir, parent=None, incompatible_params_msg=FFS_INCOMPATIBLE_PARAMS_MSG, allow_default_dialog=True): """ Determine whether the automatically upgraded OPLS_DIR is valid. Display a message box to inform the user whether an automatic upgrade was performed. If the OPLS dir remains invalid and `allow_default_dialog` is True a dialog allowing the user to use the default OPLS dir is presented. :param opls_dir: the opls directory path or None :type opls_dir: str or None :param parent: the QtWidget used to post dialogs :type parent: QtWidgets.QtWidget :param incompatible_params_msg: the message to display when incompatible parameters are found. Default is for `ValidateOplsDirMessageBox` to use FFS_INCOMPATIBLE_PARAMS_MSG. :type incompatible_params_msg: str :param allow_default_dialog: whether the user may be presented with the `ValidateOplsDirMessageBox` allowing them to run with the default OPLS dir. :type allow_default_dialog: bool :return: the validation result :rtype: OPLSDirResult """ if opls_dir is None or mm.is_valid_opls_directory(opls_dir): return OPLSDirResult.VALID try: upgrade_oplsdir(opls_dir) except RuntimeError: pass else: messagebox.show_info(parent, MIGRATED_MESSAGE) return OPLSDirResult.VALID if not allow_default_dialog: return OPLSDirResult.ABORT msg_box = ValidateOplsDirMessageBox(opls_dir, parent, incompatible_params_msg) response = msg_box.exec_() if response: return OPLSDirResult.DEFAULT return OPLSDirResult.ABORT
[docs]def launch_ffbuilder_gui(): """ Launch the Force Field builder panel """ if maestro: maestro.command("pythonrunbuiltin ffbuilder_gui.panel") else: # FIXME: This is a bad way to open another panel with prepend_sys_path(fileutils.get_mmshare_scripts_dir()): import ffbuilder_gui ffbuilder_gui.panel()
[docs]class MaestroForceFieldSelector(ForceFieldSelector): """ This class is intened to use only for Maestro cpp panels that uses force field selectors to set force field names Signals emitted by this class. :cvar forcefieldOptionChanged: Signal emitted when the force field selection is changed. Emitted with: - the current selection of force field - the cpp panel name in which the selector is embedded :cvar useCustomforceFieldToggled: Signal emitted when 'Use custom force field' toggle state is changed. Emitted with: - the cpp panel name in which the selector is embedded - the current toggle state """ forcefieldOptionChanged = QtCore.pyqtSignal(str, str) useCustomForceFieldToggled = QtCore.pyqtSignal(str, bool)
[docs] def __init__(self, panel, mmod_ffoptions=None, *args, **kwargs): """ Initialize the Maestro force field selector widget :param panel: is the cpp panel name (used with showpanel command in Maestro) :type panel: str :param mmod_ffoptions: the list of forcefield names supported by macromodel cpp panel :type mmod_ffoptions: list or None """ self.cpp_panel = panel self.mmod_ffoptions = mmod_ffoptions self.build_ffmenu = False super().__init__(*args, **kwargs) self.setObjectName(panel) self.build_ffmenu = True maestro_hub.updateMaestroFFSelection.connect( self._setMaestroFFSelection)
def _populateMenu(self): """ Populate the force field dropdown with the standard force fields plus MMFFs """ additional_ff_items = [] if self.mmod_ffoptions is not None: additional_ff_items = self.mmod_ffoptions super()._populateMenu(additional_ff_items) def _setMaestroFFSelection(self, panel, ffname): """ Change the force field menu selection to ffname """ maestro_ffselector[panel].setForceField(ffname) @QtCore.pyqtSlot(str) def _selectionChanged(self): """ Override the hooked up slot to the ForceFieldSelector """ if self.build_ffmenu: self.forcefieldOptionChanged.emit(self.cpp_panel, self.getSelectionForceField()) super()._selectionChanged()
[docs] def setForceField(self, name: str): """ Set the force_field_menu's current text to the name matching the force field name. Because Maestro panels using this selector set with values based on the common OPLS names, they must be translated to available display names. :param name: the name corresponding to the force field """ try: version_int = mm.opls_name_to_version(name) except IndexError: pass else: # For OPLS names, regularize to combobox display name name = mm.opls_version_to_name(version_int, mm.OPLSName_DISPLAY) index = self.force_field_menu.findText(name) if index == -1: raise ValueError(f"{name} is not an available force field") self.force_field_menu.setCurrentIndex(index)
[docs] def getForceFieldOptions(self) -> List[str]: """ Obtain names for all available force field choices in the selector. Because Maestro panels using this selector set options based on the common OPLS names, extracted names are regularized to be compatible. :return: all available combo box options """ ff_options = [] for i in range(self.force_field_menu.count()): ff_options.append(self.getSelectionForceField(index=i)) return ff_options
def _onUseCustomFFcbToggled(self, checked): """ Override the onUserCustomFFcbToggled function :param checked: whether the check box is on or off. : type checked: bool """ super()._onUseCustomFFcbToggled(checked) self.useCustomForceFieldToggled.emit(self.cpp_panel, checked)
# Global dict to maintain maestro force field selector widgets maestro_ffselector = {}
[docs]def get_maestro_forcefield_selector(panel, default_opls, mmod_ffoptions, check_ffld_opls=False, show_when_no_choice=False, add_customize_force_field=True): """ This function is called from Maestro. Creates an instance of the 'MaestroForceFieldSelector' widget and passes on the QWidget to Maestro via MaestroHub::setForceFieldSelectorWidget SIGNAL. Also hooks up the SIGNAL emitted from MaestroForceFieldSelector class to MaestroHub::emitffselectorOptionChanged Slot See ForceFieldSelector for arguments documentation. :param panel: C++ panel name in which force field selector is embedded :type panel: str :param default_opls: default force field option to select :type panel: str :param mmod_ffoptions: list of forcefield options supported by given mmod panel :type mmod_ffoptions: list or None """ global maestro_ffselector maestro_ffselector[panel] = MaestroForceFieldSelector( panel, mmod_ffoptions, check_ffld_opls=check_ffld_opls, show_when_no_choice=show_when_no_choice, add_customize_force_field=add_customize_force_field) maestro_ffselector[panel].setForceField(default_opls) maestro_ffselector[panel].update() maestro_ffselector[panel].forcefieldOptionChanged.connect( maestro_hub.emitffselectorOptionChanged) maestro_hub.setForceFieldSelectorWidget.emit( panel, maestro_ffselector[panel], maestro_ffselector[panel].getForceFieldOptions()) maestro_ffselector[panel].useCustomForceFieldToggled.connect( maestro_hub.emitUseCustomFFStateChanged)