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
from schrodinger.utils import qapplication
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
Super = QtWidgets.QWidget
[docs]class BasePanel(QtWidgets.QWidget):
# Emitted when the panel is closed. Expected by KNIME.
gui_closed = QtCore.pyqtSignal()
[docs] 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()
style.apply_legacy_spacing_style(self)
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()
[docs] 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 = qapplication.get_application()
self.run_mode = MODE_STANDALONE
else:
self.run_mode = MODE_SUBPANEL
[docs] 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)
[docs] 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)
[docs] def setDefaults(self):
pass
[docs] def setStyleSheet(self, stylesheet):
# Override to ensure that style sheet changes after the first are
# actually applied
require_refresh = bool(self.styleSheet())
super().setStyleSheet(stylesheet)
if require_refresh:
qt_utils.update_widget_style(self)
[docs] 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)
[docs] 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"):
[docs] def setVisible(self, set_visible):
qt_utils.linux_setVisible_positioning_workaround(
self, super(), set_visible)
[docs] def runMode(self):
return self.run_mode
[docs] 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()
[docs] def cleanup(self):
if self.maestro_dockable:
self.dock_widget = None
[docs] 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()
[docs] 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()
[docs] 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()
[docs] 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()
[docs] def runStandalone(self):
self.show()
self.application.setQuitOnLastWindowClosed(True)
self.application.exec()
[docs] def show(self):
super().show()
self.setAttribute(QtCore.Qt.WA_Resized)
[docs] 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
#=========================================================================
[docs] @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)
[docs] @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)
[docs] @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)
[docs] @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)
[docs] def setWaitCursor(self, app_wide=True):
"""
Set the cursor to the wait cursor. This will be an hourglass, clock or
similar. Call restoreCursor() to return to the default cursor.
: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))
[docs] def restoreCursor(self, app_wide=True):
"""
Restore the application level cursor to the default. If 'app_wide' is
True then if will be restored for the entire application, if it's
False, it will be just for this panel.
: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
#===========================================================================
[docs] def keyPressEvent(self, e):
if not DEV_SYSTEM:
return
if e.key() == Qt.Key_F5:
debug.start_gui_debug(self, globals(), locals())
[docs]class ValidatedPanel(BasePanel, validation.ValidationMixin):
[docs] def __init__(self, *args, **kwargs):
validation.ValidationMixin.__init__(self)
super(ValidatedPanel, self).__init__(*args, **kwargs)