Source code for schrodinger.ui.qt.appframework2.baseapp

import os
import sys

import psutil

import schrodinger
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 messagebox
from schrodinger.ui.qt import style
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.appframework2 import debug
from schrodinger.ui.qt.appframework2 import validation

maestro = schrodinger.get_maestro()

MODE_MAESTRO = 1
MODE_STANDALONE = 2
MODE_SUBPANEL = 3
MODE_CANVAS = 4

SETPANELOPTIONS = 0
SETUP = 1
SETDEFAULTS = 2
LAYOUT = 3

PANEL_HAS_NO_TITLE = 'PANEL HAS NO TITLE'

DEV_SYSTEM = 'SCHRODINGER_SRC' in os.environ


class DockWidget(maestro_ui.MM_QDockWidget):
    """
    This class propagates closeEvents to the App so that panel knows that it is
    closed. Otherwise maestro callbacks will not get removed.
    """

    def __init__(self, object_name, dockarea):
        super().__init__(object_name, dockarea, True, True)

    def closeEvent(self, event):
        """
        Do not close the floating python panel and instead emit a
        SIGNAL to Maestro which redocks the panel back into main window.
        This is a temporary workaround for Qt5 redock issues
        """
        if sys.platform.startswith("darwin") and self.isFloating():
            # If the panel is currently undocked, re-dock it instead of
            # closing it:
            event.ignore()
            self.pythonPanelAboutToBeClosed.emit(self)
        else:
            self.hide()
            self.widget().closeEvent(event)


Super = QtWidgets.QWidget


