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
import time

# 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.Qt.QtCore import Qt
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
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.appframework2 import application
from schrodinger.ui.qt.appframework2 import jobnames
from schrodinger.ui.qt.config_dialog import DISP_APPEND
from schrodinger.ui.qt.config_dialog import DISP_APPENDINPLACE
from schrodinger.ui.qt.config_dialog import DISP_FLAG_FIT
from schrodinger.ui.qt.config_dialog import DISP_IGNORE
from schrodinger.ui.qt.config_dialog import DISP_NAMES
from schrodinger.ui.qt.config_dialog import DISP_REPLACE
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
from schrodinger.ui.qt.config_dialog import Host
from schrodinger.ui.qt.config_dialog import JobParameters
from schrodinger.ui.qt.config_dialog import RequestedAction
from schrodinger.ui.qt.config_dialog import StartDialog
from schrodinger.ui.qt.config_dialog import _EntryField
from schrodinger.ui.qt.config_dialog import get_hosts
# 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
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

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


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)


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


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


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).
    """

    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))

    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()

    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'


@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)


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)


class AppDockWidget(maestro_ui.MM_QDockWidget):
    """
    A DockWidget that calls the parent class's closeEvent when closing
    """

    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

    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 ###
#


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)

    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()

        # 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)

        jobhub.get_job_manager().jobCompleted.connect(self._onJobCompleted)
        jobhub.get_job_manager().jobDownloaded.connect(self._onJobDownloaded)
        jobhub.get_job_manager().jobProgressChanged.connect(
            self._onJobProgressChanged)

        self.updateStatusBar()

    def __str__(self):
        return "{0}.{1}".format(self.__module__, self.__class__.__name__)

    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()

    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

    def interior(self):
        """
        Return the interior frame where client widgets should be
        parented from
        """
        return self.interior_frame

    def addCentralWidget(self, w):
        """
        Add a widget to the central area of the AppFramework dialog
        """
        self.interior_layout.addWidget(w)

    def exec_(self):
        """
        Calls exec_() method of the application.
        """
        self.getApp().exec_()

    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).

    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'] = QtWidgets.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'] = QtWidgets.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'] = QtWidgets.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'] = QtWidgets.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'] = QtWidgets.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)

    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]

    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']()

    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)

    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 ###
    #
    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

    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

    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

    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

    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")

    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()

    @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)

    @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)

    @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)

    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)

    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?"))

    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")

    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)

    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))

    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))

    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()

    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

    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):

    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()

    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

    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)

    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)

    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

        if not maestro or not mmutil.feature_flag_is_enabled(mmutil.JOB_SERVER):
            # Outside of maestro and with classic jobcontrol, jobDownloaded is
            # not emitted, so emit jobCompleted here
            # FIXME PANEL-18802: with JOB_SERVER outside of maestro, jobs not
            # launched with jobhandler won't be downloaded
            self.jobCompleted.emit(job)

    def _onJobDownloaded(self, job: jobcontrol.Job):
        if not self.current_job or not self.current_job_monitor:
            return
        if job.JobId != self.current_job.JobId:
            return
        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()

    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)

    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)

    def setJobname(self, jobname):
        self._jobname_le.setText(jobname)
        self.jobparam.jobname = jobname
        self.updateJobname()


#
# SCHRODINGER READ DIALOG ###
#


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.

    """

    def __init__(self, parent, **kwargs):
        """
        See class docstring.
        """
        # Reference to AppFramework instance:
        self.parent = parent
        self.kwargs = kwargs

    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 ###
#
class WriteDialog:
    """
    Toplevel Qt  widget that mimics the Write dialog.

    The jobname is returned by activate()

    """

    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)

    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()

    def warning(self, text):
        """ Display a warning window with the specified text. """

        QtWidgets.QMessageBox.warning(self.dialog, "Warning", text)

    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