Source code for vdat.gui.queue

# -*- coding: utf-8 -*-
# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2015, 2016, 2017  "The HETDEX collaboration"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""Create a window representing a queue via a list of items.

The original implementation has been generated from reading ui file
'listWindow.ui'

Created: Mon Jun 15 16:25:52 2015
by: PyQt4 UI code generator 4.10.4
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import logging

from qtpy import QtCore, QtWidgets

from vdat.command_interpreter.core import CommandInterpreter
from vdat.command_interpreter.signals import get_signal
from vdat.command_interpreter.helpers import log_command_logger

_queue = []


[docs]class QueuedCommand(QtWidgets.QListWidgetItem): """Class describing the objects stored in the :class:`ModifyableListWidget`. Each object represent a command Parameters ---------- command : :class:`QCommandInterpreter` instance of the command interpreter to add to the queue label : string a label to appear for this command on the queue tool_tip : string, optional tool tip to show parent : :class:`PyQt5.QtWidgets.QWidget` instance the parent widget """ def __init__(self, command, label, tool_tip=None, parent=None): super(QueuedCommand, self).__init__(parent=parent) self.setText(label) if tool_tip: self.setToolTip(tool_tip) self.command = command
[docs]class ModifyableListWidget(QtWidgets.QListWidget): """List widget with the possibility to remove items """
[docs] def keyPressEvent(self, event): """Override the default method, removing the selected entry Parameters ---------- event : :class:`PyQt5.QtGui.QKeyEvent` object describing a key being pressed or released """ if event.key() == QtCore.Qt.Key_Delete: for item in self.selectedItems(): row = self.row(item) self.takeItem(row) else: super(ModifyableListWidget, self).keyPressEvent(event)
[docs]class Queue(QtWidgets.QMainWindow): """A queue that stores user commands and displays them in a GUI window. .. list-table:: **Custom signals** :header-rows: 1 * - Name - Signature - Description * - :attr:`closeSignal` - - emitted when the queue window is closed * - :attr:`job_added` - - emitted when a job is added to the queue * - :attr:`run_signal` - - emitted when a new command can be run * - :attr:`queue_empty_signal` - - emitted when the queue is empty * - :attr:`command_done` - bool - these five signals are a Qt re-implementation of the signals described in the :mod:`~vdat.command_interpreter.signals` * - :attr:`command_string` - int, str - * - :attr:`global_logger` - int, str - * - :attr:`n_primaries` - int - * - :attr:`progress` - int, int, int, int - .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`toggle` - bool - hide (False) or show (True) the panel * - :meth:`run` - - grab a command from the queue and run it * - :meth:`toggle_and_rerun` - - prepare to run a new command .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot/Signal * - :attr:`job_added` - :meth:`run` * - :attr:`run_signal` - :meth:`run` * - :attr:`QCommandInterpreter.command_done` - :attr:`command_done` * - :attr:`QCommandInterpreter.command_string` - :attr:`command_string` * - :attr:`QCommandInterpreter.global_logger` - :attr:`global_logger` * - :attr:`QCommandInterpreter.n_primaries` - :attr:`n_primaries` * - :attr:`QCommandInterpreter.progress` - :attr:`progress` * - :attr:`PyQt5.QtCore.QThread.started` - :meth:`QCommandInterpreter.run` * - :attr:`QCommandInterpreter.finished` - :meth:`toggle_and_rerun` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance the parent widget Attributes ---------- is_command_running : bool mark whether a command is running or not in a thread """ closeSignal = QtCore.Signal() job_added = QtCore.Signal() run_signal = QtCore.Signal() queue_empty_signal = QtCore.Signal() # the queue re-emit the signals from the VDATCommandWorker # This signals are connected to the worker ones and disconnected when the # worker is done command_done = QtCore.Signal(bool) command_string = QtCore.Signal(int, str) global_logger = QtCore.Signal(int, str) n_primaries = QtCore.Signal(int) progress = QtCore.Signal(int, int, int, int) _reconnect_names = ['command_done', 'command_string', 'global_logger', 'n_primaries', 'progress'] def __init__(self, parent=None): super(Queue, self).__init__(parent=parent) self.log = logging.getLogger('logger') self.itemList = [] self.is_command_running = False self.setupUi() self.job_added.connect(self.run) self.run_signal.connect(self.run) # save the thread and the worker into a list to remove them only # when new ones are needed. This hopefully avoids deadlocks self._threads_workers = []
[docs] def setupUi(self): """Setup the queue window """ self.setObjectName("vdatqueue") self.resize(300, 380) self.setWindowTitle("VDAT Command Queue") self.centralwidget = QtWidgets.QWidget(self) self.centralwidget.setObjectName("centralwidget") self.listWidget = ModifyableListWidget(self.centralwidget) self.listWidget.setGeometry(QtCore.QRect(5, 0, 290, 375)) self.listWidget.setObjectName("listwidget") self.setCentralWidget(self.centralwidget)
[docs] def closeEvent(self, event): """When the user closes the window, ignore the request, hide the window and emit the :attr:`closeSignal` signal. Parameters ---------- event : :class:`PyQt5.QtGui.QKeyEvent` object describing a key being pressed or released """ event.ignore() # Ignore the request self.closeSignal.emit() self.setVisible(False) # Just hide it instead
[docs] @QtCore.Slot(bool) def toggle(self, tggl): """Hide or show the panel. Alias of :meth:`setVisible`. Parameters ---------- toggle : bool whether the window is visible or not """ self.setVisible(tggl)
[docs] def add_command(self, command, label, tool_tip=None): """Add a command to the queue. Emit the :attr:`job_added` signal. Parameters ---------- command : :class:`QCommandInterpreter` instance of the command interpreter to add to the queue label : string A label to appear for this command on the queue tool_tip : string, optional tool tip to show """ item = QueuedCommand(command, label, tool_tip=tool_tip, parent=self.listWidget) self.listWidget.addItem(item) self.job_added.emit()
[docs] def get_command(self): """Get the top item from the queue. Returns ------- :class:`QCommandInterpreter` instance command to run, or None if the list is empty """ item = self.listWidget.takeItem(0) if isinstance(item, QueuedCommand): return item.command else: return None
[docs] def connect_worker_signals(self, worker): '''Connect the ``worker`` signals to the corresponding ones in this class Parameters ---------- worker : :class:`QCommandInterpreter` worker instance with the signals to connect with this object ones ''' for name in self._reconnect_names: getattr(worker, name).connect(getattr(self, name))
[docs] def remove_old_thread_worker(self): '''Quit the used thread and mark the it and worker for deletion''' for old_thread, old_worker in self._threads_workers: old_worker.deleteLater() old_thread.quit() old_thread.deleteLater() self._threads_workers = []
[docs] @QtCore.Slot() def run(self): """Grab a job from the queue and run it on a newly created :class:`~PyQt5.QtCore.QThread`. If a command is already running, and return. If the queue is empty notify the user and return. The :class:`~PyQt5.QtCore.QThread` and :class:`QCommandInterpreter` instances are saved in a local cache and removed the next time :meth:`run` is called. """ if self.is_command_running: self.log.info("Added command to queue") return command = self.get_command() if not command: self.log.info("All commands on queue completed!") self.queue_empty_signal.emit() else: self.is_command_running = True self.log.debug("Launching new command...") # Make a worker to run the function, move it to the background # thread. The worker cannot be a local variable otherwise it goes # out of scope as soon as we exit this function! thread = QtCore.QThread(self) self.connect_worker_signals(command) self._threads_workers.append([thread, command]) command.moveToThread(thread) # start to run the worker when starting the thread thread.started.connect(command.run) # Grab the next job, toggle is_command_running command.finished.connect(self.toggle_and_rerun) thread.start()
[docs] @QtCore.Slot() def toggle_and_rerun(self): """Remove the old thread and command, mark that the command is not running and emit the :attr:`run_signal` signal""" self.remove_old_thread_worker() self.is_command_running = False self.run_signal.emit()
# Mixing Qt classes with other classes can be tricky. See: # http://python.6.x6.nabble.com/Issue-with-multiple-inheritance-td5207771.html # http://pyqt.sourceforge.net/Docs/PyQt5/multiinheritance.html#support-for-cooperative-multi-inheritance # for some reason this does not affect PyQt4 (probably because needs to cope # with old style classes.
[docs]class QCommandInterpreter(CommandInterpreter, QtCore.QObject): '''Create a QObject from the :class:`~vdat.command_interpreter.core.CommandInterpreter`. Reimplement the :meth:`~vdat.command_interpreter.core.CommandInterpreter.make_signals` to use only the ``command_logger`` signals from the command_interpreter and use :class:`~PyQt5.QtCore.Signal` for the other signals .. list-table:: **Custom signals** :header-rows: 1 * - Name - Signature - Description * - :attr:`command_done` - bool - these five signals are a Qt re-implementation of the signals described in the :mod:`~vdat.command_interpreter.signals` * - :attr:`command_string` - int, str - * - :attr:`global_logger` - int, str - * - :attr:`n_primaries` - int - * - :attr:`progress` - int, int, int, int - * - :attr:`finished` - - emitted when the run is finished .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`run` - - Run the command Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance the parent widget args, kwargs: passed to the :class:`~vdat.command_interpreter.core.CommandInterpreter` ''' # signals from the original command interpreter command_done = QtCore.Signal(bool) command_string = QtCore.Signal(int, str) global_logger = QtCore.Signal(int, str) n_primaries = QtCore.Signal(int) progress = QtCore.Signal(int, int, int, int) # signal emitted when command_done(True) is emitted finished = QtCore.Signal() def __init__(self, *args, **kwargs): # Extract the ``parent`` from ``kwargs`` and initialize # :class:`~PyQt5.QtCore.QObject` and pass all the other arguments and # kwargs to the command interpreter parent = kwargs.pop('parent', None) CommandInterpreter.__init__(self, *args, **kwargs) QtCore.QObject.__init__(self, parent=parent) # self.finished.connect(self.disconnect_signals)
[docs] def make_signals(self): '''Use Signals instead of :mod:`vdat.command_interpreter.signals`, except for the command_logger one''' self.command_logger = get_signal('command_logger')
[docs] def connect_signals(self): self.command_logger.connect(log_command_logger)
[docs] def disconnect_signals(self): self.command_logger.disconnect(log_command_logger)
[docs] @QtCore.Slot() def run(self): '''Transform the method to a Slot''' self.connect_signals() super(QCommandInterpreter, self).run() self.disconnect_signals() self.finished.emit()
[docs]class QueuAction(QtWidgets.QAction): """Action for the queue window. Create the menu entry and binds signals known by the queue window to show/hide it .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`update_text` - bool - change the text to show in the action item .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot/Signal * - :attr:`toggled` - :meth:`update_text` * - :attr:`Queue.closeSignal` - :meth:`toggle` * - :attr:`toggled` - :meth:`Queue.toggle` Parameters ---------- *args, **kargs: arguments passed to the parent class """ def __init__(self, *args, **kwargs): super(QueuAction, self).__init__(*args, **kwargs) self.setText("Hide queue") self.setCheckable(True) self.toggle() self.toggled.connect(self.update_text) self.connect_with_queue()
[docs] def connect_with_queue(self): '''Connect with signals and slots in :class:`Queue`''' queue = get_queue() queue.closeSignal.connect(self.toggle) self.toggled.connect(queue.toggle)
[docs] @QtCore.Slot(bool) def update_text(self, toggled): '''Update the text in the action when the queue window is opened or closed Parameters ---------- toggled : bool whether is checked or not ''' if toggled: self.setText("Hide queue") else: self.setText("Show queue")
[docs]def set_queue(parent=None): """Create a :class:`Queue` instance and save it. You can access it with :func:`get_queue` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance the parent widget """ _queue.append(Queue(parent=parent))
[docs]def get_queue(): """Get the locally stored :class:`Queue` instance""" return _queue[0]