class BasePanel(QtWidgets.QWidget):
    # Emitted when the panel is closed. Expected by KNIME.
    gui_closed = QtCore.pyqtSignal()

    def __init__(self,
                 stop_before=None,
                 parent=None,
                 in_knime=False,
                 workspace_st_file=None):
        """
        :param stop_before: Exit the constructor before specified step.
        :type stop_before: int

        :param parent: Parent widget, if any.
        :type parent: QWidget

        :param in_knime:  Whether we are currently running under KNIME - a mode
            in which input selector is hidden, optionally a custom Workspace
            Structure is specified, and buttom bar has OK & Cancel buttons.
        :type in_knime: bool

        :param workspace_st_file: Structure to be returned by
            getWorkspaceStructure() when in_knime is True.
        :type workspace_st_file: bool
        """
        self.application = None
        self.run_mode = None
        try:
            object.__getattribute__(self, "in_knime")
        except AttributeError:
            self.in_knime = in_knime  # TODO: Convert to run_mode eventually
        # This assignment is done in the constructor, so that panels could
        # use getWorkspaceStructure() from setup() methods.
        self.workspace_st_file = workspace_st_file
        self.startUp()
        self.dock_widget = None
        super(BasePanel, self).__init__(parent=parent)

        style.apply_styles()

        if stop_before == SETPANELOPTIONS:
            return
        self.setPanelOptions()
        if stop_before == SETUP:
            return
        self.setup()
        if stop_before == SETDEFAULTS:
            return
        self.setDefaults()
        if stop_before == LAYOUT:
            return
        self.layOut()

    def startUp(self):
        if self.application:
            return  # Prevents startUp from being run twice
        procname = psutil.Process().name()
        if maestro:
            self.application = QtWidgets.QApplication.instance()
            self.run_mode = MODE_MAESTRO
        elif procname.startswith("canvas"):
            self.application = QtWidgets.QApplication.instance()
            self.run_mode = MODE_CANVAS
        else:
            self.application = QtWidgets.QApplication.instance()
            if not self.application:
                self.application = QtWidgets.QApplication([])
                self.run_mode = MODE_STANDALONE
            else:
                self.run_mode = MODE_SUBPANEL

    def setPanelOptions(self):
        self.maestro_dockable = False
        self.dock_area = Qt.RightDockWidgetArea
        self.title = PANEL_HAS_NO_TITLE
        self.ui = None
        self.allowed_run_modes = (MODE_MAESTRO, MODE_STANDALONE, MODE_SUBPANEL,
                                  MODE_CANVAS)

    def setup(self):
        # FIXME re-factor to remove duplication
        if self.ui:
            self.ui_widget = QtWidgets.QWidget(self)
            self.ui.setupUi(self.ui_widget)
        elif hasattr(self, 'setupUi'):
            self.ui_widget = QtWidgets.QWidget(self)
            self.ui_widget = QtWidgets.QWidget(self)
            self.setupUi(self.ui_widget)

    def setDefaults(self):
        pass

    def layOut(self):
        self.panel_layout = swidgets.SVBoxLayout(self)
        self.panel_layout.setContentsMargins(2, 2, 2, 2)
        if self.ui:
            self.panel_layout.addWidget(self.ui_widget)
        if self.maestro_dockable and maestro:
            # It is possible that currently allow docking preference is OFF,
            # but user can turn it ON later. So, always create docking widget.
            self._layOutDockWidget()

    def _layOutDockWidget(self):
        """
        Creates a dock widget, which will act as an outer container for this
        panel. This should only be called when the panel is meant to be
        dockable.

        The window title is also copied from the panel to the dock widget.
        """
        object_name = self.objectName() + '_dockwidget'
        self.dock_widget = DockWidget(object_name, self.dock_area)
        self.dock_widget.setObjectName(object_name)
        self.dock_widget.setWidget(self)
        self.dock_widget.setWindowTitle(self.windowTitle())
        self.dock_widget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
                                         QtCore.Qt.RightDockWidgetArea)
        if maestro:
            # This function checks docking location preference and sets
            # the dock widget accordingly.
            self.dock_widget.setupDockWidget.emit()
            hub = maestro_ui.MaestroHub.instance()
            hub.preferenceChanged.connect(self._dockPrefChanged)

    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 showEvent(self, show_event):
        """
        Override the normal processing when the panel is shown.
        """

        # Maestro crash when undocking and again redocking panel.
        # Fix for MAE-25846, MAE-36228
        if self.maestro_dockable and self.dock_widget:
            self.dock_widget.raise_()
        QtWidgets.QWidget.showEvent(self, show_event)

    if sys.platform.startswith("linux"):

        def setVisible(self, set_visible):
            qt_utils.linux_setVisible_positioning_workaround(
                self, super(), set_visible)

    def runMode(self):
        return self.run_mode

    def run(self):
        runmode = self.runMode()
        mode_allowed = True

        try:
            mode_allowed = runmode in self.allowed_run_modes
        except TypeError:
            mode_allowed = (runmode == self.allowed_run_modes)

        if runmode == MODE_STANDALONE:
            if not mode_allowed:
                self.error('This panel cannot be run outside of Maestro.')
                return
            return self.runStandalone()

        if runmode == MODE_SUBPANEL:
            if not mode_allowed:
                self.error('This panel cannot be run as a subpanel.')
                return
            return self.runSubpanel()

        if runmode == MODE_MAESTRO:
            if not mode_allowed:
                self.error('This panel cannot be run in Maestro.')
                return
            return self.runMaestro()

        if runmode == MODE_CANVAS:
            if not mode_allowed:
                self.error('This panel cannot be run in Canvas.')
                return
            return self.runCanvas()

    def cleanup(self):
        if self.maestro_dockable:
            self.dock_widget = None

    def closeEvent(self, event):
        """
        Close panel and quit application if this is a top level standalone
        window. Otherwise, hide the panel.
        """
        if self.runMode() in (MODE_STANDALONE, MODE_CANVAS):
            self.cleanup()
            Super.closeEvent(self, event)
        else:
            # When running as a sub-panel, only hide
            self.hide()

        self.gui_closed.emit()

    def runSubpanel(self):
        if not maestro or self.parent() is not maestro.get_main_window():
            if not self.dock_widget:
                self.setWindowFlags(self.windowFlags() | Qt.Window)
        if not self.isVisible():
            # QWidget.show can be very slow (2s) so call it only if panel
            # is currently not visible. This speeds up the call to panel()
            # method.
            self.show()
        if self.dock_widget:
            # only initiated when maestro_dockable is True and
            # preference allows docking
            self.dock_widget.show()
            self.dock_widget.raise_()
        self.raise_()
        self.activateWindow()

    def runMaestro(self):
        """
        This can be extended in derived classes to perform maestro-only tasks
        such as setting up the mini-monitor or connecting maestro callbacks
        """
        if not self.dock_widget:
            top = (maestro.get_command_option('prefer',
                                              'showpanelsontop') == 'True')
            if top:
                self.setParent(maestro.get_main_window())
                self.setWindowFlags(Qt.Tool)
            else:
                self.setParent(None)

        self.runSubpanel()

    def runCanvas(self):
        """
        This handles Canvas-specific logic
        """
        # if parentless, we need to clear this attribute
        if not self.parent():
            self.setAttribute(Qt.WA_QuitOnClose, False)
        self.runSubpanel()

    def runStandalone(self):
        self.show()
        self.application.setQuitOnLastWindowClosed(True)
        self.application.exec_()

    def show(self):
        super().show()
        self.setAttribute(QtCore.Qt.WA_Resized)

    def parent(self):
        try:
            parent = Super.parent(self)
        except RuntimeError:
            parent = None
        return parent

    def __str__(self):
        return '%s.%s' % (self.__module__, self.__class__.__name__)

    #=========================================================================
    # Properties
    #=========================================================================

    @property
    def title(self):
        return self.windowTitle()

    @title.setter
    def title(self, title):
        if self.dock_widget:
            self.dock_widget.setWindowTitle(title)
        return self.setWindowTitle(title)

    #=========================================================================
    # Copied from original appframework
    #=========================================================================

    @qt_utils.remove_wait_cursor
    def warning(self, text, preferences=None, key=""):
        """
        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=""):
        """
        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=""):
        """
        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)

    @qt_utils.remove_wait_cursor
    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 messagebox.show_question(
            parent=self,
            text=msg,
            yes_text=button1,
            no_text=button2,
            add_cancel_btn=False)

    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.

        :type app_wide: bool
        :param app_wide: If True then this will apply to the entire application
            (including Maestro if running there). If False then this will apply
            only to this panel.
        """

        if app_wide:
            self.application.setOverrideCursor(QtGui.QCursor(Qt.WaitCursor))
        else:
            self.setCursor(QtGui.QCursor(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.

        :type app_wide: bool
        :param app_wide: If True then this will restore the cursor for the
            entire application (including Maestro if running there). If False then
            this will apply only to this panel.
        """

        if app_wide:
            self.application.restoreOverrideCursor()
        else:
            self.setCursor(QtGui.QCursor(Qt.ArrowCursor))

    #===========================================================================
    # Debugging
    #===========================================================================
    def keyPressEvent(self, e):
        if not DEV_SYSTEM:
            return
        if e.key() == Qt.Key_F5:
            debug.start_gui_debug(self, globals(), locals())


class ValidatedPanel(BasePanel, validation.ValidationMixin):

    def __init__(self, *args, **kwargs):
        validation.ValidationMixin.__init__(self)
        super(ValidatedPanel, self).__init__(*args, **kwargs)