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

"""
<<<<< DEPRECATED >>>>>
This module should not be used for new code. Instead, consider using
`schrodinger.ui.qt.tasks`
<<<<< !!!!!!!!!  >>>>>
"""
import os

from schrodinger.infra import util
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import table_helper
from schrodinger.ui.qt import utils as ui_qt_utils
from schrodinger.ui.qt.appframework2 import baseapp
from schrodinger.ui.qt.appframework2 import debug
from schrodinger.ui.qt.appframework2 import tasks
from schrodinger.ui.qt.standard import constants

DEFAULT_START_LATENCY = 750
IMAGE_PATH = os.path.join(os.path.dirname(__file__), "images")
ICON_PATH_TEMPLATE = os.path.join(IMAGE_PATH, "{}.png")
SPIN_ICONS = None
ERROR_ICON = None


[docs]def get_spin_icons(): global SPIN_ICONS if SPIN_ICONS is None: SPIN_ICONS = [ QtGui.QIcon(ICON_PATH_TEMPLATE.format(num)) for num in range(0, 9) ] return SPIN_ICONS
[docs]def get_error_icon(): global ERROR_ICON ERROR_ICON = QtGui.QIcon(ICON_PATH_TEMPLATE.format('error')) return ERROR_ICON
#=============================================================================== # Base Classes #===============================================================================
[docs]class TaskUIMixin(object): """ This mixin provides the framework for making a user interface for a task runner. In MVC parlance, this mixin is used for creating a view/controller for a task runner model. """ start_latency = DEFAULT_START_LATENCY
[docs] def connectRunner(self, runner): """ Sets the task runner object for this UI and connects signals. If there is already a runner connected to this UI, it will first be disconnected before connecting the new runner. Passing in None will leave the UI not connected to anything. :param runner: the task runner to act as a model for this UI :type runner: tasks.AbstractTaskRunner """ if not hasattr(self, 'start_btn'): self.start_btn = QtWidgets.QPushButton('Dummy') if not hasattr(self, 'reset_btn'): self.reset_btn = QtWidgets.QPushButton('Dummy') self.disconnectRunner() if not runner: return self.runner = runner self.start_btn.clicked.connect(self.onStartPressed) self.reset_btn.clicked.connect(self.onResetPressed) self.runner.stateChanged.connect(self.onRunnerStateChanged) self.runner.startRequested.connect(self.onStartRequested) self.runner.startFailed.connect(self.onStartFailed) self.runner.taskStarted.connect(self.onTaskStarted) self.runner.taskEnded.connect(self.onTaskEnded) # Update after all subclass connection logic QtCore.QTimer.singleShot(0, self.onRunnerStateChanged)
[docs] def disconnectRunner(self): """ Disconnects the current runner from this UI. When subclassing, first perform any subclass-specific disconnection logic before calling the parent class' disconnectRunner(). If there is no runner connected, this method will do nothing. """ if not self.runner: return self.runner.stateChanged.disconnect(self.onRunnerStateChanged) self.runner.startRequested.disconnect(self.onStartRequested) self.runner.startFailed.disconnect(self.onStartFailed) self.runner.taskStarted.disconnect(self.onTaskStarted) self.runner.taskEnded.disconnect(self.onTaskEnded) self.runner = None # Update after all subclass disconnection logic QtCore.QTimer.singleShot(0, self.onRunnerStateChanged)
[docs] def onResetPressed(self): self.runner.reset()
[docs] def onStartPressed(self): self.runner.start()
[docs] def onStartRequested(self): self.start_btn.setEnabled(False)
[docs] def onStartFailed(self): self.start_btn.setEnabled(True)
[docs] def onTaskStarted(self, task): """ Start latency is the small delay after a task is started before another task can be started. """ if self.runner.allow_concurrent: QtCore.QTimer.singleShot(self.start_latency, self._restoreReady)
def _restoreReady(self): self.start_btn.setEnabled(True)
[docs] def onTaskEnded(self, task): """ This slot will only be called if the task was run within a single Maestro session. """ self.start_btn.setEnabled(True)
[docs] def onRunnerStateChanged(self): pass
[docs]class TaskUIWidget(TaskUIMixin, QtWidgets.QWidget): """ A general base class for task UI widgets. It creates the widget and provides the familiar af2-like setOptions, setup, and layOut methods, along with a main_layout for quickly adding child widgets. """
[docs] def __init__(self, runner=None): QtWidgets.QWidget.__init__(self) self.runner = None self.setOptions() self.setup() self.layOut() self.connectRunner(runner)
[docs] def setOptions(self): pass
[docs] def setup(self): self.main_layout = QtWidgets.QHBoxLayout()
[docs] def layOut(self): self.setLayout(self.main_layout) self.setContentsMargins(0, 0, 0, 0) self.main_layout.setSpacing(0)
#=============================================================================== # Task Name Line Edit #=============================================================================== skip_if_editing_name = util.skip_if('_editing_name')
[docs]class TaskNameLineEdit(TaskUIMixin, QtWidgets.QLineEdit): """ A line edit interface for task names (i.e. job names). This widget will automatically respond to changes in the job runner. It will also set itself read-only if the job runner does not allow custom job names. """ _editingName = util.flag_context_manager('_editing_name')
[docs] def __init__(self, runner=None): QtWidgets.QLineEdit.__init__(self) self._editing_name = False self.runner = None self.setContentsMargins(2, 2, 2, 2) self.textChanged.connect(self.onTextChanged) self.editingFinished.connect(self.onNameChanged) self.connectRunner(runner)
[docs] def setText(self, text): """ Overrides parent method so that programmatic modification of the text will trigger an update of the runner. """ old_text = self.text() QtWidgets.QLineEdit.setText(self, text) if old_text != text: # Prevents infinite loop of nameChanged signals self.onNameChanged()
[docs] def connectRunner(self, runner): TaskUIMixin.connectRunner(self, runner) if not runner: return self.setReadOnly(not runner.allow_custom_name) self.setText(self.runner.nextName()) self.runner.nameChanged.connect(self.onRunnerNameChanged)
[docs] def onTextChanged(self): """ We need to respond to the textChanged signal because if a user edits the name and directly clicks the start button, the editingFinished signal can come *after* the start button clicked signal, resulting in the new name not being assigned to the task that gets launched. Because textChanged is emitted while the user is editing the field, we don't want to process the empty-name case (which re-populates the field with the default job name). """ if self.text() == '': return # We don't want this widget to respond to this name change as it is # redundant and it will result in the cursor jumping to the end of the # line. with self._editingName(): self.onNameChanged()
[docs] def onNameChanged(self): self.runner.setCustomName(self.text())
[docs] @skip_if_editing_name def onRunnerNameChanged(self): self.setText(self.runner.nextName())
[docs] def onTaskStarted(self, task): TaskUIMixin.onTaskStarted(self, task) self.setText(self.runner.nextName())
#=============================================================================== # Spinner Widgets #===============================================================================
[docs]class SpinLabel(TaskUIMixin, QtWidgets.QLabel): """ This is a simple label that displays a spinner animation. Whenever a task from the connected task launcher is running, the spinner will be animated. It stops automatically when the last task ends. Other than connecting a runner, nothing generally needs to be done with this label. Note that QLabel uses a pixmap, not an icon, so the SpinnerIconMixin will not work here. """
[docs] def __init__(self, runner=None): QtWidgets.QLabel.__init__(self) self.runner = None self.height = 16 self.current_num = 0 self._loadImages() self.timer = QtCore.QTimer() self.timer.timeout.connect(self._advanceSpinner) self.connectRunner(runner)
def _loadImages(self): height = self.height icons = get_spin_icons() self.pics = [icon.pixmap(height, height) for icon in icons[1:]] self.stop_pic = icons[0].pixmap(height, height) def _advanceSpinner(self): pixmap = self.pics[self.current_num] self.current_num += 1 if self.current_num == len(self.pics): self.current_num = 0 self.setPixmap(pixmap)
[docs] def onRunnerStateChanged(self): if self.runner.isRunning(): self.startSpinner() else: self.stopSpinner()
[docs] def startSpinner(self): if not self.timer.isActive(): self.timer.start(250)
[docs] def stopSpinner(self): self.timer.stop() self.setPixmap(self.stop_pic)
[docs]class SpinnerIconMixin(object): """ Contains common code for widgets with spinners that use icons. """
[docs] def setupSpinner(self): icons = get_spin_icons() self.spin_icons = icons[1:] self.spin_idle_icon = icons[0] self.spin_error_icon = get_error_icon() self.spin_error_state = False self.spin_current_num = 0 self.spin_timer = QtCore.QTimer() self.spin_timer.timeout.connect(self._advanceSpinner)
def _advanceSpinner(self): if self.spin_error_state: self.setIcon(self.spin_error_icon) return icon = self.spin_icons[self.spin_current_num] self.spin_current_num += 1 if self.spin_current_num == len(self.spin_icons): self.spin_current_num = 0 self.setIcon(icon)
[docs] def startSpinner(self): if not self.spin_timer.isActive(): self.spin_timer.start(250)
[docs] def stopSpinner(self): self.spin_timer.stop() self.updateSpinIcon()
[docs] def updateSpinIcon(self): if self.spin_error_state: self.setIcon(self.spin_error_icon) else: self.setIcon(self.spin_idle_icon)
[docs] def setError(self, state=False): self.spin_error_state = state self.updateSpinIcon()
[docs]class SpinButton(TaskUIMixin, SpinnerIconMixin, QtWidgets.QPushButton): """ This is a push button which displays a spinner next to the button text. Clicking the button will automatically start the associated task runner, and the spinner will animate for as long as there is at least one task from that runner still running. Like the SpinLabel, once a runner is connected, this widget should just work. """
[docs] def __init__(self, text='', runner=None): QtWidgets.QPushButton.__init__(self, text) self.runner = None self.start_btn = self self.setupSpinner() self.connectRunner(runner)
[docs] def onRunnerStateChanged(self): if self.runner.isRunning(): self.startSpinner() else: self.stopSpinner()
[docs]class SpinToolButton(TaskUIMixin, SpinnerIconMixin, QtWidgets.QToolButton): """ This is like the SpinLabel except it is a tool button. Clicking the tool button does not automatically start the task runner, so the button behavior can be customized. """
[docs] def __init__(self, runner=None): QtWidgets.QToolButton.__init__(self) self.runner = None self.setupSpinner() self.connectRunner(runner)
[docs] def onRunnerStateChanged(self): if self.runner.isRunning(): self.startSpinner() else: self.stopSpinner()
#=============================================================================== # Task Bar #===============================================================================
[docs]class TaskBar(TaskUIWidget): """ A compound widget with a task name label, a spinner, and a run button. """
[docs] def __init__(self, runner=None, label_text='Task name:', button_text='Run', show_spinner=True, task_reset=True): """ :param runner: the runner to connect to this task bar :type runner: tasks.AbstractTaskRunner :param label_text: text label associated with the task name field :type label_text: str :param button_text: text on the "start" button :type button_text: str :param show_spinner: whether to show a progress spinner :type show_spinner: bool :param task_reset: whether to include a separate task reset action. Otherwise, reset will emit a global reset signal :type task_reset: bool """ self.label_text = label_text self.button_text = button_text self.show_spinner = show_spinner self.task_reset = task_reset TaskUIWidget.__init__(self, runner)
[docs] def setOptions(self): TaskUIWidget.setOptions(self) self.start_latency = DEFAULT_START_LATENCY
[docs] def setup(self): TaskUIWidget.setup(self) self.name_lbl = QtWidgets.QLabel(self.label_text) self.name_le = TaskNameLineEdit() self.spinner = SpinLabel() self.start_btn = ui_qt_utils.AcceptsFocusPushButton(self.button_text) self.start_timer = QtCore.QTimer() self.settings_btn = SettingsButton(task_reset=self.task_reset)
[docs] def layOut(self): TaskUIWidget.layOut(self) self.main_layout.addWidget(self.name_lbl) self.main_layout.addWidget(self.name_le) self.main_layout.addWidget(self.settings_btn) if self.show_spinner: self.main_layout.addWidget(self.spinner) self.main_layout.addWidget(self.start_btn)
[docs] def connectRunner(self, runner): TaskUIWidget.connectRunner(self, runner) self.spinner.connectRunner(runner) self.name_le.connectRunner(runner) self.settings_btn.connectRunner(runner)
[docs] def disconnectRunner(self): self.settings_btn.disconnectRunner() self.spinner.disconnectRunner() self.name_le.disconnectRunner() TaskUIWidget.disconnectRunner(self)
[docs] def onNameChanged(self): self.runner.setCustomName(self.name_le.text())
[docs] def onRunnerStateChanged(self): if not self.runner.allow_concurrent: self.start_btn.setEnabled(not self.runner.isRunning())
[docs]class MiniTaskBar(TaskBar): """ A narrow version of the TaskBar for narrow or dockable panels. No functional difference. """
[docs] def setup(self): TaskBar.setup(self) self.main_layout = QtWidgets.QVBoxLayout()
[docs] def layOut(self): TaskUIWidget.layOut(self) self.main_layout.addWidget(self.name_lbl) self.run_layout = QtWidgets.QHBoxLayout() self.main_layout.addLayout(self.run_layout) self.run_layout.addWidget(self.name_le) self.run_layout.addWidget(self.start_btn) self.line = QtWidgets.QFrame() self.line.setFrameShape(QtWidgets.QFrame.HLine) self.line.setFrameShadow(QtWidgets.QFrame.Sunken) self.main_layout.addWidget(self.line) self.bottom_layout = QtWidgets.QHBoxLayout() self.bottom_layout.addStretch() self.bottom_layout.addWidget(self.settings_btn) if self.show_spinner: self.bottom_layout.addWidget(self.spinner) self.main_layout.addLayout(self.bottom_layout) self.main_layout.setSpacing(3) self.bottom_layout.setSpacing(0) self.run_layout.setSpacing(0)
#=============================================================================== # Settings Button #===============================================================================
[docs]class SettingsButton(swidgets.SToolButton, TaskUIMixin): """ The "gear" button from the job bar. This button connects with a job runner and provides access to the config dialog, as well as write, write STU, and reset functionality. """
[docs] def __init__(self, runner=None, task_reset=False): """ :param runner: the runner to connect to this task bar :type runner: tasks.AbstractTaskRunner :param task_reset: whether to include a separate task reset action. Otherwise, reset will emit a global reset signal :type task_reset: bool """ QtWidgets.QToolButton.__init__(self) self.runner = None self.task_reset = task_reset self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) # The object name is used to style the button in misc/schrodinger.sty self.setObjectName("af2SettingsButton") self.setToolTip('Task options') icon_path = os.path.join(IMAGE_PATH, 'small_settings.png') self.setIcon(QtGui.QIcon(icon_path)) self.setFixedSize(constants.BOTTOM_TOOLBUTTON_HEIGHT, constants.BOTTOM_TOOLBUTTON_HEIGHT) self.clicked.connect(self.onClicked) self.menu = QtWidgets.QMenu() self.setMenu(self.menu) self.populateMenu() self.connectRunner(runner)
[docs] def populateMenu(self): if self.task_reset: self.menu.addAction('Reset this task', self.resetAction) self.menu.addAction('Reset entire panel', self.resetAllAction) else: self.menu.addAction('Reset', self.resetAllAction) if baseapp.DEV_SYSTEM: self.menu.addAction('Start debug...', self.startGuiDebug)
[docs] def startGuiDebug(self): debug.start_gui_debug(self.window())
[docs] def onClicked(self): pass
[docs] def resetAllAction(self): if not self.runner: return self.runner.reset() self.runner.resetAllRequested.emit()
[docs] def resetAction(self): if not self.runner: return self.runner.reset()
#=============================================================================== # Task table #===============================================================================
[docs]class TaskTableColumns(object): """ Columns object expected by table_helper """ HEADERS = ['Task Name', 'Status'] NUM_COLS = len(HEADERS) NAME, STATUS = list(range(NUM_COLS))
[docs]class TaskTableModel(TaskUIMixin, table_helper.RowBasedTableModel): """ The table model for representing multiple tasks and their current statuses """ COLUMN = TaskTableColumns ROW_CLASS = tasks.AbstractTaskWrapper
[docs] def __init__(self): table_helper.RowBasedTableModel.__init__(self) self.runner = None
[docs] def onRunnerStateChanged(self): # Whenever the runner state is changed, the entire table is reloaded. # This shouldn't be a problem as the table is generally small. TaskUIMixin.onRunnerStateChanged(self) self.loadData(self.runner.tasks())
@table_helper.data_method(QtCore.Qt.DisplayRole) def _data(self, col, task, role): if col == self.COLUMN.NAME: return task.getName() if col == self.COLUMN.STATUS: return task.status() # settings are disabled since we never need to serialize the Task Table and # it causes for certain types of tasks.
[docs] def af2SettingsGetValue(self): return
[docs] def af2SettingsSetValue(self, value): return
[docs]class TaskTableView(QtWidgets.QTableView): pass
[docs]class TaskTableWidget(TaskUIWidget): """ A widget containing both the table model and view objects. """ MODEL_CLASS = TaskTableModel VIEW_CLASS = TaskTableView
[docs] def setup(self): TaskUIWidget.setup(self) self.table_model = self.MODEL_CLASS() self.table_view = self.VIEW_CLASS() self.table_view.setModel(self.table_model)
[docs] def layOut(self): TaskUIWidget.layOut(self) self.main_layout.addWidget(self.table_view)
[docs] def connectRunner(self, runner): TaskUIWidget.connectRunner(self, runner) self.table_model.connectRunner(runner)
[docs] def disconnectRunner(self): self.table_model.disconnectRunner() return TaskUIWidget.disconnectRunner(self)