Source code for schrodinger.ui.qt.appframework

"""
This module provides GUI classes that mimic Maestro panels and dialogs.  The
main class is AppFramework for running Python applications with a Maestro
look-and-feel.  It provides a menu, standard buttons (Start, Write, Reset), a
Start dialog, a Write dialog, a input source selector, and methods for adding
all the relevant job parameters to a single object (used by the user when
creating input files and launching jobs).  The StartDialog and WriteDialog
actually can be used independently of AppFramework, in case a user doesn't
require a full Maestro-like panel.  There also is an AppDialog class for a
Maestro-like dialog (i.e., with buttons packed in the lower right corner).

PyQt Note: You can specify the QtDesigner-generated form via the <ui> argument.
Widgets defined within that form are automatically included in the window. The
form should be read from the _ui.py file as follows::

    import <my_script_ui>
    ui = my_script_ui.Ui_Form()


AppFramework places key job information (e.g., the jobname and host from the
StartDialog) into a JobParameters object that is stored as the 'jobparam'
attribute.  When the user's start or write method is called, retrieve the
needed job information from that object.

AppFramework provides a method for application-specific job data to be placed
into the JobParameters object.  Suppose the user's GUI is modular, with
several frames responsible for various parameters of the job.  Any object
that has a 'setup' method can be registered with the AppFramework object, and
this method will be called as part of the setup cascade that occurs when the
Start or Write button is pressed.  To register an object, append the object to
the AppFramework's 'setup' list.  The JobParameters object will be passed to
the registered object via the 'setup' method.  The 'setup' method should
return True if the setup cascade should continue (i.e., there are no
problems).

Copyright Schrodinger, LLC. All rights reserved.

"""

import glob
import os
import sys

# Workaround for PANEL-13510 - remove *.pgf file save option for plots as it
# crashes Python (and Maestro) on Windows if LaTeX is not installed.
from matplotlib import backend_bases

import schrodinger.job.jobcontrol as jobcontrol
# Install the appropriate exception handler
from schrodinger.infra import exception_handler
from schrodinger.infra import jobhub
from schrodinger.job import jobhandler
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui import maestro_ui
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import icons
from schrodinger.ui.qt import jobwidgets
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt import style as qtstyle
from schrodinger.ui.qt import swidgets  # noqa: F401
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.appframework2 import application  # noqa: F401
from schrodinger.ui.qt.appframework2 import jobnames
from schrodinger.ui.qt.config_dialog import DISP_APPEND  # noqa: F401
from schrodinger.ui.qt.config_dialog import DISP_APPENDINPLACE  # noqa: F401
from schrodinger.ui.qt.config_dialog import DISP_FLAG_FIT  # noqa: F401
from schrodinger.ui.qt.config_dialog import DISP_IGNORE  # noqa: F401
from schrodinger.ui.qt.config_dialog import DISP_NAMES
from schrodinger.ui.qt.config_dialog import DISP_REPLACE  # noqa: F401
from schrodinger.ui.qt.config_dialog import LOCALHOST
from schrodinger.ui.qt.config_dialog import LOCALHOST_GPU
from schrodinger.ui.qt.config_dialog import ConfigDialog
from schrodinger.ui.qt.config_dialog import DialogParameters
from schrodinger.ui.qt.config_dialog import Gpu  # noqa: F401
from schrodinger.ui.qt.config_dialog import Host  # noqa: F401
from schrodinger.ui.qt.config_dialog import JobParameters
from schrodinger.ui.qt.config_dialog import RequestedAction
from schrodinger.ui.qt.config_dialog import StartDialog  # noqa: F401
from schrodinger.ui.qt.config_dialog import _EntryField
from schrodinger.ui.qt.config_dialog import get_hosts  # noqa: F401
# For backwards compatability with some scripts; remove later:
from schrodinger.ui.qt.input_selector import InputSelector
from schrodinger.ui.qt.input_selector import format_list_to_filter_string
from schrodinger.ui.qt.input_selector import get_workspace_structure  # noqa: F401
from schrodinger.ui.qt.standard.constants import BOTTOM_TOOLBUTTON_HEIGHT
from schrodinger.ui.qt.utils import help_dialog
from schrodinger.utils import fileutils
from schrodinger.utils import mmutil  # noqa: F401

INCLUDED_ENTRY = InputSelector.INCLUDED_ENTRY
INCLUDED_ENTRIES = InputSelector.INCLUDED_ENTRIES
SELECTED_ENTRIES = InputSelector.SELECTED_ENTRIES
WORKSPACE = InputSelector.WORKSPACE
FILE = InputSelector.FILE

exception_handler.set_exception_handler()

HELP_BUTTON_HEIGHT = 22

# Maestro mainwindow dock area options
LEFT_DOCK_AREA = QtCore.Qt.LeftDockWidgetArea
RIGHT_DOCK_AREA = QtCore.Qt.RightDockWidgetArea
TOP_DOCK_AREA = QtCore.Qt.TopDockWidgetArea
BOTTOM_DOCK_AREA = QtCore.Qt.BottomDockWidgetArea

DESRES_COPYRIGHT_INFO = \
"""Desmond Molecular Dynamics System, Copyright (c) D. E. Shaw Research.
Portions of Desmond Software, Copyright (c) Schrodinger, LLC.
All rights reserved."""

#
# GLOBAL FUNCTIONS ###
#

maestro = None
# Check for Maestro
try:
    from schrodinger.maestro import maestro
except:
    maestro = None

try:
    del backend_bases._default_filetypes['pgf']
except KeyError:
    pass


[docs]def get_next_jobname(prefix, starting_num=1): """ Given a job name prefix, choose the next available job name based on the names of existing files in the CWD. :param starting_num: The smallest number to append to the job name :type starting_num: int :return: The completed job name :rtype: str """ MAX_NUM = 999 filenames = glob.glob(prefix + "_*") # Includes dirs also max_used = starting_num - 1 for jobnum in range(starting_num, MAX_NUM + 1): jobname = "%s_%i" % (prefix, jobnum) if jobname in filenames: # Probably the job directory max_used = jobnum else: jdot = jobname + "." if any([f.startswith(jdot) for f in filenames]): max_used = jobnum if max_used == MAX_NUM: # Went through all combinations and all are taken return prefix else: return "%s_%i" % (prefix, max_used + 1)
[docs]def make_desres_layout(product_text='', flip=False, vertical=False, third_party_name='Desmond', third_party_dev='Developed by D. E. Shaw Research'): """ Generate a QLayout containing the Desres logo and Schrodinger product info :param product_text: Name of Schrodinger product to add opposite the DESRES logo :type product_text: str :param flip: whether to reverse the two logos left/right :type flip: bool :param vertical: whether to display the logos stacked vertically or side by side horizontally :type vertical: bool :type third_party_name: str :param third_party_name: The name of the third party product :type third_party_dev: str :param third_party_dev: The developer of the third party product """ if vertical: desres_layout = QtWidgets.QVBoxLayout() txt_format = "<b><font face=Verdana size=+1>%s </font></b><font size=-1>%s</font>" else: desres_layout = QtWidgets.QHBoxLayout() txt_format = "<b><font face=Verdana size=+1>%s</font></b><br><font size=-1>%s</font>" desmond_product_label = QtWidgets.QLabel() desmond_product_label.setTextFormat(QtCore.Qt.RichText) desmond_product_label.setText(txt_format % (third_party_name, third_party_dev)) product_label = QtWidgets.QLabel() product_label.setTextFormat(QtCore.Qt.RichText) product_label.setText(txt_format % (product_text, "Developed by Schr&ouml;dinger")) if flip and product_text: desres_layout.addWidget(product_label) if not vertical: desres_layout.addStretch() desres_layout.addWidget(desmond_product_label) else: desres_layout.addWidget(desmond_product_label) if not vertical: desres_layout.addStretch() if product_text: desres_layout.addWidget(product_label) return desres_layout
[docs]def make_desres_about_button(parent): """ Generate the about button with copyright information for DESRES panels. :param parent: The parent widget (the panel) :type parent: QWidget """ b = QtWidgets.QPushButton("About") def about(): QtWidgets.QMessageBox.about(parent, "About", DESRES_COPYRIGHT_INFO) b.clicked.connect(about) return b
[docs]class CustomProgressBar(QtWidgets.QProgressBar): """ Class for a custom progress bar (at the bottom of some panels). Brings up the monitor panel if clicked (and if monitoring a job). """
[docs] def __init__(self, parentwidget): super(CustomProgressBar, self).__init__(parentwidget) palette = self.palette() self.app = parentwidget self.normal_group_role_colors = [] for group in [QtGui.QPalette.Active, QtGui.QPalette.Inactive]: for role in [QtGui.QPalette.Highlight, QtGui.QPalette.Highlight]: color = palette.color(group, role) self.normal_group_role_colors.append((group, role, color))
[docs] def setError(self, error): """ Set the color of the progress bar to red (error=True) or normal color (error=False). """ palette = self.palette() for group, role, normal_color in self.normal_group_role_colors: if error: palette.setColor(group, role, QtCore.Qt.darkRed) else: palette.setColor(group, role, normal_color) self.setPalette(palette) self.update()
[docs] def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: if self.app.current_job: self.app.monitorJob(self.app.current_job.job_id, showpanel=True)
INFORMATION = 'information' WARNING = 'warning' CRITICAL = 'critical'
[docs]@qt_utils.remove_wait_cursor def question(msg, button1="OK", button2="Cancel", parent=0, title="Question"): """ Display a prompt dialog window with specified text. Returns True if first button (default OK) is pressed, False otherwise. """ mbox = QtWidgets.QMessageBox(parent) mbox.setText(msg) mbox.setWindowTitle(title) mbox.setIcon(QtWidgets.QMessageBox.Question) b1 = mbox.addButton(button1, QtWidgets.QMessageBox.ActionRole) b2 = mbox.addButton(button2, QtWidgets.QMessageBox.RejectRole) mbox.exec() return (mbox.clickedButton() == b1)
[docs]def filter_string_from_supported(support_mae=False, support_sd=False, support_pdb=False, support_cms=False): """ Return a Qt filter string for these formats. :param support_mae: Whether to support maestro (strict) format. :type support_mae: bool :param support_sd: Whether to support SD format. :type support_sd: bool :param support_pdb: Whether to support PDB format. :type support_pdb: bool :param support_cms: Whether to support Desmond CMS files. :type support_cms: bool :return: Qt filter string :rtype: str """ formats = [] if support_mae: formats.append(fileutils.MAESTRO_STRICT) if support_sd: formats.append(fileutils.SD) if support_pdb: formats.append(fileutils.PDB) if support_cms: formats.append(fileutils.CMS) return filedialog.filter_string_from_formats(formats)
class _ToolButton(QtWidgets.QToolButton): def __init__(self, *args, **kwargs): QtWidgets.QToolButton.__init__(self, *args, **kwargs) self.setContentsMargins(0, 0, 0, 0) self.setAutoRaise(True)
[docs]class AppDockWidget(maestro_ui.MM_QDockWidget): """ A DockWidget that calls the parent class's closeEvent when closing """
[docs] def __init__(self, master, object_name, dockarea): """ Create an AppDockWidget instance :type master: QWidget :param master: The parent widget whose closeEvent method will get called when the QDockWidget closes """ maestro_ui.MM_QDockWidget.__init__(self, object_name, dockarea, True, True) self.master = master
[docs] def closeEvent(self, event): """ Called by PyQt when the dock widget is closing. Calls the parent window's closeEvent. :type event: QCloseEvent :param event: The QCloseEvent generated by the widget closing """ # Call the parent close event (python-1930) since the parent window is # actually never shown. self.hide() self.master.closeEvent(event)
# # SCHRODINGER APPLICATION WINDOW ### #
[docs]class AppFramework(QtWidgets.QMainWindow): """ The AppFramework class is basically a hull for applications. With its own instance of a DialogParameters class, Application manages Schrodinger dialogs like start, read, and write so that the programmer is free to worry about other things. The dialogs can be manipulated at anytime to fit the needs of the programmer (please see the DialogParameters class below). The Application class also comes with an instance of the JobParameters class (jobparam). This object is intended to be used to store all GUI settings. Please see the JobParameters class below for more information on proper usage. In addition to these features, the AppFramework class can take arguments for Main Menu creation. Supposing I wanted a menu of the form:: File Edit Help ^Save ^Options ^About ^Close ^^First Option ^^Second Option When I create my AppFramework instance, I would pass the following keyword, value pair:: menu = { 1: ["File", "Edit", "Help"], "File": { "Save": {"command": <command_here>}, "Close": {"command": <command_here>}, }, "Edit": {"Options": { "First Option": {"command": <command_here>}, "Second Option": {"command": <command_here>}, }, }, "Help": { "About": {"command": <command_here>}, }, } The dictionary key 1 (int form, not string form) is the key for an ordering list. Because dictionaries are not ordered, this is needed to specify your prefered order. AppFramework also manages the Bottom Button Bar. One more keyword for the AppFramework instance is "buttons", and would appear like:: buttons = {"start": {"command": <command_here>}, "dialog": <boolean_show_dialog?>}, "read": {"command": <command_here>}, "write": {"command": <command_here>, "dialog": <boolean_show_dialog?>}, "reset": {"command": <command_here>}, "close": {"command": <command_here>}, "help": {"command": <command_here>}, } The six supported buttons are "start", "read", "write", "reset", "close", and "help". Any button name for which you supply a command will be created and placed in the correct location. Non-default button names can be used by supplying a 'text' keyword. You can also specify a command for start, read, and write buttons that will be called when the button is pressed before displaying the dialog with the 'precommand' keyword. For example, you may want to check an input before allowing the start dialog to appear. These functions should return 0 unless the dialog should not be displayed (nor the specified start/read/write function called). Any function that returns a non-zero value will halt the dialog process, and return to the GUI. If "checkcommand" is specified, then this function will be called when the user clicks on the Start/Write button (before closing the dialog), and if the function returns a non-zero value, the dialog will not close. The only argument that is given is a job name. This callback is typically used to check for existing files based on the user-specified jobname. IMPORTANT: If the function returns 0, and <jobname>.maegz exists, AppFramework will overwrite the file without asking the user. It is up to your application to make sure the user is okay with that file being overwritten. Before the user supplied command is called, if there is an associated dialog, the AppFramework presents the dialog, saves the input in the jobparam object mentioned above. Then AppFramework calls the command. Button configuration options are: - command - Required callback function. - precommand - Optional command called prior to displaying dialog. Available for Start, Read, and Write dialogs. - dialog - If False, the Start or Write dialog is not displayed. - text - If used, overrides the text of the button. Finally, there is an Input Frame that can be used. Please see that class below for information on proper use. :type subwindow: bool :param subwindow: If subwindow is True, this panel will not try to start a new QApplication instance even if running outside of Maestro (useful for subwindows when a main window is already running). :type dockable: bool :param dockable: If True this instance will be dockable in Maestro. :type dockarea: Qt QFlags (See constants at top of this module) :param dockarea: By default this is to the right, but you can specify any of the DOCK_AREA constants specified in this module. :type periodic_callback: callable :param periodic_callback: If specified, this function will be called periodically, a few times a second (frequency is not guaranteed). :type config_dialog_class: ConfigDialog class or subclass of ConfigDialog :param config_dialog_class: This allows you to pass in custom dialogs to be used to configure your panel. :type help_topic: str :param help_topic: If given, a help button will be created and connected to the specified topic. Note that if help_topic is given, then buttons['help']['command'] will be ignored. :type show_desres_icon: bool :param show_desres_icon: A large amount of panels are contractually bound to show a DE Shaw Research icon, turning this to True will automatically add it to the bottom of the panel. :type product_text: str :param product_text: In panels where DE Shaw Research requires an icon (see above), we also sometimes need to specify that parts of the technology present were developed by Schrodinger. This option will only be used if show_desres_icon==True. """ # Define custom slots for this class: jobCompleted = QtCore.pyqtSignal(jobcontrol.Job)
[docs] def __init__(self, ui=None, title=None, subwindow=False, dockable=False, dockarea=RIGHT_DOCK_AREA, buttons=None, dialogs=None, inputframe=None, menu=None, periodic_callback=None, config_dialog_class=ConfigDialog, help_topic=None, show_desres_icon=False, product_text="", flip=False): """ See class docstring. """ self.subwindow = subwindow self.config_dialog_class = config_dialog_class if maestro or self.subwindow: self._app = None else: # If running as the main panel outside of Maestro, # create a QApplication: self._app = QtWidgets.QApplication.instance() if not self._app: self._app = QtWidgets.QApplication([]) self._dockable = dockable self._dockarea = dockarea self._dock_widget = None QtWidgets.QMainWindow.__init__(self) if maestro: self._toplevel = maestro.get_maestro_toplevel_window() self._updateToolWindowFlag() else: self._toplevel = None if self._toplevel: self._dock_widget = AppDockWidget(self, title, self._dockarea) # If title option was passed, set Window title and save # in the job parameter class for later use if title: if self.isDockableInMaestro(): self._dock_widget.setWindowTitle(title) self._dock_widget.setObjectName(title) self._dock_widget.setAllowedAreas(RIGHT_DOCK_AREA | LEFT_DOCK_AREA) else: self.setWindowTitle(title) # Read and apply the Schrodinger-wide style sheet qtstyle.apply_styles() qtstyle.apply_legacy_spacing_style(self) # If running outside Maestro set the icon to the Maestro icon if not maestro: icon = QtGui.QIcon(icons.MAESTRO_ICON) self.setWindowIcon(icon) # Create instance of JobParameter class, initialize some values self.jobparam = JobParameters() # Create instance of DialogParameter class self.dialog_param = DialogParameters() # Initialize list of widgets for AppFramework # List is run through in setupJobParameters() # in order to set values in self.jobparam self.setup = [] # Create central widget for the window: if self.isDockableInMaestro(): self.main_widget = QtWidgets.QWidget(self._dock_widget) else: self.main_widget = QtWidgets.QWidget(self) # Create a Vertical layout to manage the main window: self.main_layout = QtWidgets.QVBoxLayout(self.main_widget) self.main_layout.setContentsMargins(3, 3, 3, 3) # If menu was requested, set it up if menu: self.createMenus(menu) # If Job Input Frame was requested, make, pack, and register it if inputframe: self._if = InputSelector(self, **inputframe) self.main_layout.addWidget(self._if) # Add horizontal line between the job input frame and interior: self.hline2 = QtWidgets.QFrame(self) self.hline2.setFrameShape(QtWidgets.QFrame.HLine) self.hline2.setFrameShadow(QtWidgets.QFrame.Raised) self.main_layout.addWidget(self.hline2) else: self._if = None # Create the Main Panel self.interior_frame = QtWidgets.QFrame(self) self.interior_layout = QtWidgets.QVBoxLayout(self.interior_frame) self.interior_layout.setContentsMargins(0, 0, 0, 0) # Add a stretch factor of 10: self.main_layout.addWidget(self.interior_frame, 10) if self.isDockableInMaestro(): self._dock_widget.setWidget(self) self._dock_widget.setupDockWidget.emit() hub = maestro_ui.MaestroHub.instance() hub.preferenceChanged.connect(self._dockPrefChanged) self._progress_bar_frame = QtWidgets.QFrame(self) self._progress_bar_layout = QtWidgets.QVBoxLayout( self._progress_bar_frame) self._progress_bar_layout.setContentsMargins(0, 0, 0, 0) self._progress_bar_layout.setSpacing(0) self.main_layout.addWidget(self._progress_bar_frame) # Ev:117449 Add a progress bar to the bottom of the panel (above the buttons): #self._progress_bar = QtWidgets.QProgressBar(self) self._progress_bar = CustomProgressBar(self) self._progress_bar.hide() # Hide for now if ui: # Set up the Qt Designer widgets and add to the App Framework: self.ui = ui # NOTE: The Qt-Designer & pyuic4 generated *_ui.py file can have # various ways of specifying the widgets and layouts, and we # here try to support all common ways of specifying them. # Setup a test widget using the setupUi() method defined in the # *_ui.py file: widget = QtWidgets.QFrame() try: self.ui.setupUi(widget) except AttributeError: # QMainWindow-based *.ui file was specified, re-run the # setupUi() method on self (QMainWindow): self.ui.setupUi(self) # Hide the status bar, as Maestro's panels don't have a status bar. # The status bar would also make the panel look weird with the # empty space under the Start/Write/Help/Close buttons. self.statusBar().hide() self.addCentralWidget(self.ui.centralwidget) # NOTE: If any changes are made to this section, please test # out the 2D Viewer, which uses this layout scheme else: # QWidget (or QDialog) based *.ui file was specified # Ev:97527 Have AppFramework work with different Designer # outputs: num_widget_children = 0 for child in widget.findChildren(QtWidgets.QWidget): if child.parent() == widget: num_widget_children += 1 if num_widget_children > 1: self.addCentralWidget(widget) else: for child in widget.findChildren(QtWidgets.QWidget): if child.parent() == widget: self.addCentralWidget(child) # NOTE: If this section is changed, please test out # structure_morpher.py (based on QDialog) self.status_label = QtWidgets.QLabel() self.statusBar().addWidget(self.status_label) # If help_topic was given, create a buttons dictionary entry for it self._help_topic = help_topic if help_topic: if buttons is None: buttons = {} buttons.setdefault('help', {})['command'] = self.help self.show_desres_icon = show_desres_icon self._setupDESResIcon(product_text, flip) # Add horizontal line between interior and buttons: # Disabled because it takes up too much space. if buttons: self.hline = QtWidgets.QFrame(self) self.hline.setFrameShape(QtWidgets.QFrame.HLine) self.hline.setFrameShadow(QtWidgets.QFrame.Raised) self.main_layout.addWidget(self.hline) # If dialog parameters were set, update them if dialogs: self.dialog_param.update(dialogs) self.viewname = str(self) # Initialize configuration dialog to nothing so it uses a new instance # if necessary self._sd = None self.sd_params = None # Create the bottom button bar if requested if buttons: self._setupBottomButtons(**buttons) else: self._buttonDict = {} # Since this updates job parameters, we have to place it # under the above. # Set the job viewname used to filter in the monitor panel. this # name defaults to the __name__ of the panel. self.dialog_param.start["viewname"] = self.viewname self.dialog_param.write["viewname"] = self.viewname self.setCentralWidget(self.main_widget) # Used to let the closeEvent know that the panel already knows it is # qutting. self.quitting = False # Ev:123813 Save a reference to the user's peridodic callback (if any): self._users_periodic_callback = periodic_callback # Ev:117449 The job that is currently tracked by the progress bar: self.current_job = None # Whether to actually monitor the status of this job: self.current_job_monitor = False if maestro: self._last_time_checked_status = 0 maestro.periodic_callback_add(self._periodicCallback) maestro.command_callback_add(self._commandCallback) else: # If run outside Maestro we set up a timer for the periodic # callbacks. Ev:123816 self._last_time_checked_status = 0 self._timer = QtCore.QTimer() self._timer.timeout.connect(self._periodicCallback) self._timer.start(100) jmgr = jobhub.get_job_manager() if jobhandler.is_auto_download_active(): jmgr.jobDownloaded.connect(self._onJobCompleted) else: # FIXME PANEL-18802: with JOB_SERVER outside of maestro, jobs not # launched with jobhandler won't be downloaded jmgr.jobCompleted.connect(self._onJobCompleted) jmgr.jobProgressChanged.connect(self._onJobProgressChanged) self.updateStatusBar()
def __str__(self): return "{0}.{1}".format(self.__module__, self.__class__.__name__)
[docs] def setStyleSheet(self, stylesheet): # Override to ensure that style sheet changes after the first are # actually applied require_refresh = bool(self.styleSheet()) super().setStyleSheet(stylesheet) if require_refresh: qt_utils.update_widget_style(self)
def _dockPrefChanged(self, option): """ Slot to reconfigure dock panel due to dock preference changes. Docking preference change can be one of the following: - Allow docking - Disallow docking - Allow docking in main window - Allow docking in floating window User can switch between above mentioned options, so dock panel needs to be reconfigured accordingly. :param option: Name of the changed Maestro preference. :type option: str """ if option in ["docklocation", "dockingpanels"]: self._dock_widget.reconfigureDockWidget.emit(self) def _updateToolWindowFlag(self): if sys.platform == 'darwin': # Python panels on Mac can be obscured by other Maestro # panels when 'Show panels on top' preference is on. # We need to set 'Qt.Tool' window flag to prevent this. # PYTHON-2033 and PYTHON-2070 is_shown = not self.isHidden() if maestro.get_command_option("prefer", "showpanelsontop") == "True": self.setWindowFlags(self.windowFlags() | QtCore.Qt.Tool) else: self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.Tool) # Changing the window flags will hide the window; so re-show it: if is_shown: self.show() def _commandCallback(self, command): """ Called by Maestro when a command is issued, and is used to update the 2D Viewer preferences when Maestro's preferences change. """ # PYTHON-2070 s = command.split() if s[0] == "prefer": option = s[1].split('=')[0] if option == "showpanelsontop": self._updateToolWindowFlag()
[docs] def isDockableInMaestro(self): """ Returns True if the PyQt panel can be docked into Maestro mainwindow. Otherwise returns false. This function should be called only after parsing the 'dockable' argument inside the constructor. """ if maestro and self._dockable: return True else: return False
[docs] def interior(self): """ Return the interior frame where client widgets should be parented from """ return self.interior_frame
[docs] def addCentralWidget(self, w): """ Add a widget to the central area of the AppFramework dialog """ self.interior_layout.addWidget(w)
[docs] def exec(self): """ Calls exec() method of the application. """ self.getApp().exec()
[docs] def show(self, also_raise=True, also_activate=True): """ Redefine the show() method to deiconize if necessary and also raise_() the panel if 'also_raise' is specified as True. :type also_raise: bool :param also_raise: If True (default), the `raise_` method will also be called on the window. If False it is not. This is important on some Window managers to ensure the window is placed on top of other windows. :type also_activate: bool :param also_activate: If True (default), the `activateWindow` method will also be called on the window. If False it is not. This is important on some Window managers to ensure the window is placed on top of other windows. """ if self.isMinimized(): self.showNormal() else: if self.isDockableInMaestro(): self._dock_widget.show() else: QtWidgets.QMainWindow.show(self) if also_raise: self.raise_() if also_activate: self.activateWindow()
# # SCHRODINGER MENU CREATION ### # # TODO: Allow specification of shortcuts # TODO: Support "types" for menu items (check boxes etc).
[docs] def createMenus(self, d): """ Setup for making individual menus, create MainMenuBar. """ self.menu_bar = QtWidgets.QMenuBar(self) # Ev:123651 Do not replace Maestro's menu bar on a Mac; but instead show # this menu bar at the top of this panel only: self.menu_bar.setNativeMenuBar(False) import warnings warnings.warn( "AppFramework: The ability to create menus within panels have been deprecated. Please re-design your panel to get rid of the menu bar. See Ev:123651.", PendingDeprecationWarning) self.setMenuBar(self.menu_bar) # Order the keys if possible if 1 in d: menunames = d[1] else: menunames = list(d) for menuname in menunames: self._makeMenu(menuname, d[menuname])
def _makeMenu(self, menuname, d): """ Create the menu <menuname> """ menu = self.menu_bar.addMenu(menuname) if 1 in d: # Order list specified itemnames = d[1] else: itemnames = list(d) for itemname in itemnames: # Add the item to the menu: self._createMenuItem(menu, itemname, d[itemname]) def _createMenuItem(self, menu, itemname, d): """ Setup the item <itemname> of menu <menu>. Creates the item or cascading menu. """ # Cascading menu items have values of the dict that # are dictionaries themselves: cascading = False for value in dict.values(): if type(value) is dict: cascading = True if not cascading: self._makeItem(menu, itemname, dict) else: # Cascading menu item (treat as sub-menu): sub_menu = menu.addMenu(itemname) if 1 in d: # Order list specified subitemnames = d[1] else: subitemnames = list(d) for subitemname in subitemnames: # Call this function on this sub-menu: self._createMenuItem(sub_menu, subitemname, d[subitemname]) def _makeItem(self, menu, itemname, d): """ Create individual item in a menu. """ action = menu.addAction(itemname) # If there's a command associated with that menu when connect it # up now: if 'command' in d: action.triggered.connect(d["command"]) # # SCHRODINGER BOTTOM BUTTON BAR ### # def _setButtonDefaults(self, button, default_name, default_dialog): """ Sets defaults for unspecified button options and adds ellipses to the button text if it brings up a dialog. Raises a SyntaxError if no command is specified for the button in the button dictionary. """ if button in self._buttonDict: if 'dialog' not in self._buttonDict[button]: self._buttonDict[button]['dialog'] = default_dialog if 'text' not in self._buttonDict[button]: # Was not modified by the user if 'dialog' in self._buttonDict[ button] and 'start' not in self._buttonDict: self._buttonDict[button]['text'] = default_name + '...' else: self._buttonDict[button]['text'] = default_name if 'command' not in self._buttonDict[button]: raise SyntaxError("No command specified for button '%s'." % button) # elif not callable(self._buttonDict[button]['command']): # raise SyntaxError("Command specified for button '%s' is invalid." % button) if 'precommand' not in self._buttonDict[button]: self._buttonDict[button]['precommand'] = None if 'checkcommand' not in self._buttonDict[button]: self._buttonDict[button]['checkcommand'] = None def _setupDESResIcon(self, product_text, flip=False): """ If requested, this function adds the DESRES icon and potentially Schrodinger text information. :type product_text: str :param product_text: If not empty, what Schrodinger text to put across from the DESRES icon. """ if not self.show_desres_icon: return desres_layout = make_desres_layout(product_text, flip) self.main_layout.addLayout(desres_layout) def _setupBottomButtons(self, **_buttonDict): """ Create the requested bottom buttons, configured according to the 'buttonDict'. """ self._buttonDict = _buttonDict # Create bottom button frame complete with horizontal rule self._schroBottomButtonFrame = QtWidgets.QFrame(self) self.main_layout.addWidget(self._schroBottomButtonFrame) self._schroBottomButtonLayout = QtWidgets.QHBoxLayout( self._schroBottomButtonFrame) self._schroBottomButtonLayout.setContentsMargins(0, 0, 0, 0) self._schroBottomButtonFrame.setLayout(self._schroBottomButtonLayout) self._bottomToolbar = QtWidgets.QToolBar() self._schroBottomButtonLayout.addWidget(self._bottomToolbar) self._setButtonDefaults('start', 'Start', 1) self._setButtonDefaults('write', 'Write', 1) self._setButtonDefaults('read', 'Read', 1) self._setButtonDefaults('reset', 'Reset Panel', 0) self._setButtonDefaults('help', 'Help', 0) self._setButtonDefaults('close', 'Close', 0) # Creating Buttons as Requested by 'buttons' Dictionary argument self._buttons = {} self._actions = {} # Start Button if 'start' in self._buttonDict: self.default_jobname = self.dialog_param.start.get( "jobname", "jobname") self._jobname_label = QtWidgets.QLabel("Job name:") self._bottomToolbar.addWidget(self._jobname_label) self._jobname_le = QtWidgets.QLineEdit() self._jobname_le.setContentsMargins(2, 2, 2, 2) self._jobname_le.setToolTip('Enter the job name here') self._updatePanelJobname(True) self._jobname_le.editingFinished.connect(self._populateEmptyJobname) self._bottomToolbar.addWidget(self._jobname_le) self._buttons['settings'] = _ToolButton() self._buttons['settings'].setToolTip('Show the run settings dialog') self._bottomToolbar.addWidget(self._buttons['settings']) self._buttons['settings'].clicked.connect(self._settings) self._buttons['settings'].setIcon( QtGui.QIcon(':/icons/small_settings.png')) self._buttons['settings'].setPopupMode( QtWidgets.QToolButton.MenuButtonPopup) # Set the object name so the stylesheet can style the button: self._buttons['settings'].setObjectName("af2SettingsButton") self._buttons['settings'].setFixedHeight(BOTTOM_TOOLBUTTON_HEIGHT) self._buttons['settings'].setFixedWidth(BOTTOM_TOOLBUTTON_HEIGHT) self._job_start_menu = QtWidgets.QMenu() self._buttons['settings'].setMenu(self._job_start_menu) self._job_start_menu.addAction("Job Settings...", self._settings) # If running in Maestro session, then add Preference menu item # first to be consistent with Maestro Job toolbar menu in the # panel. if maestro: self._actions['preference'] = QtGui.QAction( "Preferences...", self) self._actions['preference'].triggered.connect( self._jobprefersettings) self._job_start_menu.addAction(self._actions['preference']) if "read" in self._buttonDict or "write" in self._buttonDict or "reset" in self._buttonDict: self._job_start_menu.addSeparator() monitor_button = jobwidgets.JobStatusButton( parent=self._schroBottomButtonFrame, viewname=self.viewname) self._schroBottomButtonLayout.addWidget(monitor_button) monitor_button.setFixedHeight(BOTTOM_TOOLBUTTON_HEIGHT) monitor_button.setFixedWidth(BOTTOM_TOOLBUTTON_HEIGHT) self._buttons['monitor'] = monitor_button self._buttons['start'] = QtWidgets.QPushButton() self._buttons['start'].setText("Run") self._buttons['start'].setToolTip('Click to start the job') self._buttons['start'].setProperty("startButton", True) self._schroBottomButtonLayout.addWidget(self._buttons['start']) self._buttons['start'].clicked.connect(self._start) self._actions['start'] = QtGui.QAction( self._buttonDict['start']['text'], self) self._actions['start'].triggered.connect(self._start) # Read Button if 'read' in self._buttonDict: if "start" not in self._buttonDict: self._buttons['read'] = QtWidgets.QPushButton( self._buttonDict['read']['text']) self._schroBottomButtonLayout.addWidget(self._buttons['read']) self._buttons['read'].clicked.connect(self._read) else: self._actions['read'] = QtGui.QAction( self._buttonDict['read']['text'] + "...", self) self._actions['read'].triggered.connect(self._read) self._job_start_menu.addAction(self._actions['read']) # Write Button if 'write' in self._buttonDict: if "start" not in self._buttonDict: self._buttons['write'] = QtWidgets.QPushButton( self._buttonDict['write']['text']) self._schroBottomButtonLayout.addWidget(self._buttons['write']) self._buttons['write'].clicked.connect(self._write) else: self._actions['write'] = QtGui.QAction( self._buttonDict['write']['text'] + "...", self) self._actions['write'].triggered.connect(self._write) self._job_start_menu.addAction(self._actions['write']) # Reset Button if 'reset' in self._buttonDict: if "start" not in self._buttonDict: self._buttons['reset'] = QtWidgets.QPushButton( self._buttonDict['reset']['text']) self._schroBottomButtonLayout.addWidget(self._buttons['reset']) self._buttons['reset'].clicked.connect(self._reset) else: self._actions['reset'] = QtGui.QAction( self._buttonDict['reset']['text'], self) self._actions['reset'].triggered.connect(self._reset) self._job_start_menu.addAction(self._actions['reset']) self._buttons['reset'] = QtWidgets.QPushButton( self._buttonDict['reset']['text']) if not hasattr(self, "_jobname_le"): # Add a spacing object so that close and help are on the right. # In the case of panels with a job name line edit, the line # edit will eat the space self._schroBottomButtonLayout.addStretch() # If help button or desres about button is required, show the statusbar if 'help' in self._buttonDict or self.show_desres_icon: self.statusBar().show() # Help Button if 'help' in self._buttonDict: self._buttons['help'] = _ToolButton() self._buttons['help'].setIcon( QtGui.QIcon(":/images/toolbuttons/help.png")) height = HELP_BUTTON_HEIGHT self._buttons['help'].setFixedHeight(height) self._buttons['help'].setFixedWidth(height) self._buttons['help'].setIconSize(QtCore.QSize(height, height)) # Remove border around Help button. - MAE-29287 if sys.platform == "darwin": self._buttons['help'].setStyleSheet("QToolButton{border:0px;}") self.statusBar().addPermanentWidget(self._buttons['help']) self._buttons['help'].clicked.connect( self._buttonDict['help']['command']) # From EV:90626. Add F1 as a shortcut if this really is a help # button if self._buttonDict['help']['text'] == "Help": self._buttons['help'].setShortcut("F1") # We also need to add an About button with copyright information if self.show_desres_icon: b = make_desres_about_button(self) self.statusBar().addPermanentWidget(b) # To preserve legacy functionality, we allow you to specify buttons in # the constructor dictionary with the prefix "Custom". They will be # added in right to left order depending on sorted() order of names. # It is recommended that you use addButtonToBottomLayout for ease of # understanding code. Only by using addButtonToBottomLayout can you # access the buttons by name. custom_buttons = [] self.num_custom_buttons = 0 for button_name in self._buttonDict.keys(): if button_name.startswith("Custom"): custom_buttons.append(button_name) for button_name in sorted(custom_buttons): try: text = self._buttonDict[button_name]["text"] except KeyError: raise KeyError("No 'text' specified for button %s" % button_name) try: command = self._buttonDict[button_name]["command"] except KeyError: raise KeyError("No 'command' specified for button %s" % button_name) self.addButtonToBottomLayout(text, command)
[docs] def addButtonToBottomLayout(self, text, command): """ Adds a button to the bottom bar, to go to the right of Job Start buttons. Useful when you need a button on the bottom which is not standard job launching. Buttons are added from left to right in the order that this function is called.. :param text text that goes on the button :type text str :param command the slot that the button will run :param callable :rtype: str :return: name of button, to be used in setButtonEnabled """ button_name = "Custom_%s" % len(self._buttons) self._buttons[button_name] = QtWidgets.QPushButton(text) self._schroBottomButtonLayout.insertWidget(self.num_custom_buttons, self._buttons[button_name]) self.num_custom_buttons += 1 self._buttons[button_name].clicked.connect(command) return self._buttons[button_name]
[docs] def updateJobname(self): """ Update jobname in parameters from main window. """ jobname = self._jobname_le.text() # Set global dialog parameters in case we re-open ConfigDialog if hasattr(self.dialog_param, "start"): self.dialog_param.start["jobname"] = jobname if hasattr(self.dialog_param, "write"): self.dialog_param.write["jobname"] = jobname if self.sd_params: self.sd_params.jobname = jobname
def _applyStartDialogParams(self): """ Retrieves settings from the start dialog (aka config_dialog) and applies them to the job parameters of the app. """ # Instantiate the config dialog self._sd = self.config_dialog_class(self, **self.dialog_param.start) # If app has its own parameters, apply them to the config dialog if self.sd_params: self._sd.applySettings(self.sd_params) sd_params = self._sd.getSettings() # Get the config dialog settings # Make it possible to run APP.jobparam.commandLineOptions() self.jobparam.commandLineOptions = sd_params.commandLineOptions self.jobparam.commandLineArgs = sd_params.commandLineArgs self.jobparam.formJaguarCPUFlags = sd_params.formJaguarCPUFlags # Apply config dialog settings to JobParameters: self.jobparam.__dict__.update(sd_params.__dict__) def _start(self): """ Method for start button. Show dialog, process results, call command. """ jobid = None # Make sure that the input frame is OK before bringing up the dialog: if self._if: err_txt = self._if.validate() if err_txt: self.warning(err_txt) return # Run the pre-start command: # Application will use this to make sure that all fields are valid: if self._buttonDict['start']['precommand']: if self._buttonDict['start']['precommand'](): return ( # Return if check did not pass and function returned not # None. ) if self._buttonDict['start']['dialog']: # checkcommand will be run when the user pressed the Start button # (before closing the dialog): self.dialog_param.start["checkcommand"] = self._buttonDict['start'][ 'checkcommand'] jobname = str(self._jobname_le.text()) # Verify that the jobname entry is valid: if not fileutils.is_valid_jobname(jobname): msg = fileutils.INVALID_JOBNAME_ERR % jobname self.warning(msg) return if self.dialog_param.start.get("checkcommand"): if self.dialog_param.start["checkcommand"](jobname): # Non-zero value returned return else: filename = jobname + ".maegz" if os.path.isfile(filename): if not self.askOverwrite(): # User chose not to overwrite: return if self._buttonDict['start']['dialog']: self.updateJobname() self._applyStartDialogParams() if not self._sd.validate(): return if not self.jobparam.jobname: self.jobparam.jobname = jobname if self.setupJobParameters(): if self.jobparam.host == LOCALHOST_GPU: self.jobparam.host = LOCALHOST jobid = self._start_wrapper() self._updatePanelJobname() else: try: jobname = self.jobparam.jobname except: self.jobparam.jobname = jobname if self.setupJobParameters(): jobid = self._start_wrapper() start_wrapper_timeout = 3000 def _start_wrapper(self): """ This is a wrapper for the start command, where we disable the run button and show a status message. :returns: jobid (or None, if not yet implemented) """ self.statusBar().showMessage("Submitting Job...", self.start_wrapper_timeout) self._buttons['start'].setEnabled(False) self._buttons['settings'].setEnabled(False) try: rval = self._buttonDict['start']['command']() finally: QtCore.QTimer.singleShot( self.start_wrapper_timeout, lambda: self._buttons['start'].setEnabled(True)) QtCore.QTimer.singleShot( self.start_wrapper_timeout, lambda: self._buttons['settings'].setEnabled(True)) return rval def _read(self): """ Method for read button. Show dialog, process results, call command. """ # Run thea pre-read command: if self._buttonDict['read']['precommand']: if self._buttonDict['read']['precommand'](): return ( # Return if check did not pass and function returned not # None. ) if self._buttonDict['read']['dialog']: self._rd = ReadDialog(self, **self.dialog_param.read) readjob = self._rd.dialog() if not readjob: return # Cancel pressed self._buttonDict['read']['command']() return def _write(self): """ Method for write button. Show dialog, process results, call command. """ # Run the pre-write command: # Application will use this to make sure that all fields are valid: if self._buttonDict['write']['precommand']: if self._buttonDict['write']['precommand'](): return ( # Return if check did not pass and function returned not # None. ) # Make sure that the input frame is OK before bringing up the dialog: if self._if: err_msg = self._if.validate() if err_msg: self.warning(err_msg) return # Bring up the Write dialog: if self._buttonDict['write']['dialog']: # checkcommand will be run when the user pressed the Write button # (before closing the dialog): self.dialog_param.write["checkcommand"] = self._buttonDict['write'][ 'checkcommand'] # The default jobname for the write dialog is always the jobname # field of the main panel: if self._buttonDict['start']['dialog']: self.updateJobname() self._applyStartDialogParams() if not self._sd.validate(): return # Pass the "write" dictionary as options to WriteDialog: wd = WriteDialog(self, **self.dialog_param.write) jobname = wd.activate() if jobname: # Not cancel self._applyStartDialogParams() self.jobparam.jobname = jobname # Pressed Write button; fill out JobParameters: if self.setupJobParameters(): self._buttonDict['write']['command']() self._updatePanelJobname() else: self.jobparam.jobname = None if self.setupJobParameters(): self._buttonDict['write']['command']() return def _reset(self): """ Method for reset button. Reset file input frame, call command. """ if self._if: self._if._reset() # FFLD-560 Hide the progress bar: self.setProgress(0, 0) if 'start' in self._buttonDict: self._updatePanelJobname(True) self._buttonDict['reset']['command']()
[docs] def setButtonState(self, button, state): """ Set the state of the specified button, e.g., self.setButtonState('start', 'disabled') The standard state is 'normal'. Raises a RuntimeError if the button doesn't exist or if the requested state can't be set. Obsolete. Please use setButtonEnabled() method instead. """ # FIXME Deprecate this method in favor of setButtonEnabled() if button not in self._buttons: raise RuntimeError( "Can't set state of button '%s' because it does not exist." % button) if state == 'disabled': self._buttons[button].setEnabled(False) else: self._buttons[button].setEnabled(True)
[docs] def setButtonEnabled(self, button, enabled): """ Enable / disable the specified button, e.g., self.setButtonEnabled('start', False) Raises a RuntimeError if the button doesn't exist. """ if button not in self._buttons: raise RuntimeError( "Can't set state of button '%s' because it does not exist." % button) self._buttons[button].setEnabled(enabled)
# # SETUP JOBPARAM SETTINGS ### #
[docs] def setupJobParameters(self): """ Setups up the job parameters from the state of the input frame. Do not call directly from your script. Returns True if success, False on error (after displaying an error message). """ if self._if: ok = self._if.setup(self.jobparam.jobname) self.jobparam.__dict__.update(self._if.params.__dict__) if not ok: return False if len(self.setup) > 0: import warnings msg = "AppFramework.setup is deprecated. Custom job parameters can be processed in the start/write callback." warnings.warn(msg, DeprecationWarning, stacklevel=2) # call setup for all our registered widgets for w in self.setup: if not w.setup(self.jobparam): return False return True
[docs] def getInputSource(self): """ Return the selected input source. Available values (module constants): - WORKSPACE - SELECTED_ENTRIES - INCLUDED_ENTRIES - INCLUDED_ENTRY - FILE If the panel has no input frame, raises RuntimeError. """ try: source = self._if.inputState() except AttributeError: raise RuntimeError( "getInputSource() can't be used - no input frame") return source
[docs] def getInputFile(self): """ Return the selected input file path (Python string). If the panel has no input frame, raises RuntimeError. If FILE source is not allowed, raises RuntimeError. """ try: filename = self._if.getFile() except AttributeError: raise RuntimeError("getInputFile() can't be used - no input frame") if not hasattr(self._if, "file_text"): raise RuntimeError( "getInputFile() can't be used - file source is not allowed") return filename
[docs] def launchJobCmd(self, cmd): """ Launches job control command. Automatically tracked by the job status button (no need to call monitorJob method afterwards). NOTE: Unlike similar method in AF2, this method does not add -HOST etc options. """ try: with qt_utils.JobLaunchWaitCursorContext(): job = jobhandler.JobHandler(cmd, self.viewname).launchJob() except jobcontrol.JobLaunchFailure as err: qt_utils.show_job_launch_failure_dialog(err, self) raise return job
[docs] def monitorJob(self, jobid, showpanel=False): """ Monitor the given jobid and show the monitor panel; if in maestro. :param jobid: jobid of the job to monitor :param showpanel: whether to bring up the Monitor panel. By default, the panel will open if the "Open Monitor panel" preference is set. Example, after launching the job, use the jobid to issue the command:: <AppFramework_instance>.monitorJob(<jobid>) """ if maestro: maestro.job_started(jobid) if showpanel: maestro.command("showpanel monitor")
[docs] def processEvents(self): """ Allow the QApplication's or Maestro's main event loop to process pending events. """ if maestro: maestro.process_pending_events() else: self.getApp().processEvents()
[docs] @qt_utils.remove_wait_cursor def warning(self, text, preferences=None, key=None): """ Display a warning dialog with the specified text. If preferences and key are both supplied, then the dialog will contain a "Don't show this again" checkbox. Future invocations of this dialog with the same preferences and key values will obey the user's show preference. :type text: str :param text: The information to display in the dialog :param preferences: obsolete; ignored. :type key: str :param key: The key to store the preference under. If specified, a "Do not show again" checkbox will be rendered in the dialog box. :rtype: None """ messagebox.show_warning(parent=self, text=text, save_response_key=key)
[docs] @qt_utils.remove_wait_cursor def info(self, text, preferences=None, key=None): """ Display an information dialog with the specified text. If preferences and key are both supplied, then the dialog will contain a "Don't show this again" checkbox. Future invocations of this dialog with the same preferences and key values will obey the user's show preference. :type text: str :param text: The information to display in the dialog :param preferences: obsolete; ignored. :type key: str :param key: The key to store the preference under. If specified, a "Do not show again" checkbox will be rendered in the dialog box. :rtype: None """ messagebox.show_info(parent=self, text=text, save_response_key=key)
[docs] @qt_utils.remove_wait_cursor def error(self, text, preferences=None, key=None): """ Display an error dialog with the specified text. If preferences and key are both supplied, then the dialog will contain a "Don't show this again" checkbox. Future invocations of this dialog with the same preferences and key values will obey the user's show preference. :type text: str :param text: The information to display in the dialog :param preferences: obsolete; ignored. :type key: str :param key: The key to store the preference under. If specified, a "Do not show again" checkbox will be rendered in the dialog box. :rtype: None """ messagebox.show_error(parent=self, text=text, save_response_key=key)
[docs] def question(self, msg, button1="OK", button2="Cancel", title="Question"): """ Display a prompt dialog window with specified text. Returns True if first button (default OK) is pressed, False otherwise. """ return question(msg, button1, button2, parent=self, title=title)
[docs] def askOverwrite(self, files=None, parent=None): """ Display a dialog asking the user whether to overwrite existing files. Returns True if user chose to overwrite, False otherwise. Optionally specify a list of files that will be overwritten if the user presses the Overwrite button. """ # Ev:99336, Ev:99336 Overwrite without asking, if that's the # preference: if maestro: warn = maestro.get_command_option("prefer", "warnoverwritejobfiles") == "True" if not warn: return True msg = "Overwrite existing job files?" if files: msg += "\nThe following files will be overwritten:" for fname in files: msg += "\n " + fname if parent is None: parent = self return (question(msg, button1="Overwrite", button2="Cancel", parent=parent, title="Overwrite?"))
[docs] def askOverwriteIfNecessary(self, files): """ If any of the files in the <files> list exists, will bring up a dialog box asking the user whether they want to overwrite them. Returns True if the user chose to overwrite or if specified files do not exist. Returns False if the user cancelled. """ overwrite_files = [] for filename in files: if os.path.isfile(filename): overwrite_files.append(filename) if overwrite_files: return self.askOverwrite(overwrite_files) else: # No files to overwrite return True
def _settings(self): """ Open the config dialog. If settings are accepted (okay), returns the StartDialogParams, otherwise returns None. """ self.updateJobname() self._sd = self.config_dialog_class(self, **self.dialog_param.start) if self.sd_params: self._sd.applySettings(self.sd_params) try: self._sd.jobnameChanged.connect(self.setJobname) current_jobname = self._jobname_le.text() except AttributeError: # The jobnameChanged signal won't exist if the config dialog doesn't # have a job name line edit current_jobname = None params = self._sd.activate() if params: self.sd_params = params elif current_jobname is not None: self.setJobname(current_jobname) self.updateStatusBar() if params and self._sd.requested_action == RequestedAction.Run: self._start() return params def _jobprefersettings(self): """ Open the Maestro preference panel with Jobs/Starting node selected. """ if maestro: maestro.command("showpanel prefer:jobs_starting")
[docs] def updateStatusBar(self): """ Updates the status bar. """ if "start" not in self._buttonDict: return if not self.sd_params: self._sd = self.config_dialog_class(self, **self.dialog_param.start) self.sd_params = self._sd.getSettings() # This is true in IFD, where CPUs are specified on a per product basis if not self.sd_params.cpus: host = "Host={0}".format(self.sd_params.host) else: host = "Host={0}:{1}".format(self.sd_params.host, self.sd_params.cpus) if self._sd.options['incorporation'] and self.sd_params.disp: incorporate = ", Incorporate={0}".format( DISP_NAMES[self.sd_params.disp]) else: incorporate = "" text = "{0}{1}".format(host, incorporate) self.status_label.setText(text)
[docs] def setWaitCursor(self, app_wide=True): """ Set the cursor to the wait cursor. This will be an hourglass, clock or similar. Call restoreCursor() to return to the default cursor. If 'app_wide' is True then it will apply to the entire application (including Maestro if running there). If it's False then it will apply only to this panel. """ if app_wide: self.getApp().setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) else: self.setCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
[docs] def restoreCursor(self, app_wide=True): """ Restore the application level cursor to the default. If 'app_wide' is True then if will be restored for the entire application, if it's False, it will be just for this panel. """ if app_wide: self.getApp().restoreOverrideCursor() else: self.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
[docs] def getApp(self): """ Return QApplication instance which this panel is running under. If in Maestro, returns Maestro's global QApplication instance. """ return QtWidgets.QApplication.instance()
[docs] def closeEvent(self, event): """ Called by QApplication when the user clicked on the "x" button to close the window. Will call the user-specified close command (if specified). """ # self.quitting = True means we don't have to call the close button # function. EV 101808. if 'close' in self._buttonDict and not self.quitting: # Ev:90552 self._buttonDict['close']['command']() self.quitting = False
[docs] def closePanel(self): """ Hide panel, if in Maestro. Otherwise close it. """ if maestro or self.subwindow: self.hide() # Hide this QMainWindow else: self.close()
# Removing __del__ method since it is unnecessary and you should call # quitPanel to ensure that callbacks get unregistered since it is not # guaranteed that this method will be run # Also, there is a circular python __del__ dependency problem in PYTHON-2020, # so don't add this method without checking to see that this problem will not recur # def __del__(self):
[docs] def quitPanel(self): """ Quit the panel (even if in Maestro) Note that the calling script needs to unset the variable that holds this panel instance in order to truly delete the panel. For example, this method should be subclassed as follows: def quitPanel(self): global mypanel # Where mypanel is the variable holding this object appframework.AppFramework.quitPanel(self) mypanel = None """ if maestro or self.subwindow: # Flag to let closeEvent know that panel has already done what needs # to be done to quit. EV 101808. self.quitting = True self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) # print 'panel is being closed' if maestro: maestro.periodic_callback_remove(self._periodicCallback) # print ' unregistered periodic callback' self.close() # Close this QMainWindow only if self.isDockableInMaestro(): self._dock_widget.close() else: self.close()
[docs] def getOpenFileName( self, caption="Select a file", initial_dir=None, support_mae=True, support_sd=True, support_pdb=True, ): """ Brings up an open file dialog box for selecting structure files. By default reads Maestro, SD, and PDB formats. Returns file path that is readable by StructureReader. Is user pressed cancel, empty string is returned. """ import warnings warnings.warn( "schrodinger.ui.qt.appframework.AppFramework." "getOpenFileName() is deprecated", DeprecationWarning) filter_string = filter_string_from_supported(support_mae, support_sd, support_pdb) new_file = filedialog.get_open_file_name( self, caption, initial_dir, filter_string, ) # selected_string argument may be added after filter_string return new_file
[docs] def trackJobProgress(self, job): """ Display a progress dialog showing the progress of this job. (Any previously tracked job will no longer be tracked) job - a jobcontrol.Job object. """ self.current_job = job self.current_job_monitor = True # print "Job registered:", job self.setProgress(0, 1) self.setProgressError(False)
[docs] def setProgress(self, step, total_steps): """ Set the progress bar value (bottom of the panel) to <step> out of <total_steps> Set both to 0 to hide the progress bar. """ if total_steps == 0: if self._progress_bar.isVisible(): self._progress_bar_layout.removeWidget(self._progress_bar) self._progress_bar.hide() else: if not self._progress_bar.isVisible(): self._progress_bar_layout.addWidget(self._progress_bar) self._progress_bar.show() self._progress_bar.setMaximum(total_steps) self._progress_bar.setValue(step)
[docs] def setProgressError(self, error): """ Set the color of the progress bar to red (error=True) or normal color (error=False). """ self._progress_bar.setError(error)
def _onJobCompleted(self, job: jobcontrol.Job): if not self.current_job or not self.current_job_monitor: return if job.JobId != self.current_job.JobId: return if job.succeeded(): # Hide the progress bar: self.current_job = None self.current_job_monitor = False self.setProgress(0, 0) self.setProgressError(False) else: # Color the progress bar red: self.setProgressError(True) # Do NOT hide the progress bar, and still allow the # user to click on the progress bar to monitor the job. # Stop monitoring of this job: self.current_job_monitor = False self.jobCompleted.emit(job) def _onJobProgressChanged(self, job: jobcontrol.Job, current_step: int, total_steps: int, progress_msg: str): if not self.current_job or not self.current_job_monitor: return if job.JobId != self.current_job.JobId: return self.current_job = job if current_step == 0 and total_steps == 0: # Only description was specified # So that the progress bar still gets shown total_steps = 1 self.setProgress(current_step, total_steps) self.setProgressError(False) def _periodicCallback(self): if self._users_periodic_callback: self._users_periodic_callback()
[docs] def showEvent(self, show_event): """ Override the normal processing when the panel is shown. """ if self.isDockableInMaestro and self._dock_widget: self._dock_widget.raise_() QtWidgets.QWidget.showEvent(self, show_event)
[docs] def help(self): """ Display the help dialog (or a warning dialog if no help can be found). This function requires help_topic to have been given when the class was initialized. """ help_dialog(self._help_topic, parent=self)
def _updatePanelJobname(self, reset=False): """ Update the job name in the panel :param reset: If True, the new job name will be based on the default job name. Otherwise, it will be based on the current job name. :type reset: bool """ if reset: current_jobname = self.default_jobname else: current_jobname = self._jobname_le.text() new_jobname = jobnames.update_jobname(current_jobname, self.default_jobname) self.setJobname(new_jobname) def _populateEmptyJobname(self): """ If the user clears the job name line edit, populate it with the standard job name """ jobname = self._jobname_le.text() if not jobname: self._updatePanelJobname(True)
[docs] def setJobname(self, jobname): self._jobname_le.setText(jobname) self.jobparam.jobname = jobname self.updateJobname()
# # SCHRODINGER READ DIALOG ### #
[docs]class ReadDialog: """ Dialog allowing user to specify a file to read in. This dialog has some options which are covered in the DialogParameters class below. Any keyword arguments (e.g., from the DialogParameters for 'read') passed to this class are passed to the askopenfilename used as a file browser/selector. """
[docs] def __init__(self, parent, **kwargs): """ See class docstring. """ # Reference to AppFramework instance: self.parent = parent self.kwargs = kwargs
[docs] def dialog(self): """ Pop up a file browser. Returns the name of the file and also stores it in the parent JobParameters object as 'readfilename'. """ # Make sure we know our parent if "parent" not in self.kwargs: self.kwargs["parent"] = self.parent # Create input dialog using specified options. # If called with "Read..." button, DialogParameters are used. if self.kwargs['filetypes']: # User requested custom input file formats filter_string = format_list_to_filter_string( self.kwargs['filetypes']) # Ev:98084 directory must be absolute path: if 'initialdir' in self.kwargs: initialdir = os.path.abspath(self.kwargs['initialdir']) else: initialdir = '' fname = filedialog.get_open_file_name( self.parent, "Select File to Read", initialdir, filter_string, ) # selected_string argument may be added after filter_string else: # No custom file format requested fname = self.parent.getOpenFileName( caption="Select File to Read", initial_dir=self.kwargs['initialdir'], support_mae=self.kwargs['support_mae'], support_sd=self.kwargs['support_sd'], support_pdb=self.kwargs['support_pdb'], ) if fname: self.parent.jobparam.readfilename = fname return fname
# # SCHRODINGER WRITE DIALOG ### #
[docs]class WriteDialog: """ Toplevel Qt widget that mimics the Write dialog. The jobname is returned by activate() """
[docs] def __init__(self, parent, jobname="", title="", checkcommand=None, **kw): """ The 'jobname' will be the starting value of the job name field. The 'title' will be used as the title of the window. If pre_close_command is specified, it will be run when the user presses the Write button. The dialog is only closed if that function returns 0. """ if not title: title = parent.windowTitle() + ' - Write' self.jobname = jobname # Reference to AppFramework instance: self.parent = parent self.dialog = QtWidgets.QDialog(parent) self.dialog.setWindowTitle(title) self.pre_close_command = checkcommand # Ev:96019 Increase the width of the window so that the title is not # truncated: self.dialog.resize(300, 80) # width, height of the dialog # Create a main Vertical layout which will manage all # the components in the dialog self.main_layout = QtWidgets.QVBoxLayout(self.dialog) self.main_layout.setContentsMargins(3, 3, 3, 3) # FIXME: PYTHON-1795 Do we leave the job_name_ef here? self.job_name_ef = _EntryField(self.dialog, "Job name:", self.jobname) self.main_layout.addWidget(self.job_name_ef) self.button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Horizontal, self.dialog) self.write_button = QtWidgets.QPushButton("Write") self.cancel_button = QtWidgets.QPushButton("Cancel") self.button_box.addButton(self.write_button, QtWidgets.QDialogButtonBox.ActionRole) self.button_box.addButton(self.cancel_button, QtWidgets.QDialogButtonBox.RejectRole) self.main_layout.addStretch() # Add a stretchable section to the end self.main_layout.addWidget(self.button_box) self.write_button.clicked.connect(self.writePressed) self.cancel_button.clicked.connect(self.dialog.reject)
[docs] def writePressed(self): """ Called when the Write button is pressed. Closes the dialog only if the jobname is valid. """ # Ev:97626 jobname = self.job_name_ef.text() if not fileutils.is_valid_jobname(jobname): msg = fileutils.INVALID_JOBNAME_ERR % jobname self.warning(msg) return if self.pre_close_command: if self.pre_close_command(jobname): # Non-zero value returned return else: filename = jobname + ".maegz" if os.path.isfile(filename): if not self.parent.askOverwrite(parent=self.dialog): # User chose not to overwrite: return self.dialog.accept()
[docs] def warning(self, text): """ Display a warning window with the specified text. """ QtWidgets.QMessageBox.warning(self.dialog, "Warning", text)
[docs] def activate(self): """ Display the dialog and return user-selected job name. """ result = self.dialog.exec() # Cancelled : return None if result == QtWidgets.QDialog.Rejected: return None jobname = str(self.job_name_ef.text()) return jobname