예제 #1
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """

    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"), self, objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(
            self.tr("Zoom out"), self, objectName="action-zoom-out",
            shortcut=QKeySequence.ZoomOut,
            triggered=self.zoomOut
        )
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"), self, objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0)
        )

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def wheelEvent(self, event: QWheelEvent):
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        else:
            super().wheelEvent(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale):
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            self.setTransform(transform)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(
        float, zoomLevel, setZoomLevel, notify=zoomLevelChanged
    )

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(
                vrect.size().toSize().boundedTo(QSize(200, 200))
            )
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
예제 #2
0
class WidgetManager(QObject):
    """
    OWWidget instance manager class.

    This class handles the lifetime of OWWidget instances in a
    :class:`WidgetsScheme`.

    """
    #: A new OWWidget was created and added by the manager.
    widget_for_node_added = Signal(SchemeNode, QWidget)

    #: An OWWidget was removed, hidden and will be deleted when appropriate.
    widget_for_node_removed = Signal(SchemeNode, QWidget)

    class ProcessingState(enum.IntEnum):
        """Widget processing state flags"""
        #: Signal manager is updating/setting the widget's inputs
        InputUpdate = 1
        #: Widget has entered a blocking state (OWWidget.isBlocking)
        BlockingUpdate = 2
        #: Widget has entered processing state
        ProcessingUpdate = 4
        #: Widget is still in the process of initialization
        Initializing = 8

    InputUpdate, BlockingUpdate, ProcessingUpdate, Initializing = ProcessingState

    #: Widget initialization states
    Delayed = namedtuple(
        "Delayed", ["node"])
    PartiallyInitialized = namedtuple(
        "Materializing",
        ["node", "partially_initialized_widget"])
    Materialized = namedtuple(
        "Materialized",
        ["node", "widget"])

    class CreationPolicy(enum.Enum):
        """Widget Creation Policy"""
        #: Widgets are scheduled to be created from the event loop, or when
        #: first accessed with `widget_for_node`
        Normal = "Normal"
        #: Widgets are created immediately when added to the workflow model
        Immediate = "Immediate"
        #: Widgets are created only when first accessed with `widget_for_node`
        OnDemand = "OnDemand"

    Normal, Immediate, OnDemand = CreationPolicy

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.__scheme = None
        self.__signal_manager = None
        self.__widgets = []
        self.__initstate_for_node = {}
        self.__creation_policy = WidgetManager.Normal
        #: a queue of all nodes whose widgets are scheduled for
        #: creation/initialization
        self.__init_queue = deque()  # type: Deque[SchemeNode]
        #: Timer for scheduling widget initialization
        self.__init_timer = QTimer(self, interval=0, singleShot=True)
        self.__init_timer.timeout.connect(self.__create_delayed)

        #: A mapping of SchemeNode -> OWWidget (note: a mapping is only added
        #: after the widget is actually created)
        self.__widget_for_node = {}
        #: a mapping of OWWidget -> SchemeNode
        self.__node_for_widget = {}

        # Widgets that were 'removed' from the scheme but were at
        # the time in an input update loop and could not be deleted
        # immediately
        self.__delay_delete = set()

        #: Deleted/removed during creation/initialization.
        self.__delete_after_create = []

        #: processing state flags for all widgets (including the ones
        #: in __delay_delete).
        #: Note: widgets which have not yet been created do not have an entry
        self.__widget_processing_state = {}

        # Tracks the widget in the update loop by the SignalManager
        self.__updating_widget = None

    def set_scheme(self, scheme):
        """
        Set the :class:`WidgetsScheme` instance to manage.
        """
        self.__scheme = scheme
        self.__signal_manager = scheme.findChild(SignalManager)

        self.__signal_manager.processingStarted[SchemeNode].connect(
            self.__on_processing_started
        )
        self.__signal_manager.processingFinished[SchemeNode].connect(
            self.__on_processing_finished
        )
        scheme.node_added.connect(self.add_widget_for_node)
        scheme.node_removed.connect(self.remove_widget_for_node)
        scheme.runtime_env_changed.connect(self.__on_env_changed)
        scheme.installEventFilter(self)

    def scheme(self):
        """
        Return the scheme instance on which this manager is installed.
        """
        return self.__scheme

    def signal_manager(self):
        """
        Return the signal manager in use on the :func:`scheme`.
        """
        return self.__signal_manager

    def widget_for_node(self, node):
        """
        Return the OWWidget instance for the scheme node.
        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Delayed):
            # Create the widget now if it is still pending
            state = self.__materialize(state)
            return state.widget
        elif isinstance(state, WidgetManager.PartiallyInitialized):
            widget = state.partially_initialized_widget
            log.warning("WidgetManager.widget_for_node: "
                        "Accessing a partially created widget instance. "
                        "This is most likely a result of explicit "
                        "QApplication.processEvents call from the '%s.%s' "
                        "widgets __init__.",
                        type(widget).__module__, type(widget).__name__)
            return widget
        elif isinstance(state, WidgetManager.Materialized):
            return state.widget
        else:
            assert False

    def node_for_widget(self, widget):
        """
        Return the SchemeNode instance for the OWWidget.

        Raise a KeyError if the widget does not map to a node in the scheme.
        """
        return self.__node_for_widget[widget]

    def widget_properties(self, node):
        """
        Return the current widget properties/settings.

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        settings : dict
        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Materialized):
            return state.widget.settingsHandler.pack_data(state.widget)
        else:
            return node.properties

    def set_creation_policy(self, policy):
        """
        Set the widget creation policy

        Parameters
        ----------
        policy : WidgetManager.CreationPolicy
        """
        if self.__creation_policy != policy:
            self.__creation_policy = policy

            if self.__creation_policy == WidgetManager.Immediate:
                self.__init_timer.stop()
                while self.__init_queue:
                    state = self.__init_queue.popleft()
                    self.__materialize(state)
            elif self.__creation_policy == WidgetManager.Normal:
                if not self.__init_timer.isActive() and self.__init_queue:
                    self.__init_timer.start()
            elif self.__creation_policy == WidgetManager.OnDemand:
                self.__init_timer.stop()
            else:
                assert False

    def creation_policy(self):
        """
        Return the current widget creation policy

        Returns
        -------
        policy: WidgetManager.CreationPolicy
        """
        return self.__creation_policy

    def add_widget_for_node(self, node):
        """
        Create a new OWWidget instance for the corresponding scheme node.
        """
        state = WidgetManager.Delayed(node)
        self.__initstate_for_node[node] = state

        if self.__creation_policy == WidgetManager.Immediate:
            self.__initstate_for_node[node] = self.__materialize(state)
        elif self.__creation_policy == WidgetManager.Normal:
            self.__init_queue.append(state)
            if not self.__init_timer.isActive():
                self.__init_timer.start()
        elif self.__creation_policy == WidgetManager.OnDemand:
            self.__init_queue.append(state)

    def __materialize(self, state):
        # Create and initialize an OWWidget for a Delayed
        # widget initialization
        assert isinstance(state, WidgetManager.Delayed)
        if state in self.__init_queue:
            self.__init_queue.remove(state)

        node = state.node

        widget = self.create_widget_instance(node)

        self.__widgets.append(widget)
        self.__widget_for_node[node] = widget
        self.__node_for_widget[widget] = node

        self.__initialize_widget_state(node, widget)

        state = WidgetManager.Materialized(node, widget)
        self.__initstate_for_node[node] = state
        self.widget_for_node_added.emit(node, widget)

        return state

    def remove_widget_for_node(self, node):
        """
        Remove the OWWidget instance for node.
        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Delayed):
            del self.__initstate_for_node[node]
            self.__init_queue.remove(state)
        elif isinstance(state, WidgetManager.Materialized):
            # Update the node's stored settings/properties dict before
            # removing the widget.
            # TODO: Update/sync whenever the widget settings change.
            node.properties = self._widget_settings(state.widget)
            self.__widgets.remove(state.widget)
            del self.__initstate_for_node[node]
            del self.__widget_for_node[node]
            del self.__node_for_widget[state.widget]
            node.title_changed.disconnect(state.widget.setCaption)
            state.widget.progressBarValueChanged.disconnect(node.set_progress)

            self.widget_for_node_removed.emit(node, state.widget)
            self._delete_widget(state.widget)
        elif isinstance(state, WidgetManager.PartiallyInitialized):
            widget = state.partially_initialized_widget
            raise RuntimeError(
                "A widget/node {} was removed while being initialized. "
                "This is most likely a result of an explicit "
                "QApplication.processEvents call from the '{}.{}' "
                "widgets __init__.\n"
                .format(state.node.title, type(widget).__module__,
                        type(widget).__init__))

    def _widget_settings(self, widget):
        return widget.settingsHandler.pack_data(widget)

    def _delete_widget(self, widget):
        """
        Delete the OWBaseWidget instance.
        """
        widget.close()

        # Save settings to user global settings.
        widget.saveSettings()

        # Notify the widget it will be deleted.
        widget.onDeleteWidget()

        if self.__widget_processing_state[widget] != 0:
            # If the widget is in an update loop and/or blocking we
            # delay the scheduled deletion until the widget is done.
            self.__delay_delete.add(widget)
        else:
            widget.deleteLater()
            del self.__widget_processing_state[widget]

    def create_widget_instance(self, node):
        """
        Create a OWWidget instance for the node.
        """
        desc = node.description
        klass = widget = None
        initialized = False
        error = None
        # First try to actually retrieve the class.
        try:
            klass = name_lookup(desc.qualified_name)
        except (ImportError, AttributeError):
            sys.excepthook(*sys.exc_info())
            error = "Could not import {0!r}\n\n{1}".format(
                node.description.qualified_name, traceback.format_exc()
            )
        except Exception:
            sys.excepthook(*sys.exc_info())
            error = "An unexpected error during import of {0!r}\n\n{1}".format(
                node.description.qualified_name, traceback.format_exc()
            )

        if klass is None:
            widget = mock_error_owwidget(node, error)
            initialized = True

        if widget is None:
            log.info("WidgetManager: Creating '%s.%s' instance '%s'.",
                     klass.__module__, klass.__name__, node.title)

            widget = klass.__new__(
                klass,
                None,
                captionTitle=node.title,
                signal_manager=self.signal_manager(),
                stored_settings=node.properties,
                # NOTE: env is a view of the real env and reflects
                # changes to the environment.
                env=self.scheme().runtime_env()
            )
            initialized = False

        # Init the node/widget mapping and state before calling __init__
        # Some OWWidgets might already send data in the constructor
        # (should this be forbidden? Raise a warning?) triggering the signal
        # manager which would request the widget => node mapping or state
        # Furthermore they can (though they REALLY REALLY REALLY should not)
        # explicitly call qApp.processEvents.
        assert node not in self.__widget_for_node
        self.__widget_for_node[node] = widget
        self.__node_for_widget[widget] = node
        self.__widget_processing_state[widget] = WidgetManager.Initializing
        self.__initstate_for_node[node] = \
            WidgetManager.PartiallyInitialized(node, widget)

        if not initialized:
            try:
                widget.__init__()
            except Exception:
                sys.excepthook(*sys.exc_info())
                msg = traceback.format_exc()
                msg = "Could not create {0!r}\n\n{1}".format(
                    node.description.name, msg
                )
                # remove state tracking for widget ...
                del self.__widget_for_node[node]
                del self.__node_for_widget[widget]
                del self.__widget_processing_state[widget]

                # ... and substitute it with a mock error widget.
                widget = mock_error_owwidget(node, msg)
                self.__widget_for_node[node] = widget
                self.__node_for_widget[widget] = node
                self.__widget_processing_state[widget] = 0
                self.__initstate_for_node[node] = \
                    WidgetManager.Materialized(node, widget)

        self.__initstate_for_node[node] = \
            WidgetManager.Materialized(node, widget)
        # Clear Initializing flag
        self.__widget_processing_state[widget] &= ~WidgetManager.Initializing

        node.title_changed.connect(widget.setCaption)

        # Widget's info/warning/error messages.
        widget.messageActivated.connect(self.__on_widget_state_changed)
        widget.messageDeactivated.connect(self.__on_widget_state_changed)

        # Widget's statusTip
        node.set_status_message(widget.statusMessage())
        widget.statusMessageChanged.connect(node.set_status_message)

        # Widget's progress bar value state.
        widget.progressBarValueChanged.connect(node.set_progress)

        # Widget processing state (progressBarInit/Finished)
        # and the blocking state.
        widget.processingStateChanged.connect(
            self.__on_processing_state_changed
        )
        widget.blockingStateChanged.connect(self.__on_blocking_state_changed)

        if widget.isBlocking():
            # A widget can already enter blocking state in __init__
            self.__widget_processing_state[widget] |= self.BlockingUpdate

        if widget.processingState != 0:
            # It can also start processing (initialization of resources, ...)
            self.__widget_processing_state[widget] |= self.ProcessingUpdate
            node.set_processing_state(1)
            node.set_progress(widget.progressBarValue)

        # Install a help shortcut on the widget
        help_shortcut = QShortcut(QKeySequence("F1"), widget)
        help_shortcut.activated.connect(self.__on_help_request)

        # Up shortcut (activate/open parent)
        up_shortcut = QShortcut(
            QKeySequence(Qt.ControlModifier + Qt.Key_Up), widget)
        up_shortcut.activated.connect(self.__on_activate_parent)

        # Call setters only after initialization.
        widget.setWindowIcon(
            icon_loader.from_description(desc).get(desc.icon)
        )
        widget.setCaption(node.title)

        # Schedule an update with the signal manager, due to the cleared
        # implicit Initializing flag
        self.signal_manager()._update()

        return widget

    def node_processing_state(self, node):
        """
        Return the processing state flags for the node.

        Same as `manager.widget_processing_state(manger.widget_for_node(node))`

        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Materialized):
            return self.__widget_processing_state[state.widget]
        elif isinstance(state, WidgetManager.PartiallyInitialized):
            return self.__widget_processing_state[state.partially_initialized_widget]
        else:
            return WidgetManager.Initializing

    def widget_processing_state(self, widget):
        """
        Return the processing state flags for the widget.

        The state is an bitwise or of `InputUpdate` and `BlockingUpdate`.

        """
        return self.__widget_processing_state[widget]

    def __create_delayed(self):
        if self.__init_queue:
            state = self.__init_queue.popleft()
            node = state.node
            self.__initstate_for_node[node] = self.__materialize(state)

        if self.__creation_policy == WidgetManager.Normal and \
                self.__init_queue:
            # restart the timer if pending widgets still in the queue
            self.__init_timer.start()

    def eventFilter(self, receiver, event):
        if event.type() == QEvent.Close and receiver is self.__scheme:
            self.signal_manager().stop()

            # Notify the widget instances.
            for widget in list(self.__widget_for_node.values()):
                widget.close()
                widget.saveSettings()
                widget.onDeleteWidget()

            event.accept()
            return True

        return QObject.eventFilter(self, receiver, event)

    def __on_help_request(self):
        """
        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to
        the scheme and hope someone responds to it.

        """
        # Sender is the QShortcut, and parent the OWBaseWidget
        widget = self.sender().parent()
        try:
            node = self.node_for_widget(widget)
        except KeyError:
            pass
        else:
            qualified_name = node.description.qualified_name
            help_url = "help://search?" + urlencode({"id": qualified_name})
            event = QWhatsThisClickedEvent(help_url)
            QCoreApplication.sendEvent(self.scheme(), event)

    def __on_activate_parent(self):
        """
        Activate parent shortcut was pressed.
        """
        event = ActivateParentEvent()
        QCoreApplication.sendEvent(self.scheme(), event)

    def __initialize_widget_state(self, node, widget):
        """
        Initialize the tracked info/warning/error message state.
        """
        for message_group in widget.message_groups:
            message = user_message_from_state(message_group)
            if message:
                node.set_state_message(message)

    def __on_widget_state_changed(self, msg):
        """
        The OWBaseWidget info/warning/error state has changed.
        """
        widget = msg.group.widget
        try:
            node = self.node_for_widget(widget)
        except KeyError:
            pass
        else:
            self.__initialize_widget_state(node, widget)

    def __on_processing_state_changed(self, state):
        """
        A widget processing state has changed (progressBarInit/Finished)
        """
        widget = self.sender()
        try:
            node = self.node_for_widget(widget)
        except KeyError:
            return

        if state:
            self.__widget_processing_state[widget] |= self.ProcessingUpdate
        else:
            self.__widget_processing_state[widget] &= ~self.ProcessingUpdate
        self.__update_node_processing_state(node)

    def __on_processing_started(self, node):
        """
        Signal manager entered the input update loop for the node.
        """
        widget = self.widget_for_node(node)
        # Remember the widget instance. The node and the node->widget mapping
        # can be removed between this and __on_processing_finished.
        self.__updating_widget = widget
        self.__widget_processing_state[widget] |= self.InputUpdate
        self.__update_node_processing_state(node)

    def __on_processing_finished(self, node):
        """
        Signal manager exited the input update loop for the node.
        """
        widget = self.__updating_widget
        self.__widget_processing_state[widget] &= ~self.InputUpdate

        if widget in self.__node_for_widget:
            self.__update_node_processing_state(node)
        elif widget in self.__delay_delete:
            self.__try_delete(widget)
        else:
            raise ValueError("%r is not managed" % widget)

        self.__updating_widget = None

    def __on_blocking_state_changed(self, state):
        """
        OWWidget blocking state has changed.
        """
        if not state:
            # schedule an update pass.
            self.signal_manager()._update()

        widget = self.sender()
        if state:
            self.__widget_processing_state[widget] |= self.BlockingUpdate
        else:
            self.__widget_processing_state[widget] &= ~self.BlockingUpdate

        if widget in self.__node_for_widget:
            node = self.node_for_widget(widget)
            self.__update_node_processing_state(node)

        elif widget in self.__delay_delete:
            self.__try_delete(widget)

    def __update_node_processing_state(self, node):
        """
        Update the `node.processing_state` to reflect the widget state.
        """
        state = self.node_processing_state(node)
        node.set_processing_state(1 if state else 0)

    def __try_delete(self, widget):
        if self.__widget_processing_state[widget] == 0:
            self.__delay_delete.remove(widget)
            widget.deleteLater()
            del self.__widget_processing_state[widget]

    def __on_env_changed(self, key, newvalue, oldvalue):
        # Notify widgets of a runtime environment change
        for widget in self.__widget_for_node.values():
            widget.workflowEnvChanged(key, newvalue, oldvalue)
예제 #3
0
class SignalManager(QObject):
    """
    SignalManager handles the runtime signal propagation for a :class:`.Scheme`
    instance.

    Note
    ----
    If a scheme instance is passed as a parent to the constructor it is also
    set as the workflow model.
    """
    class State(enum.IntEnum):
        """
        SignalManager state flags.

        .. seealso:: :func:`SignalManager.state()`
        """
        #: The manager is running, i.e. it propagates signals
        Running = 0
        #: The manager is stopped. It does not track node output changes,
        #: and does not deliver signals to dependent nodes
        Stopped = 1
        #: The manager is paused. It still tracks node output changes, but
        #: does not deliver new signals to dependent nodes. The pending signals
        #: will be delivered once it enters Running state again
        Paused = 2

    #: The manager is running, i.e. it propagates signals
    Running = State.Running
    #: The manager is stopped. It does not track node ouput changes,
    #: and does not deliver signals to dependent nodes
    Stopped = State.Stopped
    #: The manager is paused. It still tracks node output changes, but
    #: does not deliver new signals to dependent nodes. The pending signals
    #: will be delivered once it enters Running state again
    Paused = State.Paused

    # unused; back-compatibility
    Error = 3

    class RuntimeState(enum.IntEnum):
        """
        SignalManager runtime state.

        See Also
        --------
        SignalManager.runtime_state
        """
        #: Waiting, idle state. The signal queue is empty
        Waiting = 0
        #: ...
        Processing = 1

    Waiting = RuntimeState.Waiting
    Processing = RuntimeState.Processing

    #: Emitted when the state of the signal manager changes.
    stateChanged = pyqtSignal(int)
    #: Emitted when signals are added to the queue.
    updatesPending = pyqtSignal()
    #: Emitted right before a `SchemeNode` instance has its inputs updated.
    processingStarted = pyqtSignal([], [SchemeNode])
    #: Emitted right after a `SchemeNode` instance has had its inputs updated.
    processingFinished = pyqtSignal([], [SchemeNode])
    #: Emitted when `SignalManager`'s runtime state changes.
    runtimeStateChanged = pyqtSignal(int)

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.__workflow = None  # type: Optional[Scheme]
        self.__input_queue = []  # type: List[Signal]

        # mapping a node to its current outputs
        self.__node_outputs = {
        }  # type: Dict[SchemeNode, Dict[OutputSignal, Dict[Any, Any]]]

        self.__state = SignalManager.Running
        self.__runtime_state = SignalManager.Waiting

        self.__update_timer = QTimer(self, interval=100, singleShot=True)
        self.__update_timer.timeout.connect(self.__process_next)

        if isinstance(parent, Scheme):
            self.set_workflow(parent)

    def _can_process(self):  # type: () -> bool
        """
        Return a bool indicating if the manger can enter the main
        processing loop.

        """
        return self.__state not in [SignalManager.Error, SignalManager.Stopped]

    def workflow(self):
        # type: () -> Optional[Scheme]
        """
        Return the :class:`Scheme` instance.
        """
        return self.__workflow

    #: Alias
    scheme = workflow

    def set_workflow(self, workflow):
        # type: (Scheme) -> None
        """
        Set the workflow model.

        Parameters
        ----------
        workflow : Scheme
        """
        if workflow is self.__workflow:
            return

        if self.__workflow is not None:
            for link in self.__workflow.links:
                link.enabled_changed.disconnect(self.__on_link_enabled_changed)

            self.__workflow.node_added.disconnect(self.__on_node_added)
            self.__workflow.node_removed.disconnect(self.__on_node_removed)
            self.__workflow.link_added.disconnect(self.__on_link_added)
            self.__workflow.link_removed.disconnect(self.__on_link_removed)
            self.__workflow.removeEventFilter(self)
            self.__node_outputs = {}
            self.__input_queue = []

        self.__workflow = workflow

        if workflow is not None:
            workflow.node_added.connect(self.__on_node_added)
            workflow.node_removed.connect(self.__on_node_removed)
            workflow.link_added.connect(self.__on_link_added)
            workflow.link_removed.connect(self.__on_link_removed)
            for node in workflow.nodes:
                self.__node_outputs[node] = defaultdict(dict)
            for link in workflow.links:
                link.enabled_changed.connect(self.__on_link_enabled_changed)
            workflow.installEventFilter(self)

    def has_pending(self):  # type: () -> bool
        """
        Does the manager have any signals to deliver?
        """
        return bool(self.__input_queue)

    def start(self):  # type: () -> None
        """
        Start the update loop.

        Note
        ----
        The updates will not happen until the control reaches the Qt event
        loop.
        """
        if self.__state != SignalManager.Running:
            self.__state = SignalManager.Running
            self.stateChanged.emit(SignalManager.Running)
            self._update()

    def stop(self):  # type: () -> None
        """
        Stop the update loop.

        Note
        ----
        If the `SignalManager` is currently in `process_queues` it will
        still update all current pending signals, but will not re-enter
        until `start()` is called again.
        """
        if self.__state != SignalManager.Stopped:
            self.__state = SignalManager.Stopped
            self.stateChanged.emit(SignalManager.Stopped)
            self.__update_timer.stop()

    def pause(self):  # type: () -> None
        """
        Pause the delivery of signals.
        """
        if self.__state != SignalManager.Paused:
            self.__state = SignalManager.Paused
            self.stateChanged.emit(SignalManager.Paused)
            self.__update_timer.stop()

    def resume(self):
        # type: () -> None
        """
        Resume the delivery of signals.
        """
        if self.__state == SignalManager.Paused:
            self.__state = SignalManager.Running
            self.stateChanged.emit(self.__state)
            self._update()

    def step(self):
        # type: () -> None
        """
        Deliver signals to a single node (only applicable while the `state()`
        is `Paused`).
        """
        if self.__state == SignalManager.Paused:
            self.process_queued()

    def state(self):
        # type: () -> State
        """
        Return the current state.

        Return
        ------
        state : SignalManager.State
        """
        return self.__state

    def _set_runtime_state(self, state):
        # type: (RuntimeState) -> None
        """
        Set the runtime state.

        Should only be called by `SignalManager` implementations.
        """
        if self.__runtime_state != state:
            self.__runtime_state = state
            self.runtimeStateChanged.emit(self.__runtime_state)

    def runtime_state(self):
        # type: () -> RuntimeState
        """
        Return the runtime state. This can be `SignalManager.Waiting`
        or `SignalManager.Processing`.

        """
        return self.__runtime_state

    def __on_node_removed(self, node):
        # remove all pending input signals for node so we don't get
        # stale references in process_node.
        # NOTE: This does not remove output signals for this node. In
        # particular the final 'None' will be delivered to the sink
        # nodes even after the source node is no longer in the scheme.
        log.info("Removing pending signals for '%s'.", node.title)
        self.remove_pending_signals(node)

        del self.__node_outputs[node]

    def __on_node_added(self, node):
        self.__node_outputs[node] = defaultdict(dict)

    def __on_link_added(self, link):
        # push all current source values to the sink
        link.set_runtime_state(SchemeLink.Empty)
        if link.enabled:
            log.info("Scheduling signal data update for '%s'.", link)
            self._schedule(self.signals_on_link(link))
            self._update()

        link.enabled_changed.connect(self.__on_link_enabled_changed)

    def __on_link_removed(self, link):
        # purge all values in sink's queue
        log.info("Scheduling signal data purge (%s).", link)
        self.purge_link(link)
        link.enabled_changed.disconnect(self.__on_link_enabled_changed)

    def __on_link_enabled_changed(self, enabled):
        if enabled:
            link = self.sender()
            log.info("Link %s enabled. Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))

    def signals_on_link(self, link):
        # type: (SchemeLink) -> List[Signal]
        """
        Return :class:`Signal` instances representing the current values
        present on the `link`.
        """
        items = self.link_contents(link)
        signals = []

        for key, value in items.items():
            signals.append(Signal(link, value, key))

        return signals

    def link_contents(self, link):
        # type: (SchemeLink) -> Dict[Any, Any]
        """
        Return the contents on the `link`.
        """
        node, channel = link.source_node, link.source_channel

        if node in self.__node_outputs:
            return self.__node_outputs[node][channel]
        else:
            # if the the node was already removed its tracked outputs in
            # __node_outputs are cleared, however the final 'None' signal
            # deliveries for the link are left in the _input_queue.
            pending = [sig for sig in self.__input_queue if sig.link is link]
            return {sig.id: sig.value for sig in pending}

    def send(self, node, channel, value, id):
        # type: (SchemeNode, OutputSignal, Any, Any) -> None
        """
        Send the `value` with `id` on an output `channel` from node.

        Schedule the signal delivery to all dependent nodes

        Parameters
        ----------
        node : SchemeNode
            The originating node.
        channel : OutputSignal
            The nodes output on which the value is sent.
        value : Any
            The value to send,
        id : Any
            Signal id.
        """
        if self.__workflow is None:
            raise RuntimeError("'send' called with no workflow!.")

        log.debug("%r sending %r (id: %r) on channel %r", node.title,
                  type(value), id, channel.name)

        scheme = self.__workflow

        self.__node_outputs[node][channel][id] = value

        links = filter(
            is_enabled,
            scheme.find_links(source_node=node, source_channel=channel))
        signals = []
        for link in links:
            signals.append(Signal(link, value, id))

        self._schedule(signals)

    def purge_link(self, link):
        # type: (SchemeLink) -> None
        """
        Purge the link (send None for all ids currently present)
        """
        contents = self.link_contents(link)
        ids = contents.keys()
        signals = [Signal(link, None, id) for id in ids]

        self._schedule(signals)

    def _schedule(self, signals):
        # type: (List[Signal]) -> None
        """
        Schedule a list of :class:`Signal` for delivery.
        """
        self.__input_queue.extend(signals)

        for link in {sig.link for sig in signals}:
            # update the SchemeLink's runtime state flags
            contents = self.link_contents(link)
            if any(value is not None for value in contents.values()):
                state = SchemeLink.Active
            else:
                state = SchemeLink.Empty
            link.set_runtime_state(state | SchemeLink.Pending)

        if signals:
            self.updatesPending.emit()

        self._update()

    def _update_link(self, link):
        # type: (SchemeLink) -> None
        """
        Schedule update of a single link.
        """
        signals = self.signals_on_link(link)
        self._schedule(signals)

    def process_queued(self, max_nodes=None):
        # type: (Any) -> None
        """
        Process queued signals.

        Take one node node from the pending input queue and deliver
        all scheduled signals.
        """
        if not (max_nodes is None or max_nodes == 1):
            warnings.warn(
                "`max_nodes` is deprecated and will be removed in the future",
                FutureWarning,
                stacklevel=2)

        if self.__runtime_state == SignalManager.Processing:
            raise RuntimeError("Cannot re-enter 'process_queued'")

        if not self._can_process():
            raise RuntimeError("Can't process in state %i" % self.__state)

        log.info("SignalManager: Processing queued signals")

        node_update_front = self.node_update_front()
        log.debug("SignalManager: Nodes eligible for update %s",
                  [node.title for node in node_update_front])

        if node_update_front:
            self.process_node(node_update_front[0])

    def process_node(self, node):
        # type: (SchemeNode) -> None
        """
        Process pending input signals for `node`.
        """
        assert self.__runtime_state != SignalManager.Processing

        signals_in = self.pending_input_signals(node)
        self.remove_pending_signals(node)

        signals_in = self.compress_signals(signals_in)

        log.debug("Processing %r, sending %i signals.", node.title,
                  len(signals_in))
        # Clear the link's pending flag.
        for link in {sig.link for sig in signals_in}:
            link.set_runtime_state(link.runtime_state() & ~SchemeLink.Pending)

        def process_dynamic(signals):
            # type: (List[Signal]) -> List[Signal]
            """
            Process dynamic signals; Update the link's dynamic_enabled flag if
            the value is valid; replace values that do not type check with
            `None`
            """
            res = []
            for sig in signals:
                # Check and update the dynamic link state
                link = sig.link
                if sig.link.is_dynamic():
                    enabled = can_enable_dynamic(link, sig.value)
                    link.set_dynamic_enabled(enabled)
                    if not enabled:
                        # Send None instead (clear the link)
                        sig = Signal(link, None, sig.id)
                res.append(sig)
            return res

        signals_in = process_dynamic(signals_in)
        assert ({sig.link
                 for sig in self.__input_queue
                 }.intersection({sig.link
                                 for sig in signals_in}) == set([]))

        self._set_runtime_state(SignalManager.Processing)
        self.processingStarted.emit()
        self.processingStarted[SchemeNode].emit(node)
        try:
            self.send_to_node(node, signals_in)
        finally:
            self.processingFinished.emit()
            self.processingFinished[SchemeNode].emit(node)
            self._set_runtime_state(SignalManager.Waiting)

    def compress_signals(self, signals):
        # type: (List[Signal]) -> List[Signal]
        """
        Compress a list of :class:`Signal` instances to be delivered.

        Before the signal values are delivered to the sink node they can be
        optionally `compressed`, i.e. values can be merged or dropped
        depending on the execution semantics.

        The input list is in the order that the signals were enqueued.

        The base implementation returns the list unmodified.

        Parameters
        ----------
        signals : List[Signal]

        Return
        ------
        signals : List[Signal]
        """
        return signals

    def send_to_node(self, node, signals):
        # type: (SchemeNode, List[Signal]) -> None
        """
        Abstract. Reimplement in subclass.

        Send/notify the `node` instance (or whatever object/instance it is a
        representation of) that it has new inputs as represented by the
        `signals` list).

        Parameters
        ----------
        node : SchemeNode
        signals : List[Signal]
        """
        raise NotImplementedError

    def is_pending(self, node):
        # type: (SchemeNode) -> bool
        """
        Is `node` (class:`SchemeNode`) scheduled for processing (i.e.
        it has incoming pending signals).

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        pending : bool
        """
        return node in [signal.link.sink_node for signal in self.__input_queue]

    def pending_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of pending nodes.

        The nodes are returned in the order they were enqueued for
        signal delivery.

        Returns
        -------
        nodes : List[SchemeNode]
        """
        return list(unique(sig.link.sink_node for sig in self.__input_queue))

    def pending_input_signals(self, node):
        # type: (SchemeNode) -> List[Signal]
        """
        Return a list of pending input signals for node.
        """
        return [
            signal for signal in self.__input_queue
            if node is signal.link.sink_node
        ]

    def remove_pending_signals(self, node):
        # type: (SchemeNode) -> None
        """
        Remove pending signals for `node`.
        """
        for signal in self.pending_input_signals(node):
            try:
                self.__input_queue.remove(signal)
            except ValueError:
                pass

    def blocking_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of nodes in a blocking state.
        """
        workflow = self.__workflow
        if workflow is None:
            return []
        else:
            return [node for node in workflow.nodes if self.is_blocking(node)]

    def is_blocking(self, node):
        # type: (SchemeNode) -> bool
        """
        Is the node in `blocking` state.

        Is it currently in a state where will produce new outputs and
        therefore no signals should be delivered to dependent nodes until
        it does so.

        The default implementation returns False.
        """
        # TODO: this needs a different name
        return False

    def node_update_front(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of nodes on the update front, i.e. nodes scheduled for
        an update that have no ancestor which is either itself scheduled
        for update or is in a blocking state).

        Note
        ----
        The node's ancestors are only computed over enabled links.
        """
        scheme = self.__workflow
        if scheme is None:
            return []

        def expand(node):
            return [
                link.sink_node for link in scheme.find_links(source_node=node)
                if link.enabled
            ]

        components = strongly_connected_components(scheme.nodes, expand)
        node_scc = {node: scc for scc in components for node in scc}

        def isincycle(node):
            # type: (SchemeNode) -> bool
            return len(node_scc[node]) > 1

        # a list of all nodes currently active/executing a task.
        blocking_nodes = set(self.blocking_nodes())

        dependents = partial(dependent_nodes, scheme)

        blocked_nodes = reduce(set.union, map(dependents, blocking_nodes),
                               set(blocking_nodes))

        pending = self.pending_nodes()
        pending_downstream = set()
        for n in pending:
            depend = set(dependents(n))
            if isincycle(n):
                # a pending node in a cycle would would have a circular
                # dependency on itself, preventing any progress being made
                # by the workflow execution.
                cc = node_scc[n]
                depend -= set(cc)
            pending_downstream.update(depend)

        log.debug("Pending nodes: %s", pending)
        log.debug("Blocking nodes: %s", blocking_nodes)

        noneligible = pending_downstream | blocked_nodes
        return [node for node in pending if node not in noneligible]

    @Slot()
    def __process_next(self):
        if not self.__state == SignalManager.Running:
            log.debug("Received 'UpdateRequest' while not in 'Running' state")
            return

        if self.__runtime_state == SignalManager.Processing:
            # This happens if QCoreApplication.processEvents is called from
            # the input handlers. A `__process_next` must be rescheduled when
            # exiting process_queued.
            log.warning("Received 'UpdateRequest' while in 'process_queued'. "
                        "An update will be re-scheduled when exiting the "
                        "current update.")
            return

        nbusy = len(self.blocking_nodes())
        log.info(
            "'UpdateRequest' event, queued signals: %i, nbusy: %i "
            "(MAX_CONCURRENT: %i)", len(self.__input_queue), nbusy,
            MAX_CONCURRENT)

        if self.__input_queue and nbusy < MAX_CONCURRENT:
            try:
                self.process_queued()
            finally:
                # Schedule another update (will be a noop if nothing to do).
                if self.__input_queue and self.__state == SignalManager.Running:
                    self.__update_timer.start()

    def _update(self):  # type: () -> None
        """
        Schedule processing at a later time.
        """
        if self.__state == SignalManager.Running and \
                not self.__update_timer.isActive():
            self.__update_timer.start()

    def eventFilter(self, receiver, event):
        """
        Reimplemented.
        """
        if event.type() == QEvent.DeferredDelete \
                and receiver is self.__workflow:
            # ?? This is really, probably, mostly, likely not needed. Should
            # just raise error from __process_next.
            state = self.runtime_state()
            if state == SignalManager.Processing:
                log.critical(
                    "The workflow model %r received a deferred delete request "
                    "while performing an input update. "
                    "Deferring a 'DeferredDelete' event for the workflow "
                    "until SignalManager exits the current update step.",
                    self.__workflow)
                warnings.warn(
                    "The workflow model received a deferred delete request "
                    "while updating inputs. In the future this will raise "
                    "a RuntimeError",
                    _FutureRuntimeWarning,
                )
                event.setAccepted(False)
                self.processingFinished.connect(self.__workflow.deleteLater)
                self.stop()
                return True
        return super().eventFilter(receiver, event)
예제 #4
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        QGraphicsView.__init__(self, *args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        self.__scale = 10

    def setScene(self, scene):
        QGraphicsView.setScene(self, scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        QGraphicsView.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive(
            ) and self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()

        QGraphicsView.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()

        return QGraphicsView.mouseReleaseEvent(self, event)

    def reset_zoom(self):
        self.__set_zoom(10)

    def change_zoom(self, delta):
        self.__set_zoom(self.__scale + delta)

    def __set_zoom(self, scale):
        self.__scale = min(15, max(scale, 3))
        transform = QTransform()
        transform.scale(self.__scale / 10, self.__scale / 10)
        self.setTransform(transform)

    def wheelEvent(self, event: QWheelEvent):
        # use mouse position as anchor while zooming
        self.setTransformationAnchor(2)
        if event.modifiers() & Qt.ControlModifier and event.buttons(
        ) == Qt.NoButton:
            delta = event.angleDelta().y()
            if (QT_VERSION >= 0x050500
                    and event.source() != Qt.MouseEventNotSynthesized
                    and abs(delta) < 50):
                self.change_zoom(delta / 10)
            else:
                self.change_zoom(copysign(1, delta))
        else:
            super().wheelEvent(event)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if (self.verticalScrollBar().value() == vvalue
                    and self.horizontalScrollBar().value() == hvalue):
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        QGraphicsView.drawBackground(self, painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
예제 #5
0
class SignalManager(QObject):
    """
    Handle all runtime signal propagation for a :clas:`Scheme` instance.
    The scheme must be passed to the constructor and will become the parent
    of this object. Furthermore this should happen before any items
    (nodes, links) are added to the scheme.

    """
    Running, Stoped, Paused, Error = range(4)
    """SignalManger state flags."""

    Waiting, Processing = range(2)
    """SignalManager runtime state flags."""

    stateChanged = Signal(int)
    """Emitted when the state of the signal manager changes."""

    updatesPending = Signal()
    """Emitted when signals are added to the queue."""

    processingStarted = Signal([], [SchemeNode])
    """Emitted right before a `SchemeNode` instance has its inputs
    updated.
    """

    processingFinished = Signal([], [SchemeNode])
    """Emitted right after a `SchemeNode` instance has had its inputs
    updated.
    """

    runtimeStateChanged = Signal(int)
    """Emitted when `SignalManager`'s runtime state changes."""

    def __init__(self, scheme):
        assert(scheme)
        QObject.__init__(self, scheme)
        self._input_queue = []

        # mapping a node to it's current outputs
        # {node: {channel: {id: signal_value}}}
        self._node_outputs = {}

        self.__state = SignalManager.Running
        self.__runtime_state = SignalManager.Waiting

        # A flag indicating if UpdateRequest event should be rescheduled
        self.__reschedule = False
        self.__update_timer = QTimer(self, interval=100, singleShot=True)
        self.__update_timer.timeout.connect(self.__process_next)

    def _can_process(self):
        """
        Return a bool indicating if the manger can enter the main
        processing loop.

        """
        return self.__state not in [SignalManager.Error, SignalManager.Stoped]

    def scheme(self):
        """
        Return the parent class:`Scheme` instance.
        """
        return self.parent()

    def start(self):
        """
        Start the update loop.

        .. note:: The updates will not happen until the control reaches
                  the Qt event loop.

        """
        if self.__state != SignalManager.Running:
            self.__state = SignalManager.Running
            self.stateChanged.emit(SignalManager.Running)
            self._update()

    def stop(self):
        """
        Stop the update loop.

        .. note:: If the `SignalManager` is currently in `process_queues` it
                  will still update all current pending signals, but will not
                  re-enter until `start()` is called again

        """
        if self.__state != SignalManager.Stoped:
            self.__state = SignalManager.Stoped
            self.stateChanged.emit(SignalManager.Stoped)
            self.__update_timer.stop()

    def pause(self):
        """
        Pause the updates.

        """
        if self.__state != SignalManager.Paused:
            self.__state = SignalManager.Paused
            self.stateChanged.emit(SignalManager.Paused)
            self.__update_timer.stop()

    def resume(self):
        if self.__state == SignalManager.Paused:
            self.__state = SignalManager.Running
            self.stateChanged.emit(self.__state)
            self._update()

    def step(self):
        if self.__state == SignalManager.Paused:
            self.process_queued()

    def state(self):
        """
        Return the current state.
        """
        return self.__state

    def _set_runtime_state(self, state):
        """
        Set the runtime state.

        Should only be called by `SignalManager` implementations.

        """
        if self.__runtime_state != state:
            self.__runtime_state = state
            self.runtimeStateChanged.emit(self.__runtime_state)

    def runtime_state(self):
        """
        Return the runtime state. This can be `SignalManager.Waiting`
        or `SignalManager.Processing`.

        """
        return self.__runtime_state

    def on_node_removed(self, node):
        # remove all pending input signals for node so we don't get
        # stale references in process_node.
        # NOTE: This does not remove output signals for this node. In
        # particular the final 'None' will be delivered to the sink
        # nodes even after the source node is no longer in the scheme.
        log.info("Node %r removed. Removing pending signals.",
                 node.title)
        self.remove_pending_signals(node)

        del self._node_outputs[node]

    def on_node_added(self, node):
        self._node_outputs[node] = defaultdict(dict)

    def link_added(self, link):
        # push all current source values to the sink
        link.set_runtime_state(SchemeLink.Empty)
        if link.enabled:
            log.info("Link added (%s). Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))
            self._update()

        link.enabled_changed.connect(self.link_enabled_changed)

    def link_removed(self, link):
        # purge all values in sink's queue
        log.info("Link removed (%s). Scheduling signal data purge.", link)
        self.purge_link(link)
        link.enabled_changed.disconnect(self.link_enabled_changed)

    def link_enabled_changed(self, enabled):
        if enabled:
            link = self.sender()
            log.info("Link %s enabled. Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))

    def signals_on_link(self, link):
        """
        Return _Signal instances representing the current values
        present on the link.

        """
        items = self.link_contents(link)
        signals = []

        for key, value in items.items():
            signals.append(_Signal(link, value, key))

        return signals

    def link_contents(self, link):
        """
        Return the contents on link.
        """
        node, channel = link.source_node, link.source_channel

        if node in self._node_outputs:
            return self._node_outputs[node][channel]
        else:
            # if the the node was already removed it's tracked outputs in
            # _node_outputs are cleared, however the final 'None' signal
            # deliveries for the link are left in the _input_queue.
            pending = [sig for sig in self._input_queue
                       if sig.link is link]
            return {sig.id: sig.value for sig in pending}

    def send(self, node, channel, value, id):
        """
        """
        log.debug("%r sending %r (id: %r) on channel %r",
                  node.title, type(value), id, channel.name)

        scheme = self.scheme()

        self._node_outputs[node][channel][id] = value

        links = scheme.find_links(source_node=node, source_channel=channel)
        links = filter(is_enabled, links)

        signals = []
        for link in links:
            signals.append(_Signal(link, value, id))

        self._schedule(signals)

    def purge_link(self, link):
        """
        Purge the link (send None for all ids currently present)
        """
        contents = self.link_contents(link)
        ids = contents.keys()
        signals = [_Signal(link, None, id) for id in ids]

        self._schedule(signals)

    def _schedule(self, signals):
        """
        Schedule a list of :class:`_Signal` for delivery.
        """
        self._input_queue.extend(signals)

        for link in {sig.link for sig in signals}:
            # update the SchemeLink's runtime state flags
            contents = self.link_contents(link)
            if any(value is not None for value in contents.values()):
                state = SchemeLink.Active
            else:
                state = SchemeLink.Empty
            link.set_runtime_state(state | SchemeLink.Pending)

        if signals:
            self.updatesPending.emit()

        self._update()

    def _update_link(self, link):
        """
        Schedule update of a single link.
        """
        signals = self.signals_on_link(link)
        self._schedule(signals)

    def process_queued(self, max_nodes=None):
        """
        Process queued signals.

        Take one node node from the pending input queue and deliver
        all scheduled signals.
        """
        if not (max_nodes is None or max_nodes == 1):
            warnings.warn(
                "`max_nodes` is deprecated and unused (will always equal 1)",
                DeprecationWarning, stacklevel=2)

        if self.__runtime_state == SignalManager.Processing:
            raise RuntimeError("Cannot re-enter 'process_queued'")

        if not self._can_process():
            raise RuntimeError("Can't process in state %i" % self.__state)

        log.info("SignalManager: Processing queued signals")

        node_update_front = self.node_update_front()
        log.debug("SignalManager: Nodes eligible for update %s",
                  [node.title for node in node_update_front])

        if node_update_front:
            node = node_update_front[0]
            self._set_runtime_state(SignalManager.Processing)
            try:
                self.process_node(node)
            finally:
                self._set_runtime_state(SignalManager.Waiting)

    def process_node(self, node):
        """
        Process pending input signals for `node`.
        """
        signals_in = self.pending_input_signals(node)
        self.remove_pending_signals(node)

        signals_in = self.compress_signals(signals_in)

        log.debug("Processing %r, sending %i signals.",
                  node.title, len(signals_in))
        # Clear the link's pending flag.
        for link in {sig.link for sig in signals_in}:
            link.set_runtime_state(link.runtime_state() & ~SchemeLink.Pending)

        assert ({sig.link for sig in self._input_queue}
                .intersection({sig.link for sig in signals_in}) == set([]))
        self.processingStarted.emit()
        self.processingStarted[SchemeNode].emit(node)
        try:
            self.send_to_node(node, signals_in)
        finally:
            self.processingFinished.emit()
            self.processingFinished[SchemeNode].emit(node)

    def compress_signals(self, signals):
        """
        Compress a list of :class:`_Signal` instances to be delivered.

        The base implementation returns the list unmodified.

        """
        return signals

    def send_to_node(self, node, signals):
        """
        Abstract. Reimplement in subclass.

        Send/notify the :class:`SchemeNode` instance (or whatever
        object/instance it is a representation of) that it has new inputs
        as represented by the signals list (list of :class:`_Signal`).

        """
        raise NotImplementedError

    def is_pending(self, node):
        """
        Is `node` (class:`SchemeNode`) scheduled for processing (i.e.
        it has incoming pending signals).

        """
        return node in [signal.link.sink_node for signal in self._input_queue]

    def pending_nodes(self):
        """
        Return a list of pending nodes.

        The nodes are returned in the order they were enqueued for
        signal delivery.

        Returns
        -------
        nodes : List[SchemeNode]
        """
        return list(unique(sig.link.sink_node for sig in self._input_queue))

    def pending_input_signals(self, node):
        """
        Return a list of pending input signals for node.
        """
        return [signal for signal in self._input_queue
                if node is signal.link.sink_node]

    def remove_pending_signals(self, node):
        """
        Remove pending signals for `node`.
        """
        for signal in self.pending_input_signals(node):
            try:
                self._input_queue.remove(signal)
            except ValueError:
                pass

    def blocking_nodes(self):
        """
        Return a list of nodes in a blocking state.
        """
        scheme = self.scheme()
        return [node for node in scheme.nodes if self.is_blocking(node)]

    def is_blocking(self, node):
        return False

    def node_update_front(self):
        """
        Return a list of nodes on the update front, i.e. nodes scheduled for
        an update that have no ancestor which is either itself scheduled
        for update or is in a blocking state)

        .. note::
            The node's ancestors are only computed over enabled links.

        """
        scheme = self.scheme()

        def expand(node):
            return [link.sink_node for
                link in scheme.find_links(source_node=node) if
                link.enabled]

        components = strongly_connected_components(scheme.nodes, expand)
        node_scc = {node: scc for scc in components for node in scc}

        def isincycle(node):
            return len(node_scc[node]) > 1

        # a list of all nodes currently active/executing a task.
        blocking_nodes = set(self.blocking_nodes())

        dependents = partial(dependent_nodes, scheme)

        blocked_nodes = reduce(set.union,
                               map(dependents, blocking_nodes),
                               set(blocking_nodes))

        pending = self.pending_nodes()
        pending_downstream = set()
        for n in pending:
            depend = set(dependents(n))
            if isincycle(n):
                # a pending node in a cycle would would have a circular
                # dependency on itself, preventing any progress being made
                # by the workflow execution.
                cc = node_scc[n]
                depend -= set(cc)
            pending_downstream.update(depend)

        log.debug("Pending nodes: %s", pending)
        log.debug("Blocking nodes: %s", blocking_nodes)

        noneligible = pending_downstream | blocked_nodes
        return [node for node in pending if node not in noneligible]

    @Slot()
    def __process_next(self):
        if not self.__state == SignalManager.Running:
            log.debug("Received 'UpdateRequest' while not in 'Running' state")
            return

        if self.__runtime_state == SignalManager.Processing:
            # This happens if someone calls QCoreApplication.processEvents
            # from the signal handlers.
            # A `__process_next` must be rescheduled when exiting
            # process_queued.
            log.warning("Received 'UpdateRequest' while in 'process_queued'. "
                        "An update will be re-scheduled when exiting the "
                        "current update.")
            self.__reschedule = True
            return

        nbusy = len(self.blocking_nodes())
        log.info("'UpdateRequest' event, queued signals: %i, nbusy: %i "
                 "(MAX_CONCURRENT: %i)",
                 len(self._input_queue), nbusy, MAX_CONCURRENT)

        if self._input_queue and nbusy < MAX_CONCURRENT:
            self.process_queued()

        if self.__reschedule and self.__state == SignalManager.Running:
            self.__reschedule = False
            log.debug("Rescheduling signal update")
            self.__update_timer.start()

        nbusy = len(self.blocking_nodes())
        if self.node_update_front() and nbusy < MAX_CONCURRENT:
            log.debug("More nodes are eligible for an update. "
                      "Scheduling another update.")
            self._update()

    def _update(self):
        """
        Schedule processing at a later time.
        """
        if self.__state == SignalManager.Running and \
                not self.__update_timer.isActive():
            self.__update_timer.start()
예제 #6
0
class SignalManager(QObject):
    """
    SignalManager handles the runtime signal propagation for a :class:`.Scheme`
    instance.

    Note
    ----
    If a scheme instance is passed as a parent to the constructor it is also
    set as the workflow model.
    """
    class State(enum.IntEnum):
        """
        SignalManager state flags.

        .. seealso:: :func:`SignalManager.state()`
        """
        #: The manager is running, i.e. it propagates signals
        Running = 0
        #: The manager is stopped. It does not track node output changes,
        #: and does not deliver signals to dependent nodes
        Stopped = 1
        #: The manager is paused. It still tracks node output changes, but
        #: does not deliver new signals to dependent nodes. The pending signals
        #: will be delivered once it enters Running state again
        Paused = 2

    #: The manager is running, i.e. it propagates signals
    Running = State.Running
    #: The manager is stopped. It does not track node ouput changes,
    #: and does not deliver signals to dependent nodes
    Stopped = State.Stopped
    #: The manager is paused. It still tracks node output changes, but
    #: does not deliver new signals to dependent nodes. The pending signals
    #: will be delivered once it enters Running state again
    Paused = Stopped.Paused

    # unused; back-compatibility
    Error = 3

    class RuntimeState(enum.IntEnum):
        """
        SignalManager runtime state.

        See Also
        --------
        SignalManager.runtime_state
        """
        #: Waiting, idle state. The signal queue is empty
        Waiting = 0
        #: ...
        Processing = 1

    Waiting = RuntimeState.Waiting
    Processing = RuntimeState.Processing

    #: Emitted when the state of the signal manager changes.
    stateChanged = pyqtSignal(int)
    #: Emitted when signals are added to the queue.
    updatesPending = pyqtSignal()
    #: Emitted right before a `SchemeNode` instance has its inputs updated.
    processingStarted = pyqtSignal([], [SchemeNode])
    #: Emitted right after a `SchemeNode` instance has had its inputs updated.
    processingFinished = pyqtSignal([], [SchemeNode])
    #: Emitted when `SignalManager`'s runtime state changes.
    runtimeStateChanged = pyqtSignal(int)

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.__workflow = None  # type: Optional[Scheme]
        self.__input_queue = []  # type: List[Signal]

        # mapping a node to its current outputs
        self.__node_outputs = {}  # type: Dict[Node, Dict[OutputSignal, Dict[Any, Any]]]

        self.__state = SignalManager.Running
        self.__runtime_state = SignalManager.Waiting

        # A flag indicating if UpdateRequest event should be rescheduled
        self.__reschedule = False
        self.__update_timer = QTimer(self, interval=100, singleShot=True)
        self.__update_timer.timeout.connect(self.__process_next)

        if isinstance(parent, Scheme):
            self.set_workflow(parent)

    def _can_process(self):
        """
        Return a bool indicating if the manger can enter the main
        processing loop.

        """
        return self.__state not in [SignalManager.Error, SignalManager.Stopped]

    def workflow(self):
        # type: () -> Scheme
        """
        Return the :class:`Scheme` instance.
        """
        return self.__workflow
    #: Alias
    scheme = workflow

    def set_workflow(self, workflow):
        # type: (Scheme) -> None
        """
        Set the workflow model.

        Parameters
        ----------
        workflow : Scheme
        """
        if workflow is self.__workflow:
            return

        if self.__workflow is not None:
            for link in self.__workflow.links:
                link.enabled_changed.disconnect(self.link_enabled_changed)

            self.__workflow.node_added.disconnect(self.on_node_added)
            self.__workflow.node_removed.disconnect(self.on_node_removed)
            self.__workflow.link_added.disconnect(self.link_added)
            self.__workflow.link_removed.disconnect(self.link_removed)
            self.__workflow.removeEventFilter(self)
            self.__node_outputs = {}
            self.__input_queue = []

        self.__workflow = workflow

        if workflow is not None:
            workflow.node_added.connect(self.on_node_added)
            workflow.node_removed.connect(self.on_node_removed)
            workflow.link_added.connect(self.link_added)
            workflow.link_removed.connect(self.link_removed)
            for node in workflow.nodes:
                self.__node_outputs[node] = defaultdict(dict)
            for link in workflow.links:
                link.enabled_changed.connect(self.link_enabled_changed)
            workflow.installEventFilter(self)

    def has_pending(self):
        """
        Does the manager have any signals to deliver?
        """
        return bool(self.__input_queue)

    def start(self):
        """
        Start the update loop.

        Note
        ----
        The updates will not happen until the control reaches the Qt event
        loop.
        """
        if self.__state != SignalManager.Running:
            self.__state = SignalManager.Running
            self.stateChanged.emit(SignalManager.Running)
            self._update()

    def stop(self):
        """
        Stop the update loop.

        Note
        ----
        If the `SignalManager` is currently in `process_queues` it will
        still update all current pending signals, but will not re-enter
        until `start()` is called again.
        """
        if self.__state != SignalManager.Stopped:
            self.__state = SignalManager.Stopped
            self.stateChanged.emit(SignalManager.Stopped)
            self.__update_timer.stop()

    def pause(self):
        """
        Pause the delivery of signals.
        """
        if self.__state != SignalManager.Paused:
            self.__state = SignalManager.Paused
            self.stateChanged.emit(SignalManager.Paused)
            self.__update_timer.stop()

    def resume(self):
        """
        Resume the delivery of signals.
        """
        if self.__state == SignalManager.Paused:
            self.__state = SignalManager.Running
            self.stateChanged.emit(self.__state)
            self._update()

    def step(self):
        """
        Deliver signals to a single node (only applicable while the `state()`
        is `Paused`).
        """
        if self.__state == SignalManager.Paused:
            self.process_queued()

    def state(self):
        # type: () -> State
        """
        Return the current state.

        Return
        ------
        state : SignalManager.State
        """
        return self.__state

    def _set_runtime_state(self, state):
        """
        Set the runtime state.

        Should only be called by `SignalManager` implementations.
        """
        if self.__runtime_state != state:
            self.__runtime_state = state
            self.runtimeStateChanged.emit(self.__runtime_state)

    def runtime_state(self):
        # type: () -> RuntimeState
        """
        Return the runtime state. This can be `SignalManager.Waiting`
        or `SignalManager.Processing`.

        """
        return self.__runtime_state

    def on_node_removed(self, node):
        # remove all pending input signals for node so we don't get
        # stale references in process_node.
        # NOTE: This does not remove output signals for this node. In
        # particular the final 'None' will be delivered to the sink
        # nodes even after the source node is no longer in the scheme.
        log.info("Removing pending signals for '%s'.", node.title)
        self.remove_pending_signals(node)

        del self.__node_outputs[node]

    def on_node_added(self, node):
        self.__node_outputs[node] = defaultdict(dict)

    def link_added(self, link):
        # push all current source values to the sink
        link.set_runtime_state(SchemeLink.Empty)
        if link.enabled:
            log.info("Scheduling signal data update for '%s'.", link)
            self._schedule(self.signals_on_link(link))
            self._update()

        link.enabled_changed.connect(self.link_enabled_changed)

    def link_removed(self, link):
        # purge all values in sink's queue
        log.info("Scheduling signal data purge (%s).", link)
        self.purge_link(link)
        link.enabled_changed.disconnect(self.link_enabled_changed)

    def link_enabled_changed(self, enabled):
        if enabled:
            link = self.sender()
            log.info("Link %s enabled. Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))

    def signals_on_link(self, link):
        # type: (SchemeLink) -> List[Signal]
        """
        Return :class:`Signal` instances representing the current values
        present on the link.
        """
        items = self.link_contents(link)
        signals = []

        for key, value in items.items():
            signals.append(Signal(link, value, key))

        return signals

    def link_contents(self, link):
        """
        Return the contents on link.
        """
        node, channel = link.source_node, link.source_channel

        if node in self.__node_outputs:
            return self.__node_outputs[node][channel]
        else:
            # if the the node was already removed its tracked outputs in
            # __node_outputs are cleared, however the final 'None' signal
            # deliveries for the link are left in the _input_queue.
            pending = [sig for sig in self.__input_queue
                       if sig.link is link]
            return {sig.id: sig.value for sig in pending}

    def send(self, node, channel, value, id):
        # type: (SchemeNode, OutputSignal, Any, Any) -> None
        """
        Send the `value` with `id` on an output `channel` from node.

        Schedule the signal delivery to all dependent nodes

        Parameters
        ----------
        node : SchemeNode
            The originating node.
        channel : OutputSignal
            The nodes output on which the value is sent.
        value : Any
            The value to send,
        id : Any
            Signal id.
        """
        log.debug("%r sending %r (id: %r) on channel %r",
                  node.title, type(value), id, channel.name)

        scheme = self.scheme()

        self.__node_outputs[node][channel][id] = value

        links = scheme.find_links(source_node=node, source_channel=channel)
        links = filter(is_enabled, links)

        signals = []
        for link in links:
            signals.append(Signal(link, value, id))

        self._schedule(signals)

    def purge_link(self, link):
        """
        Purge the link (send None for all ids currently present)
        """
        contents = self.link_contents(link)
        ids = contents.keys()
        signals = [Signal(link, None, id) for id in ids]

        self._schedule(signals)

    def _schedule(self, signals):
        """
        Schedule a list of :class:`Signal` for delivery.
        """
        self.__input_queue.extend(signals)

        for link in {sig.link for sig in signals}:
            # update the SchemeLink's runtime state flags
            contents = self.link_contents(link)
            if any(value is not None for value in contents.values()):
                state = SchemeLink.Active
            else:
                state = SchemeLink.Empty
            link.set_runtime_state(state | SchemeLink.Pending)

        if signals:
            self.updatesPending.emit()

        self._update()

    def _update_link(self, link):
        """
        Schedule update of a single link.
        """
        signals = self.signals_on_link(link)
        self._schedule(signals)

    def process_queued(self, max_nodes=None):
        """
        Process queued signals.

        Take one node node from the pending input queue and deliver
        all scheduled signals.
        """
        if not (max_nodes is None or max_nodes == 1):
            warnings.warn(
                "`max_nodes` is deprecated and unused (will always equal 1)",
                DeprecationWarning, stacklevel=2)

        if self.__runtime_state == SignalManager.Processing:
            raise RuntimeError("Cannot re-enter 'process_queued'")

        if not self._can_process():
            raise RuntimeError("Can't process in state %i" % self.__state)

        log.info("SignalManager: Processing queued signals")

        node_update_front = self.node_update_front()
        log.debug("SignalManager: Nodes eligible for update %s",
                  [node.title for node in node_update_front])

        if node_update_front:
            node = node_update_front[0]
            self._set_runtime_state(SignalManager.Processing)
            try:
                self.process_node(node)
            finally:
                self._set_runtime_state(SignalManager.Waiting)

    def process_node(self, node):
        """
        Process pending input signals for `node`.
        """
        signals_in = self.pending_input_signals(node)
        self.remove_pending_signals(node)

        signals_in = self.compress_signals(signals_in)

        log.debug("Processing %r, sending %i signals.",
                  node.title, len(signals_in))
        # Clear the link's pending flag.
        for link in {sig.link for sig in signals_in}:
            link.set_runtime_state(link.runtime_state() & ~SchemeLink.Pending)

        def process_dynamic(signals):
            # type: (List[Signal]) -> List[Signal]
            """
            Process dynamic signals; Update the link's dynamic_enabled flag if
            the value is valid; replace values that do not type check with
            `None`
            """
            res = []
            for sig in signals:
                # Check and update the dynamic link state
                link = sig.link
                if sig.link.is_dynamic():
                    link.dynamic_enabled = can_enable_dynamic(link, sig.value)
                    if not link.dynamic_enabled:
                        # Send None instead
                        sig = Signal(link, None, sig.id)
                res.append(sig)
            return res
        signals_in = process_dynamic(signals_in)
        assert ({sig.link for sig in self.__input_queue}
                .intersection({sig.link for sig in signals_in}) == set([]))
        self.processingStarted.emit()
        self.processingStarted[SchemeNode].emit(node)
        try:
            self.send_to_node(node, signals_in)
        finally:
            self.processingFinished.emit()
            self.processingFinished[SchemeNode].emit(node)

    def compress_signals(self, signals):
        # type: (List[Signal]) -> List[Signal]
        """
        Compress a list of :class:`Signal` instances to be delivered.

        Before the signal values are delivered to the sink node they can be
        optionally `compressed`, i.e. values can be merged or dropped
        depending on the execution semantics.

        The input list is in the order that the signals were enqueued.

        The base implementation returns the list unmodified.

        Parameters
        ----------
        signals : List[Signal]

        Return
        ------
        signals : List[Signal]
        """
        return signals

    def send_to_node(self, node, signals):
        # type: (SchemeNode, List[Signal]) -> None
        """
        Abstract. Reimplement in subclass.

        Send/notify the `node` instance (or whatever object/instance it is a
        representation of) that it has new inputs as represented by the
        `signals` list).

        Parameters
        ----------
        node : SchemeNode
        signals : List[Signal]
        """
        raise NotImplementedError

    def is_pending(self, node):
        # type: (SchemeNode) -> bool
        """
        Is `node` (class:`SchemeNode`) scheduled for processing (i.e.
        it has incoming pending signals).

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        pending : bool
        """
        return node in [signal.link.sink_node for signal in self.__input_queue]

    def pending_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of pending nodes.

        The nodes are returned in the order they were enqueued for
        signal delivery.

        Returns
        -------
        nodes : List[SchemeNode]
        """
        return list(unique(sig.link.sink_node for sig in self.__input_queue))

    def pending_input_signals(self, node):
        # type: (SchemeNode) -> List[Signal]
        """
        Return a list of pending input signals for node.
        """
        return [signal for signal in self.__input_queue
                if node is signal.link.sink_node]

    def remove_pending_signals(self, node):
        # type: (SchemeNode) -> None
        """
        Remove pending signals for `node`.
        """
        for signal in self.pending_input_signals(node):
            try:
                self.__input_queue.remove(signal)
            except ValueError:
                pass

    def blocking_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of nodes in a blocking state.
        """
        scheme = self.scheme()
        return [node for node in scheme.nodes if self.is_blocking(node)]

    def is_blocking(self, node):
        # type: (SchemeNode) -> bool
        """
        Is the node in `blocking` state.

        Is it currently in a state where will produce new outputs and
        therefore no signals should be delivered to dependent nodes until
        it does so.

        The default implementation returns False.
        """
        # TODO: this needs a different name
        return False

    def node_update_front(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of nodes on the update front, i.e. nodes scheduled for
        an update that have no ancestor which is either itself scheduled
        for update or is in a blocking state).

        Note
        ----
        The node's ancestors are only computed over enabled links.
        """
        scheme = self.scheme()

        def expand(node):
            return [link.sink_node
                    for link in scheme.find_links(source_node=node)
                    if link.enabled]

        components = strongly_connected_components(scheme.nodes, expand)
        node_scc = {node: scc for scc in components for node in scc}

        def isincycle(node):
            return len(node_scc[node]) > 1

        # a list of all nodes currently active/executing a task.
        blocking_nodes = set(self.blocking_nodes())

        dependents = partial(dependent_nodes, scheme)

        blocked_nodes = reduce(set.union,
                               map(dependents, blocking_nodes),
                               set(blocking_nodes))

        pending = self.pending_nodes()
        pending_downstream = set()
        for n in pending:
            depend = set(dependents(n))
            if isincycle(n):
                # a pending node in a cycle would would have a circular
                # dependency on itself, preventing any progress being made
                # by the workflow execution.
                cc = node_scc[n]
                depend -= set(cc)
            pending_downstream.update(depend)

        log.debug("Pending nodes: %s", pending)
        log.debug("Blocking nodes: %s", blocking_nodes)

        noneligible = pending_downstream | blocked_nodes
        return [node for node in pending if node not in noneligible]

    @Slot()
    def __process_next(self):
        if not self.__state == SignalManager.Running:
            log.debug("Received 'UpdateRequest' while not in 'Running' state")
            return

        if self.__runtime_state == SignalManager.Processing:
            # This happens if QCoreApplication.processEvents is called from
            # the input handlers. A `__process_next` must be rescheduled when
            # exiting process_queued.
            log.warning("Received 'UpdateRequest' while in 'process_queued'. "
                        "An update will be re-scheduled when exiting the "
                        "current update.")
            self.__reschedule = True
            return

        nbusy = len(self.blocking_nodes())
        log.info("'UpdateRequest' event, queued signals: %i, nbusy: %i "
                 "(MAX_CONCURRENT: %i)",
                 len(self.__input_queue), nbusy, MAX_CONCURRENT)

        if self.__input_queue and nbusy < MAX_CONCURRENT:
            self.process_queued()

        if self.__reschedule and self.__state == SignalManager.Running:
            self.__reschedule = False
            log.debug("Rescheduling signal update")
            self.__update_timer.start()

        nbusy = len(self.blocking_nodes())
        if self.node_update_front() and nbusy < MAX_CONCURRENT:
            log.debug("More nodes are eligible for an update. "
                      "Scheduling another update.")
            self._update()

    def _update(self):
        """
        Schedule processing at a later time.
        """
        if self.__state == SignalManager.Running and \
                not self.__update_timer.isActive():
            self.__update_timer.start()

    def eventFilter(self, receiver, event):
        """
        Reimplemented.
        """
        if event.type() == QEvent.DeferredDelete \
                and receiver is self.__workflow:
            # ?? This is really, probably, mostly, likely not needed. Should
            # just raise error from __process_next.
            state = self.runtime_state()
            if state == SignalManager.Processing:
                log.critical(
                    "The workflow model %r received a deferred delete request "
                    "while performing an input update. "
                    "Deferring a 'DeferredDelete' event for the workflow "
                    "until SignalManager exits the current update step.",
                    self.__workflow
                )
                warnings.warn(
                    "The workflow model received a deferred delete request "
                    "while updating inputs. In the future this will raise "
                    "a RuntimeError", _FutureRuntimeWarning,
                )
                event.setAccepted(False)
                self.processingFinished.connect(self.__workflow.deleteLater)
                self.stop()
                return True
        return super().eventFilter(receiver, event)
예제 #7
0
class WidgetManager(QObject):
    """
    WidgetManager class is responsible for creation, tracking and deletion
    of UI elements constituting an interactive workflow.

    It does so by reacting to changes in the underlying workflow model,
    creating and destroying the components when needed.

    This is an abstract class, subclassed MUST reimplement at least
    :func:`create_widget_for_node` and :func:`delete_widget_for_node`.

    The widgets created with :func:`create_widget_for_node` will automatically
    receive dispatched events:

        * :data:`WorkflowEvent.InputLinkAdded` - when a new input link is added to
          the workflow.
        * :data:`LinkEvent.InputLinkRemoved` - when a input link is removed
        * :data:`LinkEvent.OutputLinkAdded` - when a new output link is added to
          the workflow
        * :data:`LinkEvent.InputLinkRemoved` - when a output link is removed
        * :data:`WorkflowEnvEvent.WorkflowEnvironmentChanged` - when the
          workflow environment changes.

    .. seealso:: :func:`.Scheme.add_link()`, :func:`Scheme.remove_link`,
                 :func:`.Scheme.runtime_env`
    """
    #: A new QWidget was created and added by the manager.
    widget_for_node_added = Signal(SchemeNode, QWidget)

    #: A QWidget was removed, hidden and will be deleted when appropriate.
    widget_for_node_removed = Signal(SchemeNode, QWidget)

    class CreationPolicy(enum.Enum):
        """
        Widget Creation Policy.
        """
        #: Widgets are scheduled to be created from the event loop, or when
        #: first accessed with `widget_for_node`
        Normal = "Normal"
        #: Widgets are created immediately when a node is added to the
        #: workflow model.
        Immediate = "Immediate"
        #: Widgets are created only when first accessed with `widget_for_node`
        #: (e.g. when activated in the view).
        OnDemand = "OnDemand"

    Normal = CreationPolicy.Normal
    Immediate = CreationPolicy.Immediate
    OnDemand = CreationPolicy.OnDemand

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__workflow = None  # type: Optional[Scheme]
        self.__creation_policy = WidgetManager.Normal
        self.__float_widgets_on_top = False

        self.__item_for_node = {}  # type: Dict[SchemeNode, Item]
        self.__item_for_widget = {}  # type: Dict[QWidget, Item]

        self.__init_queue = deque()  # type: Deque[SchemeNode]

        self.__init_timer = QTimer(self, singleShot=True)
        self.__init_timer.timeout.connect(self.__process_init_queue)

        self.__activation_monitor = ActivationMonitor(self)
        self.__activation_counter = itertools.count()
        self.__activation_monitor.activated.connect(self.__mark_activated)

    def set_workflow(self, workflow):
        # type: (Scheme) -> None
        """
        Set the workflow.
        """
        if workflow is self.__workflow:
            return

        if self.__workflow is not None:
            # cleanup
            for node in self.__workflow.nodes:
                self.__remove_node(node)
            self.__workflow.node_added.disconnect(self.__on_node_added)
            self.__workflow.node_removed.disconnect(self.__on_node_removed)
            self.__workflow.link_added.disconnect(self.__on_link_added)
            self.__workflow.link_removed.disconnect(self.__on_link_removed)
            self.__workflow.runtime_env_changed.disconnect(self.__on_env_changed)
            self.__workflow.removeEventFilter(self)

        self.__workflow = workflow

        workflow.node_added.connect(
            self.__on_node_added, Qt.UniqueConnection)
        workflow.node_removed.connect(
            self.__on_node_removed, Qt.UniqueConnection)
        workflow.link_added.connect(
            self.__on_link_added, Qt.UniqueConnection)
        workflow.link_removed.connect(
            self.__on_link_removed, Qt.UniqueConnection)
        workflow.runtime_env_changed.connect(
            self.__on_env_changed, Qt.UniqueConnection)
        workflow.installEventFilter(self)
        for node in workflow.nodes:
            self.__add_node(node)

    def workflow(self):
        return self.__workflow

    scheme = workflow
    set_scheme = set_workflow

    def set_creation_policy(self, policy):
        # type: (CreationPolicy) -> None
        """
        Set the widget creation policy.
        """
        if self.__creation_policy != policy:
            self.__creation_policy = policy
            if self.__creation_policy == WidgetManager.Immediate:
                self.__init_timer.stop()
                # create all
                if self.__workflow is not None:
                    for node in self.__workflow.nodes:
                        self.ensure_created(node)
            elif self.__creation_policy == WidgetManager.Normal:
                if not self.__init_timer.isActive() and self.__init_queue:
                    self.__init_timer.start()
            elif self.__creation_policy == WidgetManager.OnDemand:
                self.__init_timer.stop()
            else:
                assert False

    def creation_policy(self):
        """
        Return the current widget creation policy.
        """
        return self.__creation_policy

    def create_widget_for_node(self, node):
        # type: (SchemeNode) -> QWidget
        """
        Create and initialize a widget for node.

        This is an abstract method. Subclasses must reimplemented it.
        """
        raise NotImplementedError()

    def delete_widget_for_node(self, node, widget):
        """
        Remove and delete widget for node.

        This is an abstract method. Subclasses must reimplemented it.
        """
        raise NotImplementedError()

    def node_for_widget(self, widget):
        # type: (QWidget) -> Optional[SchemeNode]
        """
        Return the node for widget.
        """
        item = self.__item_for_widget.get(widget)
        if item is not None:
            return item.node
        else:
            return None

    def widget_for_node(self, node):
        # type: (SchemeNode) -> Optional[QWidget]
        """
        Return the widget for node.
        """
        self.ensure_created(node)
        item = self.__item_for_node.get(node)
        return item.widget if item is not None else None

    def __add_widget_for_node(self, node):
        # type: (SchemeNode) -> None
        item = self.__item_for_node.get(node)
        if item is not None:
            return
        if node not in self.__workflow.nodes:
            return

        if node in self.__init_queue:
            self.__init_queue.remove(node)

        item = Item(node, None, -1)
        # Insert on the node -> item mapping.
        self.__item_for_node[node] = item
        log.debug("Creating widget for node %s", node)
        try:
            w = self.create_widget_for_node(node)
        except Exception:  # pylint: disable=broad-except
            log.critical("", exc_info=True)
            lines = traceback.format_exception(*sys.exc_info())
            text = "".join(lines)
            errorwidget = QLabel(
                textInteractionFlags=Qt.TextSelectableByMouse, wordWrap=True,
                objectName="widgetmanager-error-placeholder",
                text="<pre>" + escape(text) + "</pre>"
            )
            item.errorwidget = errorwidget
            node.set_state_message(UserMessage(text, UserMessage.Error, 0))
            return
        else:
            item.widget = w
            self.__item_for_widget[w] = item

        self.__set_float_on_top_flag(w)

        w.installEventFilter(self.__activation_monitor)
        # Up shortcut (activate/open parent)
        up_shortcut = QShortcut(
            QKeySequence(Qt.ControlModifier + Qt.Key_Up), w)
        up_shortcut.activated.connect(self.__on_activate_parent)

        # send all the post creation notification events
        workflow = self.__workflow
        assert workflow is not None
        inputs = workflow.find_links(sink_node=node)
        for link in inputs:
            ev = LinkEvent(LinkEvent.InputLinkAdded, link)
            QCoreApplication.sendEvent(w, ev)
        outputs = workflow.find_links(source_node=node)
        for link in outputs:
            ev = LinkEvent(LinkEvent.OutputLinkAdded, link)
            QCoreApplication.sendEvent(w, ev)

        self.widget_for_node_added.emit(node, w)

    def ensure_created(self, node):
        # type: (SchemeNode) -> None
        """
        Ensure that the widget for node is created.
        """
        if node not in self.__workflow.nodes:
            return
        item = self.__item_for_node.get(node)
        if item is None:
            self.__add_widget_for_node(node)

    def __on_node_added(self, node):  # type: (SchemeNode) -> None
        assert self.__workflow is not None
        assert node in self.__workflow.nodes
        assert node not in self.__item_for_node
        self.__add_node(node)

    def __add_node(self, node): # type: (SchemeNode) -> None
        # add node for tracking
        node.installEventFilter(self)
        if self.__creation_policy == WidgetManager.Immediate:
            self.ensure_created(node)
        elif self.__creation_policy == WidgetManager.Normal:
            self.__init_queue.append(node)
            self.__init_timer.start()

    def __on_node_removed(self, node):  # type: (SchemeNode) -> None
        assert self.__workflow is not None
        assert node not in self.__workflow.nodes
        self.__remove_node(node)

    def __remove_node(self, node):  # type: (SchemeNode) -> None
        # remove the node and its widget from tracking.
        node.removeEventFilter(self)
        if node in self.__init_queue:
            self.__init_queue.remove(node)
        item = self.__item_for_node.get(node)

        if item is not None and item.widget is not None:
            widget = item.widget
            assert widget in self.__item_for_widget
            del self.__item_for_widget[widget]
            widget.removeEventFilter(self.__activation_monitor)
            item.widget = None
            self.widget_for_node_removed.emit(node, widget)
            self.delete_widget_for_node(node, widget)

        if item is not None:
            del self.__item_for_node[node]

    @Slot()
    def __process_init_queue(self):
        log.debug("__process_init_queue")
        while self.__init_queue:
            node = self.__init_queue.popleft()
            assert node in self.__workflow.nodes
            self.ensure_created(node)

    def __on_link_added(self, link):  # type: (SchemeLink) -> None
        assert link.source_node in self.__workflow.nodes
        assert link.sink_node in self.__workflow.nodes
        source = self.__item_for_widget.get(link.source_node)
        sink = self.__item_for_widget.get(link.sink_node)
        # notify the node gui of an added link
        if source is not None:
            ev = LinkEvent(LinkEvent.OutputLinkAdded, link)
            QCoreApplication.sendEvent(source.widget, ev)
        if sink is not None:
            ev = LinkEvent(LinkEvent.InputLinkAdded, link)
            QCoreApplication.sendEvent(sink.widget, ev)

    def __on_link_removed(self, link):  # type: (SchemeLink) -> None
        assert link.source_node in self.__workflow.nodes
        assert link.sink_node in self.__workflow.nodes
        source = self.__item_for_widget.get(link.source_node)
        sink = self.__item_for_widget.get(link.sink_node)
        # notify the node gui of an removed link
        if source is not None:
            ev = LinkEvent(LinkEvent.OutputLinkRemoved, link)
            QCoreApplication.sendEvent(source.widget, ev)
        if sink is not None:
            ev = LinkEvent(LinkEvent.InputLinkRemoved, link)
            QCoreApplication.sendEvent(sink.widget, ev)

    def __mark_activated(self, widget):  # type: (QWidget) ->  None
        # Update the tracked stacking order for `widget`
        item = self.__item_for_widget.get(widget)
        if item is not None:
            item.activation_order = next(self.__activation_counter)

    def activate_widget_for_node(self, node, widget):
        # type: (SchemeNode, QWidget) -> None
        """
        Activate the widget for node (show and raise above other)
        """
        if widget.windowState() == Qt.WindowMinimized:
            widget.showNormal()
        widget.setVisible(True)
        widget.raise_()
        widget.activateWindow()

    def activate_window_group(self, group):
        # type: (Scheme.WindowGroup) -> None
        self.restore_window_state(group.state)

    def raise_widgets_to_front(self):
        """
        Raise all current visible widgets to the front.

        The widgets will be stacked by activation order.
        """
        workflow = self.__workflow
        if workflow is None:
            return

        items = filter(
            lambda item: (
                item.widget.isVisible()
                if item is not None and item.widget is not None
                else False)
            ,
            map(self.__item_for_node.get, workflow.nodes))
        self.__raise_and_activate(items)

    def set_float_widgets_on_top(self, float_on_top):
        """
        Set `Float Widgets on Top` flag on all widgets.
        """
        self.__float_widgets_on_top = float_on_top
        for item in self.__item_for_node.values():
            if item.widget is not None:
                self.__set_float_on_top_flag(item.widget)

    def save_window_state(self):
        # type: () -> List[Tuple[SchemeNode, bytes]]
        """
        Save current open window arrangement.
        """
        workflow = self.__workflow  # type: Scheme
        state = []
        for node in workflow.nodes:  # type: SchemeNode
            item = self.__item_for_node.get(node, None)
            if item is None:
                continue
            stackorder = item.activation_order
            if item.widget is not None and not item.widget.isHidden():
                data = self.save_widget_geometry(node, item.widget)
                state.append((stackorder, node, data))

        state = [(node, data)
                 for _, node, data in sorted(state, key=lambda t: t[0])]
        return state

    def restore_window_state(self, state):
        # type: (List[Tuple[SchemeNode, bytes]]) -> None
        """
        Restore the window state.
        """
        workflow = self.__workflow  # type: Scheme
        visible = {node for node, _ in state}
        # first hide all other widgets
        for node in workflow.nodes:
            if node not in visible:
                # avoid creating widgets if not needed
                item = self.__item_for_node.get(node, None)
                if item is not None and item.widget is not None:
                    item.widget.hide()
        allnodes = set(workflow.nodes)
        # restore state for visible group; windows are stacked as they appear
        # in the state list.
        w = None
        for node, state in filter(lambda t: t[0] in allnodes, state):
            w = self.widget_for_node(node)  # also create it if needed
            if w is not None:
                w.show()
                self.restore_widget_geometry(node, w, state)
                w.raise_()
                self.__mark_activated(w)

        # activate (give focus to) the last window
        if w is not None:
            w.activateWindow()

    def save_widget_geometry(self, node, widget):
        # type: (SchemeNode, QWidget) -> bytes
        """
        Save and return the current geometry and state for node.
        """
        return b''

    def restore_widget_geometry(self, node, widget, state):
        # type: (SchemeNode, QWidget, bytes) -> bool
        """
        Restore the widget geometry and state for node.

        Return True if the geometry was restored successfully.

        The default implementation does nothing.
        """
        return False

    def __raise_and_activate(self, items):
        # type: (Iterable[Item]) -> None
        """Show and raise a set of widgets."""
        # preserve the tracked stacking order
        items = sorted(items, key=lambda item: item.activation_order)
        w = None
        for item in items:
            if item.widget is not None:
                w = item.widget
            elif item.errorwidget is not None:
                w = item.errorwidget
            else:
                continue
            w.show()
            w.raise_()
        if w is not None:
            # give focus to the last activated top window
            w.activateWindow()

    def __activate_widget_for_node(self, node):  # type: (SchemeNode) -> None
        # activate the widget for the node.
        self.ensure_created(node)
        item = self.__item_for_node.get(node)
        if item is None:
            return
        if item.widget is not None:
            self.activate_widget_for_node(node, item.widget)
        elif item.errorwidget is not None:
            item.errorwidget.show()
            item.errorwidget.raise_()
            item.errorwidget.activateWindow()

    def __on_activate_parent(self):
        event = WorkflowEvent(WorkflowEvent.ActivateParentRequest)
        QCoreApplication.sendEvent(self.scheme(), event)

    def eventFilter(self, recv, event):
        # type: (QObject, QEvent) -> bool
        if event.type() == NodeEvent.NodeActivateRequest \
                and isinstance(recv, SchemeNode):
            self.__activate_widget_for_node(recv)
        return False

    def __set_float_on_top_flag(self, widget):
        """Set or unset widget's float on top flag"""
        should_float_on_top = self.__float_widgets_on_top
        float_on_top = bool(widget.windowFlags() & Qt.WindowStaysOnTopHint)

        if float_on_top == should_float_on_top:
            return

        widget_was_visible = widget.isVisible()
        if should_float_on_top:
            widget.setWindowFlags(
                widget.windowFlags() | Qt.WindowStaysOnTopHint)
        else:
            widget.setWindowFlags(
                widget.windowFlags() & ~Qt.WindowStaysOnTopHint)

        # Changing window flags hid the widget
        if widget_was_visible:
            widget.show()

    def __on_env_changed(self, key, newvalue, oldvalue):
        # Notify widgets of a runtime environment change
        for item in self.__item_for_node.values():
            if item.widget is not None:
                ev = WorkflowEnvChanged(key, newvalue, oldvalue)
                QCoreApplication.sendEvent(item.widget, ev)

    def actions_for_context_menu(self, node):
        # type: (SchemeNode) -> List[QAction]
        """
        Return a list of extra actions that can be inserted into context
        menu in the workflow editor.

        Subclasses can reimplement this method to extend the default context
        menu.

        Parameters
        ----------
        node: SchemeNode
            The node for which the context menu is requested.

        Return
        ------
        actions: List[QAction]
            Actions that are appended to the default menu.
        """
        return []
예제 #8
0
class WidgetManager(QObject):
    """
    OWWidget instance manager class.

    This class handles the lifetime of OWWidget instances in a
    :class:`WidgetsScheme`.

    """
    #: A new OWWidget was created and added by the manager.
    widget_for_node_added = Signal(SchemeNode, QWidget)

    #: An OWWidget was removed, hidden and will be deleted when appropriate.
    widget_for_node_removed = Signal(SchemeNode, QWidget)

    class ProcessingState(enum.IntEnum):
        """Widget processing state flags"""
        #: Signal manager is updating/setting the widget's inputs
        InputUpdate = 1
        #: Widget has entered a blocking state (OWWidget.isBlocking)
        BlockingUpdate = 2
        #: Widget has entered processing state
        ProcessingUpdate = 4
        #: Widget is still in the process of initialization
        Initializing = 8

    InputUpdate, BlockingUpdate, ProcessingUpdate, Initializing = ProcessingState

    #: State mask for widgets that cannot be deleted immediately
    #: (see __try_delete)
    _DelayDeleteMask = InputUpdate | BlockingUpdate

    #: Widget initialization states
    Delayed = namedtuple("Delayed", ["node"])
    PartiallyInitialized = namedtuple("Materializing",
                                      ["node", "partially_initialized_widget"])
    Materialized = namedtuple("Materialized", ["node", "widget"])

    class CreationPolicy(enum.Enum):
        """Widget Creation Policy"""
        #: Widgets are scheduled to be created from the event loop, or when
        #: first accessed with `widget_for_node`
        Normal = "Normal"
        #: Widgets are created immediately when added to the workflow model
        Immediate = "Immediate"
        #: Widgets are created only when first accessed with `widget_for_node`
        OnDemand = "OnDemand"

    Normal, Immediate, OnDemand = CreationPolicy

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.__scheme = None
        self.__signal_manager = None
        self.__widgets = []
        self.__initstate_for_node = {}
        self.__creation_policy = WidgetManager.Normal
        #: a queue of all nodes whose widgets are scheduled for
        #: creation/initialization
        self.__init_queue = deque()  # type: Deque[SchemeNode]
        #: Timer for scheduling widget initialization
        self.__init_timer = QTimer(self, interval=0, singleShot=True)
        self.__init_timer.timeout.connect(self.__create_delayed)

        #: A mapping of SchemeNode -> OWWidget (note: a mapping is only added
        #: after the widget is actually created)
        self.__widget_for_node = {}
        #: a mapping of OWWidget -> SchemeNode
        self.__node_for_widget = {}

        # Widgets that were 'removed' from the scheme but were at
        # the time in an input update loop and could not be deleted
        # immediately
        self.__delay_delete = set()

        #: processing state flags for all widgets (including the ones
        #: in __delay_delete).
        #: Note: widgets which have not yet been created do not have an entry
        self.__widget_processing_state = {}

        # Tracks the widget in the update loop by the SignalManager
        self.__updating_widget = None

        # Widgets float above other windows
        self.__float_widgets_on_top = False
        if hasattr(qApp, "applicationStateChanged"):
            # disables/enables widget floating when app (de)activates
            # available in Qt >= 5.2
            def reapply_float_on_top():
                self.set_float_widgets_on_top(self.__float_widgets_on_top)

            qApp.applicationStateChanged.connect(reapply_float_on_top)

    def set_scheme(self, scheme):
        """
        Set the :class:`WidgetsScheme` instance to manage.
        """
        self.__scheme = scheme
        self.__signal_manager = scheme.findChild(SignalManager)

        self.__signal_manager.processingStarted[SchemeNode].connect(
            self.__on_processing_started)
        self.__signal_manager.processingFinished[SchemeNode].connect(
            self.__on_processing_finished)
        scheme.node_added.connect(self.add_widget_for_node)
        scheme.node_removed.connect(self.remove_widget_for_node)
        scheme.runtime_env_changed.connect(self.__on_env_changed)
        scheme.installEventFilter(self)

    def scheme(self):
        """
        Return the scheme instance on which this manager is installed.
        """
        return self.__scheme

    def signal_manager(self):
        """
        Return the signal manager in use on the :func:`scheme`.
        """
        return self.__signal_manager

    def widget_for_node(self, node):
        """
        Return the OWWidget instance for the scheme node.
        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Delayed):
            # Create the widget now if it is still pending
            state = self.__materialize(state)
            return state.widget
        elif isinstance(state, WidgetManager.PartiallyInitialized):
            widget = state.partially_initialized_widget
            log.warning(
                "WidgetManager.widget_for_node: "
                "Accessing a partially created widget instance. "
                "This is most likely a result of explicit "
                "QApplication.processEvents call from the '%s.%s' "
                "widgets __init__.",
                type(widget).__module__,
                type(widget).__name__)
            return widget
        elif isinstance(state, WidgetManager.Materialized):
            return state.widget
        else:
            assert False

    def node_for_widget(self, widget):
        """
        Return the SchemeNode instance for the OWWidget.

        Raise a KeyError if the widget does not map to a node in the scheme.
        """
        return self.__node_for_widget[widget]

    def widget_properties(self, node):
        """
        Return the current widget properties/settings.

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        settings : dict
        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Materialized):
            return state.widget.settingsHandler.pack_data(state.widget)
        else:
            return node.properties

    def set_creation_policy(self, policy):
        """
        Set the widget creation policy

        Parameters
        ----------
        policy : WidgetManager.CreationPolicy
        """
        if self.__creation_policy != policy:
            self.__creation_policy = policy

            if self.__creation_policy == WidgetManager.Immediate:
                self.__init_timer.stop()
                while self.__init_queue:
                    state = self.__init_queue.popleft()
                    self.__materialize(state)
            elif self.__creation_policy == WidgetManager.Normal:
                if not self.__init_timer.isActive() and self.__init_queue:
                    self.__init_timer.start()
            elif self.__creation_policy == WidgetManager.OnDemand:
                self.__init_timer.stop()
            else:
                assert False

    def creation_policy(self):
        """
        Return the current widget creation policy

        Returns
        -------
        policy: WidgetManager.CreationPolicy
        """
        return self.__creation_policy

    def add_widget_for_node(self, node):
        """
        Create a new OWWidget instance for the corresponding scheme node.
        """
        state = WidgetManager.Delayed(node)
        self.__initstate_for_node[node] = state

        if self.__creation_policy == WidgetManager.Immediate:
            self.__initstate_for_node[node] = self.__materialize(state)
        elif self.__creation_policy == WidgetManager.Normal:
            self.__init_queue.append(state)
            if not self.__init_timer.isActive():
                self.__init_timer.start()
        elif self.__creation_policy == WidgetManager.OnDemand:
            self.__init_queue.append(state)

    def __materialize(self, state):
        # Create and initialize an OWWidget for a Delayed
        # widget initialization
        assert isinstance(state, WidgetManager.Delayed)
        if state in self.__init_queue:
            self.__init_queue.remove(state)

        node = state.node

        widget = self.create_widget_instance(node)

        self.__widgets.append(widget)
        self.__widget_for_node[node] = widget
        self.__node_for_widget[widget] = node

        self.__initialize_widget_state(node, widget)

        state = WidgetManager.Materialized(node, widget)
        self.__initstate_for_node[node] = state
        self.widget_for_node_added.emit(node, widget)

        return state

    def remove_widget_for_node(self, node):
        """
        Remove the OWWidget instance for node.
        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Delayed):
            del self.__initstate_for_node[node]
            self.__init_queue.remove(state)
        elif isinstance(state, WidgetManager.Materialized):
            # Update the node's stored settings/properties dict before
            # removing the widget.
            # TODO: Update/sync whenever the widget settings change.
            node.properties = self._widget_settings(state.widget)
            self.__widgets.remove(state.widget)
            del self.__initstate_for_node[node]
            del self.__widget_for_node[node]
            del self.__node_for_widget[state.widget]
            node.title_changed.disconnect(state.widget.setCaption)
            state.widget.progressBarValueChanged.disconnect(node.set_progress)
            del state.widget._Report__report_view
            self.widget_for_node_removed.emit(node, state.widget)
            self._delete_widget(state.widget)
        elif isinstance(state, WidgetManager.PartiallyInitialized):
            widget = state.partially_initialized_widget
            raise RuntimeError(
                "A widget/node {} was removed while being initialized. "
                "This is most likely a result of an explicit "
                "QApplication.processEvents call from the '{}.{}' "
                "widgets __init__.\n".format(state.node.title,
                                             type(widget).__module__,
                                             type(widget).__init__))

    def _widget_settings(self, widget):
        return widget.settingsHandler.pack_data(widget)

    def _delete_widget(self, widget):
        """
        Delete the OWBaseWidget instance.
        """
        widget.close()
        # Save settings to user global settings.
        widget.saveSettings()
        # Notify the widget it will be deleted.
        widget.onDeleteWidget()

        state = self.__widget_processing_state[widget]
        if state & WidgetManager._DelayDeleteMask:
            # If the widget is in an update loop and/or blocking we
            # delay the scheduled deletion until the widget is done.
            log.debug(
                "Widget %s removed but still in state :%s. "
                "Deferring deletion.", widget, state)
            self.__delay_delete.add(widget)
        else:
            widget.deleteLater()
            del self.__widget_processing_state[widget]

    def create_widget_instance(self, node):
        """
        Create a OWWidget instance for the node.
        """
        desc = node.description
        klass = widget = None
        initialized = False
        error = None
        # First try to actually retrieve the class.
        try:
            klass = name_lookup(desc.qualified_name)
        except (ImportError, AttributeError):
            sys.excepthook(*sys.exc_info())
            error = "Could not import {0!r}\n\n{1}".format(
                node.description.qualified_name, traceback.format_exc())
        except Exception:
            sys.excepthook(*sys.exc_info())
            error = "An unexpected error during import of {0!r}\n\n{1}".format(
                node.description.qualified_name, traceback.format_exc())

        if klass is None:
            widget = mock_error_owwidget(node, error)
            initialized = True

        if widget is None:
            log.info("WidgetManager: Creating '%s.%s' instance '%s'.",
                     klass.__module__, klass.__name__, node.title)

            widget = klass.__new__(
                klass,
                None,
                captionTitle=node.title,
                signal_manager=self.signal_manager(),
                stored_settings=node.properties,
                # NOTE: env is a view of the real env and reflects
                # changes to the environment.
                env=self.scheme().runtime_env())
            initialized = False

        # Init the node/widget mapping and state before calling __init__
        # Some OWWidgets might already send data in the constructor
        # (should this be forbidden? Raise a warning?) triggering the signal
        # manager which would request the widget => node mapping or state
        # Furthermore they can (though they REALLY REALLY REALLY should not)
        # explicitly call qApp.processEvents.
        assert node not in self.__widget_for_node
        self.__widget_for_node[node] = widget
        self.__node_for_widget[widget] = node
        self.__widget_processing_state[widget] = WidgetManager.Initializing
        self.__initstate_for_node[node] = \
            WidgetManager.PartiallyInitialized(node, widget)

        if not initialized:
            try:
                widget.__init__()
            except Exception:
                sys.excepthook(*sys.exc_info())
                msg = traceback.format_exc()
                msg = "Could not create {0!r}\n\n{1}".format(
                    node.description.name, msg)
                # remove state tracking for widget ...
                del self.__widget_for_node[node]
                del self.__node_for_widget[widget]
                del self.__widget_processing_state[widget]

                # ... and substitute it with a mock error widget.
                widget = mock_error_owwidget(node, msg)
                self.__widget_for_node[node] = widget
                self.__node_for_widget[widget] = node
                self.__widget_processing_state[widget] = 0
                self.__initstate_for_node[node] = \
                    WidgetManager.Materialized(node, widget)

        self.__initstate_for_node[node] = \
            WidgetManager.Materialized(node, widget)
        # Clear Initializing flag
        self.__widget_processing_state[widget] &= ~WidgetManager.Initializing

        node.title_changed.connect(widget.setCaption)

        # Widget's info/warning/error messages.
        widget.messageActivated.connect(self.__on_widget_state_changed)
        widget.messageDeactivated.connect(self.__on_widget_state_changed)

        # Widget's statusTip
        node.set_status_message(widget.statusMessage())
        widget.statusMessageChanged.connect(node.set_status_message)

        # Widget's progress bar value state.
        widget.progressBarValueChanged.connect(node.set_progress)

        # Widget processing state (progressBarInit/Finished)
        # and the blocking state.
        widget.processingStateChanged.connect(
            self.__on_processing_state_changed)
        widget.blockingStateChanged.connect(self.__on_blocking_state_changed)

        if widget.isBlocking():
            # A widget can already enter blocking state in __init__
            self.__widget_processing_state[widget] |= self.BlockingUpdate

        if widget.processingState != 0:
            # It can also start processing (initialization of resources, ...)
            self.__widget_processing_state[widget] |= self.ProcessingUpdate
            node.set_processing_state(1)
            node.set_progress(widget.progressBarValue)

        # Install a help shortcut on the widget
        help_action = widget.findChild(QAction, "action-help")
        if help_action is not None:
            help_action.setEnabled(True)
            help_action.setVisible(True)
            help_action.triggered.connect(self.__on_help_request)

        # Up shortcut (activate/open parent)
        up_shortcut = QShortcut(QKeySequence(Qt.ControlModifier + Qt.Key_Up),
                                widget)
        up_shortcut.activated.connect(self.__on_activate_parent)

        # Call setters only after initialization.
        widget.setWindowIcon(icon_loader.from_description(desc).get(desc.icon))
        widget.setCaption(node.title)
        # befriend class Report
        widget._Report__report_view = self.scheme().report_view

        self.__set_float_on_top_flag(widget)

        # Schedule an update with the signal manager, due to the cleared
        # implicit Initializing flag
        self.signal_manager()._update()

        return widget

    def node_processing_state(self, node):
        """
        Return the processing state flags for the node.

        Same as `manager.widget_processing_state(manger.widget_for_node(node))`

        """
        state = self.__initstate_for_node[node]
        if isinstance(state, WidgetManager.Materialized):
            return self.__widget_processing_state[state.widget]
        elif isinstance(state, WidgetManager.PartiallyInitialized):
            return self.__widget_processing_state[
                state.partially_initialized_widget]
        else:
            return WidgetManager.Initializing

    def widget_processing_state(self, widget):
        """
        Return the processing state flags for the widget.

        The state is an bitwise or of `InputUpdate` and `BlockingUpdate`.

        """
        return self.__widget_processing_state[widget]

    def set_float_widgets_on_top(self, float_on_top):
        """
        Set `Float Widgets on Top` flag on all widgets.
        """
        self.__float_widgets_on_top = float_on_top

        for widget in self.__widget_for_node.values():
            self.__set_float_on_top_flag(widget)

    def __create_delayed(self):
        if self.__init_queue:
            state = self.__init_queue.popleft()
            node = state.node
            self.__initstate_for_node[node] = self.__materialize(state)

        if self.__creation_policy == WidgetManager.Normal and \
                self.__init_queue:
            # restart the timer if pending widgets still in the queue
            self.__init_timer.start()

    def eventFilter(self, receiver, event):
        if event.type() == QEvent.Close and receiver is self.__scheme:
            self.signal_manager().stop()

            # Notify the widget instances.
            for widget in list(self.__widget_for_node.values()):
                widget.close()
                widget.saveSettings()
                widget.onDeleteWidget()
                widget.deleteLater()

        return QObject.eventFilter(self, receiver, event)

    def __on_help_request(self):
        """
        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to
        the scheme and hope someone responds to it.

        """
        # Sender is the QShortcut, and parent the OWBaseWidget
        widget = self.sender().parent()
        try:
            node = self.node_for_widget(widget)
        except KeyError:
            pass
        else:
            qualified_name = node.description.qualified_name
            help_url = "help://search?" + urlencode({"id": qualified_name})
            event = QWhatsThisClickedEvent(help_url)
            QCoreApplication.sendEvent(self.scheme(), event)

    def __on_activate_parent(self):
        """
        Activate parent shortcut was pressed.
        """
        event = ActivateParentEvent()
        QCoreApplication.sendEvent(self.scheme(), event)

    def __initialize_widget_state(self, node, widget):
        """
        Initialize the tracked info/warning/error message state.
        """
        for message_group in widget.message_groups:
            message = user_message_from_state(message_group)
            if message:
                node.set_state_message(message)

    def __on_widget_state_changed(self, msg):
        """
        The OWBaseWidget info/warning/error state has changed.
        """
        widget = msg.group.widget
        try:
            node = self.node_for_widget(widget)
        except KeyError:
            pass
        else:
            self.__initialize_widget_state(node, widget)

    def __on_processing_state_changed(self, state):
        """
        A widget processing state has changed (progressBarInit/Finished)
        """
        widget = self.sender()

        if state:
            self.__widget_processing_state[widget] |= self.ProcessingUpdate
        else:
            self.__widget_processing_state[widget] &= ~self.ProcessingUpdate

        # propagate the change to the workflow model.
        try:
            # we can still track widget state after it was removed from the
            # workflow model (`__delay_delete`)
            node = self.node_for_widget(widget)
        except KeyError:
            pass
        else:
            self.__update_node_processing_state(node)

    def __on_processing_started(self, node):
        """
        Signal manager entered the input update loop for the node.
        """
        widget = self.widget_for_node(node)
        # Remember the widget instance. The node and the node->widget mapping
        # can be removed between this and __on_processing_finished.
        self.__updating_widget = widget
        self.__widget_processing_state[widget] |= self.InputUpdate
        self.__update_node_processing_state(node)

    def __on_processing_finished(self, node):
        """
        Signal manager exited the input update loop for the node.
        """
        widget = self.__updating_widget
        self.__widget_processing_state[widget] &= ~self.InputUpdate

        if widget in self.__node_for_widget:
            self.__update_node_processing_state(node)
        elif widget in self.__delay_delete:
            self.__try_delete(widget)
        else:
            raise ValueError("%r is not managed" % widget)

        self.__updating_widget = None

    def __on_blocking_state_changed(self, state):
        """
        OWWidget blocking state has changed.
        """
        if not state:
            # schedule an update pass.
            self.signal_manager()._update()

        widget = self.sender()
        if state:
            self.__widget_processing_state[widget] |= self.BlockingUpdate
        else:
            self.__widget_processing_state[widget] &= ~self.BlockingUpdate

        if widget in self.__node_for_widget:
            node = self.node_for_widget(widget)
            self.__update_node_processing_state(node)

        elif widget in self.__delay_delete:
            self.__try_delete(widget)

    def __update_node_processing_state(self, node):
        """
        Update the `node.processing_state` to reflect the widget state.
        """
        state = self.node_processing_state(node)
        node.set_processing_state(1 if state else 0)

    def __try_delete(self, widget):
        if not (self.__widget_processing_state[widget]
                & WidgetManager._DelayDeleteMask):
            log.debug("Delayed delete for widget %s", widget)
            self.__delay_delete.remove(widget)
            del self.__widget_processing_state[widget]
            widget.blockingStateChanged.disconnect(
                self.__on_blocking_state_changed)
            widget.processingStateChanged.disconnect(
                self.__on_processing_state_changed)
            widget.deleteLater()

    def __on_env_changed(self, key, newvalue, oldvalue):
        # Notify widgets of a runtime environment change
        for widget in self.__widget_for_node.values():
            widget.workflowEnvChanged(key, newvalue, oldvalue)

    def __set_float_on_top_flag(self, widget):
        """Set or unset widget's float on top flag"""
        should_float_on_top = self.__float_widgets_on_top
        if hasattr(qApp, "applicationState"):
            # only float on top when the application is active
            # available in Qt >= 5.2
            should_float_on_top &= qApp.applicationState(
            ) == Qt.ApplicationActive
        float_on_top = widget.windowFlags() & Qt.WindowStaysOnTopHint

        if float_on_top == should_float_on_top:
            return

        widget_was_visible = widget.isVisible()
        if should_float_on_top:
            widget.setWindowFlags(Qt.WindowStaysOnTopHint)
        else:
            widget.setWindowFlags(widget.windowFlags()
                                  & ~Qt.WindowStaysOnTopHint)

        # Changing window flags hid the widget
        if widget_was_visible:
            widget.show()
예제 #9
0
class RotaryEncoderModuleGUI(RotaryEncoderModule, BaseWidget):

    TITLE = 'Rotary encoder module'

    def __init__(self, parent_win=None):
        BaseWidget.__init__(self, self.TITLE, parent_win=parent_win)
        RotaryEncoderModule.__init__(self)

        self._port = ControlCombo(
            'Serial port', changed_event=self.__combo_serial_ports_changed_evt)
        self._refresh_serial_ports = ControlButton(
            '',
            icon=QtGui.QIcon(conf.REFRESH_SMALL_ICON),
            default=self.__refresh_serial_ports_btn_pressed,
            helptext="Press here to refresh the list of available devices.")

        self._connect_btn = ControlButton('Connect', checkable=True)

        self._filename = ControlText('Stream Filename', '')
        self._saveas_btn = ControlButton('Save As...')

        self._events = ControlCheckBox('Enable events')
        self._output_stream = ControlCheckBox('Output stream')
        self._stream = ControlCheckBox('Stream data')
        self._stream_file = ControlCheckBox('Stream to file')
        self._zero_btn = ControlButton('Reset position')
        self._start_reading = ControlButton('Start Reading')
        self._reset_threshs = ControlButton('Reset thresholds')
        self._thresh_lower = ControlNumber('Lower threshold (deg)',
                                           0,
                                           minimum=-360,
                                           maximum=360)
        self._thresh_upper = ControlNumber('Upper threshold (deg)',
                                           0,
                                           minimum=-360,
                                           maximum=360)
        self._graph = ControlMatplotlib('Value')
        self._clear_btn = ControlButton('Clear')

        self.set_margin(10)

        self.formset = [('_port', '_refresh_serial_ports', '_connect_btn'),
                        ('_filename', '_saveas_btn'),
                        ('_events', '_output_stream', '_stream',
                         '_stream_file', '_zero_btn'), '_start_reading',
                        ('_thresh_lower', '_thresh_upper', '_reset_threshs'),
                        '=', '_graph', '_clear_btn']

        self._stream.enabled = False
        self._stream_file.enabled = False
        self._events.enabled = False
        self._output_stream.enabled = False
        self._zero_btn.enabled = False
        self._reset_threshs.enabled = False
        self._thresh_lower.enabled = False
        self._thresh_upper.enabled = False
        self._start_reading.enabled = False

        self._connect_btn.value = self.__toggle_connection_evt
        self._saveas_btn.value = self.__prompt_savig_evt
        self._stream_file.changed_event = self.__stream_file_changed_evt
        self._events.changed_event = self.__events_changed_evt
        self._output_stream.changed_event = self.__output_stream_changed_evt
        self._thresh_upper.changed_event = self.__thresh_evt
        self._thresh_lower.changed_event = self.__thresh_evt
        self._reset_threshs.value = self.__reset_thresholds_evt
        self._zero_btn.value = self.__zero_btn_evt
        self._start_reading.value = self.__start_reading_evt
        self._graph.on_draw = self.__on_draw_evt
        self._clear_btn.value = self.__clear_btn_evt
        self._filename.changed_event = self.__filename_changed_evt

        self.history_x = []
        self.history_y = []

        self._timer = QTimer()
        self._timer.timeout.connect(self.__update_readings)

        self._fill_serial_ports()

    def _fill_serial_ports(self):
        self._port.add_item('', '')
        for n, port in enumerate(sorted(serial.tools.list_ports.comports()),
                                 1):
            self._port.add_item("{device}".format(device=port.device),
                                str(port.device))

    def __filename_changed_evt(self):
        if not self._filename.value:
            self._stream_file.value = False
            self._stream_file.enabled = False

    def __prompt_savig_evt(self):
        '''
        Opens a window for user to select where to save the csv file
        '''
        self._filename.value, _ = QFileDialog.getSaveFileName()
        if self._filename.value:
            self._stream_file.enabled = True
        else:
            self._stream_file.value = False
            self._stream_file.enabled = False

    def __stream_file_changed_evt(self):
        '''
        User wants to store rotary encoder measurements in a CSV file. Create it
        '''
        if self._stream_file.value is True:
            self._csvfile = open(self._filename.value, 'w')
            self._csvwriter = csv.writer(
                self._csvfile,
                def_text=
                'This file has all the rotary encoder data recorded during a PyBpod session.',
                columns_headers=['PC_TIME', 'DATA_TYPE', 'EVT_TIME', 'VALUE'
                                 ])  # Check if we need something else after

    def __start_reading_evt(self):
        '''
        Toggle timer
        '''
        if self._timer.isActive():
            self.disable_stream()
            self._start_reading.label = 'Start Reading'
            self._timer.stop()
        else:
            self.enable_stream()
            self.history_x = []
            self.history_y = []
            self._start_reading.label = 'Stop Reading'
            self._timer.start(30)

    def __clear_btn_evt(self):
        '''
        Clear recorded data
        '''
        self.history_x = []
        self.history_y = []
        self._graph.draw()

    def __on_draw_evt(self, figure):
        '''
        The actual draw function. Pick just the last 200 measurements in order to avoid app freezing
        '''
        axes = figure.add_subplot(111)
        axes.clear()
        totallen = len(self.history_x)
        if totallen > 200:
            x = self.history_x[totallen - 201:]
            y = self.history_y[totallen - 201:]
            axes.plot(x, y)
            if len(x) >= 2:
                x_range = [x[0], x[-1]]
                axes.plot(x_range,
                          [self._thresh_upper.value, self._thresh_upper.value],
                          linestyle='dotted',
                          color='red')
                axes.plot(x_range,
                          [self._thresh_lower.value, self._thresh_lower.value],
                          linestyle='dotted',
                          color='blue')
        else:
            axes.plot(self.history_x, self.history_y)
            if len(self.history_x) >= 2:
                x_range = [self.history_x[0], self.history_x[-1]]
                axes.plot(x_range,
                          [self._thresh_upper.value, self._thresh_upper.value],
                          linestyle='dotted',
                          color='red')
                axes.plot(x_range,
                          [self._thresh_lower.value, self._thresh_lower.value],
                          linestyle='dotted',
                          color='blue')

        self._graph.repaint()

    def __update_graph(self, readings):
        '''
        Add new data to the reading history and update the graph
        '''
        for data in readings:
            if data[0] == 'P':
                self.history_x.append(data[1])
                self.history_y.append(data[2])
        self._graph.draw()

    def __update_readings(self):
        '''
        Get new measurements and channel them to the graph or the file being written
        '''
        data = self.read_stream()

        if self._stream.value:
            self.__update_graph(data)
        if self._stream_file.value:
            self.__write_to_file(data)

    def __write_to_file(self, readings):
        '''
        Write new readings to the file
        '''
        now = datetime_now.now()
        for data in readings:
            self._csvwriter.writerow([now.strftime('%Y%m%d%H%M%S')] + data)

    def __zero_btn_evt(self):
        self.set_zero_position()

    def __reset_thresholds_evt(self):
        self._thresh_lower.value = 0
        self._thresh_upper.value = 0

    def __thresh_evt(self):
        thresholds = [
            int(self._thresh_lower.value),
            int(self._thresh_upper.value)
        ]
        self.set_thresholds(thresholds)

    def __events_changed_evt(self):
        if self._stream.value:
            self.enable_evt_transmission()
        else:
            self.disable_evt_transmission()

    def __output_stream_changed_evt(self):
        if self._stream.value:
            self.enable_module_outputstream()
        else:
            self.disable_module_outputstream()

    def __toggle_connection_evt(self):
        if not self._connect_btn.checked:
            if hasattr(self, 'arcom'):
                self.disable_stream()
                self._timer.stop()
                self.close()
            self._connect_btn.label = 'Connect'
            self._stream.enabled = False
            self._events.enabled = False
            self._output_stream.enabled = False
            self._zero_btn.enabled = False
            self._reset_threshs.enabled = False
            self._thresh_lower.enabled = False
            self._thresh_upper.enabled = False
            self._start_reading.enabled = False
            self._stream_file.enabled = False

            self._port.enabled = True
            self._refresh_serial_ports.enabled = True
        else:
            try:
                self.open(self._port.value)

                self._connect_btn.label = 'Disconnect'
                self._stream.enabled = True
                self._events.enabled = True
                self._output_stream.enabled = True
                self._zero_btn.enabled = True
                self._reset_threshs.enabled = True
                self._thresh_lower.enabled = True
                self._thresh_upper.enabled = True
                self._start_reading.enabled = True

                self._port.enabled = False
                self._refresh_serial_ports.enabled = False

                if self._filename.value:
                    self._stream_file.enabled = True
                else:
                    self._stream_file.value = False
                    self._stream_file.enabled = False
            except Exception as err:
                self.critical(str(err), "Error")
                self._connect_btn.checked = False

    def __combo_serial_ports_changed_evt(self):
        self._connect_btn.enabled = True

    def __refresh_serial_ports_btn_pressed(self):
        tmp = self._port.value
        self._port.clear()
        self._fill_serial_ports()
        self._port.value = tmp
예제 #10
0
class SignalManager(QObject):
    """
    SignalManager handles the runtime signal propagation for a :class:`.Scheme`
    instance.

    Note
    ----
    If a scheme instance is passed as a parent to the constructor it is also
    set as the workflow model.
    """
    class State(enum.IntEnum):
        """
        SignalManager state flags.

        .. seealso:: :func:`SignalManager.state()`
        """
        #: The manager is running, i.e. it propagates signals
        Running = 0
        #: The manager is stopped. It does not track node output changes,
        #: and does not deliver signals to dependent nodes
        Stopped = 1
        #: The manager is paused. It still tracks node output changes, but
        #: does not deliver new signals to dependent nodes. The pending signals
        #: will be delivered once it enters Running state again
        Paused = 2

    #: The manager is running, i.e. it propagates signals
    Running = State.Running
    #: The manager is stopped. It does not track node ouput changes,
    #: and does not deliver signals to dependent nodes
    Stopped = State.Stopped
    #: The manager is paused. It still tracks node output changes, but
    #: does not deliver new signals to dependent nodes. The pending signals
    #: will be delivered once it enters Running state again
    Paused = State.Paused

    # unused; back-compatibility
    Error = 3

    class RuntimeState(enum.IntEnum):
        """
        SignalManager runtime state.

        See Also
        --------
        SignalManager.runtime_state
        """
        #: Waiting, idle state. The signal queue is empty
        Waiting = 0
        #: ...
        Processing = 1

    Waiting = RuntimeState.Waiting
    Processing = RuntimeState.Processing

    #: Emitted when the state of the signal manager changes.
    stateChanged = pyqtSignal(int)
    #: Emitted when signals are added to the queue.
    updatesPending = pyqtSignal()
    #: Emitted right before a `SchemeNode` instance has its inputs updated.
    processingStarted = pyqtSignal([], [SchemeNode])
    #: Emitted right after a `SchemeNode` instance has had its inputs updated.
    processingFinished = pyqtSignal([], [SchemeNode])
    #: Emitted when `SignalManager`'s runtime state changes.
    runtimeStateChanged = pyqtSignal(int)

    def __init__(self, parent=None, *, max_running=None, **kwargs):
        # type: (Optional[QObject], Optional[int], Any) -> None
        super().__init__(parent, **kwargs)
        self.__workflow = None  # type: Optional[Scheme]
        self.__input_queue = []  # type: List[Signal]

        # mapping a node to its current outputs
        self.__node_outputs = {
        }  # type: Dict[SchemeNode, DefaultDict[OutputSignal, _OutputState]]

        self.__state = SignalManager.Running
        self.__runtime_state = SignalManager.Waiting

        self.__update_timer = QTimer(self, interval=100, singleShot=True)
        self.__update_timer.timeout.connect(self.__process_next)
        self.__max_running = max_running
        if isinstance(parent, Scheme):
            self.set_workflow(parent)

    def _can_process(self):  # type: () -> bool
        """
        Return a bool indicating if the manger can enter the main
        processing loop.

        """
        return self.__state not in [SignalManager.Error, SignalManager.Stopped]

    def workflow(self):
        # type: () -> Optional[Scheme]
        """
        Return the :class:`Scheme` instance.
        """
        return self.__workflow

    #: Alias
    scheme = workflow

    def set_workflow(self, workflow):
        # type: (Scheme) -> None
        """
        Set the workflow model.

        Parameters
        ----------
        workflow : Scheme
        """
        if workflow is self.__workflow:
            return

        if self.__workflow is not None:
            for node in self.__workflow.nodes:
                node.state_changed.disconnect(self._update)
            for link in self.__workflow.links:
                link.enabled_changed.disconnect(self.__on_link_enabled_changed)

            self.__workflow.node_added.disconnect(self.__on_node_added)
            self.__workflow.node_removed.disconnect(self.__on_node_removed)
            self.__workflow.link_added.disconnect(self.__on_link_added)
            self.__workflow.link_removed.disconnect(self.__on_link_removed)
            self.__workflow.removeEventFilter(self)
            self.__node_outputs = {}
            self.__input_queue = []

        self.__workflow = workflow

        if workflow is not None:
            workflow.node_added.connect(self.__on_node_added)
            workflow.node_removed.connect(self.__on_node_removed)
            workflow.link_added.connect(self.__on_link_added)
            workflow.link_removed.connect(self.__on_link_removed)
            for node in workflow.nodes:
                self.__node_outputs[node] = defaultdict(_OutputState)
                node.state_changed.connect(self._update)

            for link in workflow.links:
                link.enabled_changed.connect(self.__on_link_enabled_changed)
            workflow.installEventFilter(self)

    def has_pending(self):  # type: () -> bool
        """
        Does the manager have any signals to deliver?
        """
        return bool(self.__input_queue)

    def start(self):  # type: () -> None
        """
        Start the update loop.

        Note
        ----
        The updates will not happen until the control reaches the Qt event
        loop.
        """
        if self.__state != SignalManager.Running:
            self.__state = SignalManager.Running
            self.stateChanged.emit(SignalManager.Running)
            self._update()

    def stop(self):  # type: () -> None
        """
        Stop the update loop.

        Note
        ----
        If the `SignalManager` is currently in `process_queues` it will
        still update all current pending signals, but will not re-enter
        until `start()` is called again.
        """
        if self.__state != SignalManager.Stopped:
            self.__state = SignalManager.Stopped
            self.stateChanged.emit(SignalManager.Stopped)
            self.__update_timer.stop()

    def pause(self):  # type: () -> None
        """
        Pause the delivery of signals.
        """
        if self.__state != SignalManager.Paused:
            self.__state = SignalManager.Paused
            self.stateChanged.emit(SignalManager.Paused)
            self.__update_timer.stop()

    def resume(self):
        # type: () -> None
        """
        Resume the delivery of signals.
        """
        if self.__state == SignalManager.Paused:
            self.__state = SignalManager.Running
            self.stateChanged.emit(self.__state)
            self._update()

    def step(self):
        # type: () -> None
        """
        Deliver signals to a single node (only applicable while the `state()`
        is `Paused`).
        """
        if self.__state == SignalManager.Paused:
            self.process_queued()

    def state(self):
        # type: () -> State
        """
        Return the current state.

        Return
        ------
        state : SignalManager.State
        """
        return self.__state

    def _set_runtime_state(self, state):
        # type: (Union[RuntimeState, int]) -> None
        """
        Set the runtime state.

        Should only be called by `SignalManager` implementations.
        """
        if self.__runtime_state != state:
            self.__runtime_state = state
            self.runtimeStateChanged.emit(self.__runtime_state)

    def runtime_state(self):
        # type: () -> RuntimeState
        """
        Return the runtime state. This can be `SignalManager.Waiting`
        or `SignalManager.Processing`.

        """
        return self.__runtime_state

    def __on_node_removed(self, node):
        # type: (SchemeNode) -> None
        # remove all pending input signals for node so we don't get
        # stale references in process_node.
        # NOTE: This does not remove output signals for this node. In
        # particular the final 'None' will be delivered to the sink
        # nodes even after the source node is no longer in the scheme.
        log.info("Removing pending signals for '%s'.", node.title)
        self.remove_pending_signals(node)

        del self.__node_outputs[node]
        node.state_changed.disconnect(self._update)

    def __on_node_added(self, node):
        # type: (SchemeNode) -> None
        self.__node_outputs[node] = defaultdict(_OutputState)
        # schedule update pass on state change
        node.state_changed.connect(self._update)

    def __on_link_added(self, link):
        # type: (SchemeLink) -> None
        # push all current source values to the sink
        link.set_runtime_state(SchemeLink.Empty)
        state = self.__node_outputs[link.source_node][link.source_channel]
        link.set_runtime_state_flag(
            SchemeLink.Invalidated,
            bool(state.flags & _OutputState.Invalidated))
        if link.enabled:
            log.info("Scheduling signal data update for '%s'.", link)
            self._schedule(self.signals_on_link(link))
            self._update()

        link.enabled_changed.connect(self.__on_link_enabled_changed)

    def __on_link_removed(self, link):
        # type: (SchemeLink) -> None
        # purge all values in sink's queue
        log.info("Scheduling signal data purge (%s).", link)
        self.purge_link(link)
        link.enabled_changed.disconnect(self.__on_link_enabled_changed)

    def __on_link_enabled_changed(self, enabled):
        if enabled:
            link = self.sender()
            log.info("Link %s enabled. Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))

    def signals_on_link(self, link):
        # type: (SchemeLink) -> List[Signal]
        """
        Return :class:`Signal` instances representing the current values
        present on the `link`.
        """
        items = self.link_contents(link)
        signals = []

        for key, value in items.items():
            signals.append(Signal(link, value, key))

        return signals

    def link_contents(self, link):
        # type: (SchemeLink) -> Dict[Any, Any]
        """
        Return the contents on the `link`.
        """
        node, channel = link.source_node, link.source_channel

        if node in self.__node_outputs:
            return self.__node_outputs[node][channel].outputs
        else:
            # if the the node was already removed its tracked outputs in
            # __node_outputs are cleared, however the final 'None' signal
            # deliveries for the link are left in the _input_queue.
            pending = [sig for sig in self.__input_queue if sig.link is link]
            return {sig.id: sig.value for sig in pending}

    def send(self, node, channel, value, id):
        # type: (SchemeNode, OutputSignal, Any, Any) -> None
        """
        Send the `value` with `id` on an output `channel` from node.

        Schedule the signal delivery to all dependent nodes

        Parameters
        ----------
        node : SchemeNode
            The originating node.
        channel : OutputSignal
            The nodes output on which the value is sent.
        value : Any
            The value to send,
        id : Any
            Signal id.
        """
        if self.__workflow is None:
            raise RuntimeError("'send' called with no workflow!.")

        log.debug("%r sending %r (id: %r) on channel %r", node.title,
                  type(value), id, channel.name)

        scheme = self.__workflow

        state = self.__node_outputs[node][channel]
        state.outputs[id] = value

        # clear invalidated flag
        if state.flags & _OutputState.Invalidated:
            log.debug("%r clear invalidated flag on channel %r", node.title,
                      channel.name)
            state.flags &= ~_OutputState.Invalidated

        links = filter(
            is_enabled,
            scheme.find_links(source_node=node, source_channel=channel))
        signals = []
        for link in links:
            signals.append(Signal(link, value, id))
            link.set_runtime_state_flag(SchemeLink.Invalidated, False)

        self._schedule(signals)

    def invalidate(self, node, channel):
        # type: (SchemeNode, OutputSignal) -> None
        """
        Invalidate the `channel` on `node`.

        The channel is effectively considered changed but unavailable until
        a new value is sent via `send`. While this state is set the dependent
        nodes will not be updated.

        All links originating with this node/channel will be marked with
        `SchemeLink.Invalidated` flag until a new value is sent with `send`.

        Parameters
        ----------
        node: SchemeNode
            The originating node.
        channel: OutputSignal
            The channel to invalidate.


        .. versionadded:: 0.1.8
        """
        log.debug("%r invalidating channel %r", node.title, channel.name)
        self.__node_outputs[node][channel].flags |= _OutputState.Invalidated
        if self.__workflow is None:
            return
        links = self.__workflow.find_links(source_node=node,
                                           source_channel=channel)
        for link in links:
            link.set_runtime_state(link.runtime_state() | link.Invalidated)

    def purge_link(self, link):
        # type: (SchemeLink) -> None
        """
        Purge the link (send None for all ids currently present)
        """
        contents = self.link_contents(link)
        ids = contents.keys()
        signals = [Signal(link, None, id) for id in ids]

        self._schedule(signals)

    def _schedule(self, signals):
        # type: (List[Signal]) -> None
        """
        Schedule a list of :class:`Signal` for delivery.
        """
        self.__input_queue.extend(signals)

        for link in {sig.link for sig in signals}:
            # update the SchemeLink's runtime state flags
            contents = self.link_contents(link)
            if any(value is not None for value in contents.values()):
                state = SchemeLink.Active
            else:
                state = SchemeLink.Empty
            link.set_runtime_state(state | SchemeLink.Pending)

        for node in {sig.link.sink_node
                     for sig in signals}:  # type: SchemeNode
            # update the SchemeNodes's runtime state flags
            node.set_state_flags(SchemeNode.Pending, True)

        if signals:
            self.updatesPending.emit()

        self._update()

    def _update_link(self, link):
        # type: (SchemeLink) -> None
        """
        Schedule update of a single link.
        """
        signals = self.signals_on_link(link)
        self._schedule(signals)

    def process_queued(self, max_nodes=None):
        # type: (Any) -> None
        """
        Process queued signals.

        Take the first eligible node from the pending input queue and deliver
        all scheduled signals.
        """
        if not (max_nodes is None or max_nodes == 1):
            warnings.warn(
                "`max_nodes` is deprecated and will be removed in the future",
                FutureWarning,
                stacklevel=2)

        if self.__runtime_state == SignalManager.Processing:
            raise RuntimeError("Cannot re-enter 'process_queued'")

        if not self._can_process():
            raise RuntimeError("Can't process in state %i" % self.__state)

        self.process_next()

    def process_next(self):
        # type: () -> bool
        """
        Process queued signals.

        Take the first eligible node from the pending input queue and deliver
        all scheduled signals for it and return `True`.

        If no node is eligible for update do nothing and return `False`.
        """
        return self.__process_next_helper(use_max_active=False)

    def process_node(self, node):
        # type: (SchemeNode) -> None
        """
        Process pending input signals for `node`.
        """
        assert self.__runtime_state != SignalManager.Processing

        signals_in = self.pending_input_signals(node)
        self.remove_pending_signals(node)

        signals_in = self.compress_signals(signals_in)

        log.debug("Processing %r, sending %i signals.", node.title,
                  len(signals_in))
        # Clear the link's pending flag.
        for link in {sig.link for sig in signals_in}:
            link.set_runtime_state(link.runtime_state() & ~SchemeLink.Pending)

        def process_dynamic(signals):
            # type: (List[Signal]) -> List[Signal]
            """
            Process dynamic signals; Update the link's dynamic_enabled flag if
            the value is valid; replace values that do not type check with
            `None`
            """
            res = []
            for sig in signals:
                # Check and update the dynamic link state
                link = sig.link
                if sig.link.is_dynamic():
                    enabled = can_enable_dynamic(link, sig.value)
                    link.set_dynamic_enabled(enabled)
                    if not enabled:
                        # Send None instead (clear the link)
                        sig = Signal(link, None, sig.id)
                res.append(sig)
            return res

        signals_in = process_dynamic(signals_in)
        assert ({sig.link
                 for sig in self.__input_queue
                 }.intersection({sig.link
                                 for sig in signals_in}) == set([]))

        self._set_runtime_state(SignalManager.Processing)
        self.processingStarted.emit()
        self.processingStarted[SchemeNode].emit(node)
        try:
            self.send_to_node(node, signals_in)
        finally:
            node.set_state_flags(SchemeNode.Pending, False)
            self.processingFinished.emit()
            self.processingFinished[SchemeNode].emit(node)
            self._set_runtime_state(SignalManager.Waiting)

    def compress_signals(self, signals):
        # type: (List[Signal]) -> List[Signal]
        """
        Compress a list of :class:`Signal` instances to be delivered.

        Before the signal values are delivered to the sink node they can be
        optionally `compressed`, i.e. values can be merged or dropped
        depending on the execution semantics.

        The input list is in the order that the signals were enqueued.

        The base implementation returns the list unmodified.

        Parameters
        ----------
        signals : List[Signal]

        Return
        ------
        signals : List[Signal]
        """
        return signals

    def send_to_node(self, node, signals):
        # type: (SchemeNode, List[Signal]) -> None
        """
        Abstract. Reimplement in subclass.

        Send/notify the `node` instance (or whatever object/instance it is a
        representation of) that it has new inputs as represented by the
        `signals` list).

        Parameters
        ----------
        node : SchemeNode
        signals : List[Signal]
        """
        raise NotImplementedError

    def is_pending(self, node):
        # type: (SchemeNode) -> bool
        """
        Is `node` (class:`SchemeNode`) scheduled for processing (i.e.
        it has incoming pending signals).

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        pending : bool
        """
        return node in [signal.link.sink_node for signal in self.__input_queue]

    def pending_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of pending nodes.

        The nodes are returned in the order they were enqueued for
        signal delivery.

        Returns
        -------
        nodes : List[SchemeNode]
        """
        return list(unique(sig.link.sink_node for sig in self.__input_queue))

    def pending_input_signals(self, node):
        # type: (SchemeNode) -> List[Signal]
        """
        Return a list of pending input signals for node.
        """
        return [
            signal for signal in self.__input_queue
            if node is signal.link.sink_node
        ]

    def remove_pending_signals(self, node):
        # type: (SchemeNode) -> None
        """
        Remove pending signals for `node`.
        """
        for signal in self.pending_input_signals(node):
            try:
                self.__input_queue.remove(signal)
            except ValueError:
                pass

    def __nodes(self):
        # type: () -> Sequence[SchemeNode]
        return self.__workflow.nodes if self.__workflow else []

    def blocking_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of nodes in a blocking state.
        """
        return [node for node in self.__nodes() if self.is_blocking(node)]

    def invalidated_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of invalidated nodes.

        .. versionadded:: 0.1.8
        """
        return [
            node for node in self.__nodes()
            if self.has_invalidated_outputs(node) or self.is_invalidated(node)
        ]

    def active_nodes(self):
        # type: () -> List[SchemeNode]
        """
        Return a list of active nodes.

        .. versionadded:: 0.1.8
        """
        return [node for node in self.__nodes() if self.is_active(node)]

    def is_blocking(self, node):
        # type: (SchemeNode) -> bool
        """
        Is the node in `blocking` state.

        Is it currently in a state where will produce new outputs and
        therefore no signals should be delivered to dependent nodes until
        it does so. Also no signals will be delivered to the node until
        it exits this state.

        The default implementation returns False.

        .. deprecated:: 0.1.8
            Use a combination of `is_invalidated` and `is_ready`.
        """
        return False

    def is_ready(self, node: SchemeNode) -> bool:
        """
        Is the node in a state where it can receive inputs.

        Re-implement this method in as subclass to prevent specific nodes from
        being considered for input update (e.g. they are still initializing
        runtime resources, executing a non-interruptable task, ...)

        Note that whenever the implicit state changes the
        `post_update_request` should be called.

        The default implementation returns the state of the node's
        `SchemeNode.NotReady` flag.

        Parameters
        ----------
        node: SchemeNode
        """
        return not node.test_state_flags(SchemeNode.NotReady)

    def is_invalidated(self, node: SchemeNode) -> bool:
        """
        Is the node marked as invalidated.

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        state: bool
        """
        return node.test_state_flags(SchemeNode.Invalidated)

    def has_invalidated_outputs(self, node):
        # type: (SchemeNode) -> bool
        """
        Does node have any explicitly invalidated outputs.

        Parameters
        ----------
        node: SchemeNode

        Returns
        -------
        state: bool

        See also
        --------
        invalidate


        .. versionadded:: 0.1.8
        """
        out = self.__node_outputs.get(node)
        if out is not None:
            return any(state.flags & _OutputState.Invalidated
                       for state in out.values())
        else:
            return False

    def has_invalidated_inputs(self, node):
        # type: (SchemeNode) -> bool
        """
        Does the node have any immediate ancestor with invalidated outputs.

        Parameters
        ----------
        node : SchemeNode

        Returns
        -------
        state: bool

        Note
        ----
        The node's ancestors are only computed over enabled links.


        .. versionadded:: 0.1.8
        """
        if self.__workflow is None:
            return False
        workflow = self.__workflow
        return any(
            self.has_invalidated_outputs(link.source_node)
            for link in workflow.find_links(sink_node=node)
            if link.is_enabled())

    def is_active(self, node):
        # type: (SchemeNode) -> bool
        """
        Is the node considered active (executing a task).

        Parameters
        ----------
        node: SchemeNode

        Returns
        -------
        active: bool
        """
        return bool(node.state() & SchemeNode.Running)

    def node_update_front(self):
        # type: () -> Sequence[SchemeNode]
        """
        Return a list of nodes on the update front, i.e. nodes scheduled for
        an update that have no ancestor which is either itself scheduled
        for update or is in a blocking state).

        Note
        ----
        The node's ancestors are only computed over enabled links.
        """
        if self.__workflow is None:
            return []
        workflow = self.__workflow
        expand = partial(expand_node, workflow)

        components = strongly_connected_components(workflow.nodes, expand)
        node_scc = {node: scc for scc in components for node in scc}

        def isincycle(node):  # type: (SchemeNode) -> bool
            return len(node_scc[node]) > 1

        def dependents(node):  # type: (SchemeNode) -> List[SchemeNode]
            return dependent_nodes(workflow, node)

        # A list of all nodes currently active/executing a non-interruptable
        # task.
        blocking_nodes = set(self.blocking_nodes())
        # nodes marked as having invalidated outputs (not yet available)
        invalidated_nodes = set(self.invalidated_nodes())

        #: transitive invalidated nodes (including the legacy self.is_blocked
        #: behaviour - blocked nodes are both invalidated and cannot receive
        #: new inputs)
        invalidated_ = reduce(
            set.union,
            map(dependents, invalidated_nodes | blocking_nodes),
            set([]),
        )  # type: Set[SchemeNode]

        pending = self.pending_nodes()
        pending_ = set()
        for n in pending:
            depend = set(dependents(n))
            if isincycle(n):
                # a pending node in a cycle would would have a circular
                # dependency on itself, preventing any progress being made
                # by the workflow execution.
                cc = node_scc[n]
                depend -= set(cc)
            pending_.update(depend)

        def has_invalidated_ancestor(node):  # type: (SchemeNode) -> bool
            return node in invalidated_

        def has_pending_ancestor(node):  # type: (SchemeNode) -> bool
            return node in pending_

        #: nodes that are eligible for update.
        ready = list(
            filter(
                lambda node: not has_pending_ancestor(node) and
                not has_invalidated_ancestor(node) and not self.is_blocking(
                    node), pending))
        return ready

    @Slot()
    def __process_next(self):
        if not self.__state == SignalManager.Running:
            log.debug("Received 'UpdateRequest' while not in 'Running' state")
            return

        if self.__runtime_state == SignalManager.Processing:
            # This happens if QCoreApplication.processEvents is called from
            # the input handlers. A `__process_next` must be rescheduled when
            # exiting process_queued.
            log.warning("Received 'UpdateRequest' while in 'process_queued'. "
                        "An update will be re-scheduled when exiting the "
                        "current update.")
            return

        if not self.__input_queue:
            return

        if self.__process_next_helper(use_max_active=True):
            # Schedule another update (will be a noop if nothing to do).
            self._update()

    def __process_next_helper(self, use_max_active=True) -> bool:
        eligible = [n for n in self.node_update_front() if self.is_ready(n)]
        if not eligible:
            return False
        max_active = self.max_active()
        nactive = len(set(self.active_nodes()) | set(self.blocking_nodes()))

        log.debug(
            "Process next, queued signals: %i, nactive: %i "
            "(max_active: %i)", len(self.__input_queue), nactive, max_active)
        _ = lambda nodes: list(map(attrgetter('title'), nodes))
        log.debug("Pending nodes: %s", _(self.pending_nodes()))
        log.debug("Blocking nodes: %s", _(self.blocking_nodes()))
        log.debug("Invalidated nodes: %s", _(self.invalidated_nodes()))
        log.debug("Nodes ready for update: %s", _(eligible))

        # Select an node that is already running (effectively cancelling
        # already executing tasks that are immediately updatable)
        selected_node = None  # type: Optional[SchemeNode]
        for node in eligible:
            if self.is_active(node):
                selected_node = node
                break

        # Return if over committed, except in the case that the selected_node
        # is already active.
        if use_max_active and nactive >= max_active and selected_node is None:
            return False

        if selected_node is None:
            selected_node = eligible[0]

        self.process_node(selected_node)
        return True

    def _update(self):  # type: () -> None
        """
        Schedule processing at a later time.
        """
        if self.__state == SignalManager.Running and \
                not self.__update_timer.isActive():
            self.__update_timer.start()

    def post_update_request(self):
        """
        Schedule an update pass.

        Call this method whenever:

        * a node's outputs change (note that this is already done by `send`)
        * any change in the node that influences its eligibility to be picked
          for an input update (is_ready, is_blocking ...).

        Multiple update requests are merged into one.
        """
        self._update()

    def set_max_active(self, val: int) -> None:
        if self.__max_running != val:
            self.__max_running = val
            self._update()

    def max_active(self) -> int:
        value = self.__max_running  # type: Optional[int]
        if value is None:
            value = mapping_get(os.environ, "MAX_ACTIVE_NODES", int, None)
        if value is None:
            s = QSettings()
            s.beginGroup(__name__)
            value = s.value("max-active-nodes", defaultValue=1, type=int)

        if value < 0:
            ccount = os.cpu_count()
            if ccount is None:
                return 1
            else:
                return max(1, ccount + value)
        else:
            return max(1, value)
예제 #11
0
class SignalManager(QObject):
    """
    Handle all runtime signal propagation for a :clas:`Scheme` instance.
    The scheme must be passed to the constructor and will become the parent
    of this object. Furthermore this should happen before any items
    (nodes, links) are added to the scheme.

    """

    Running, Stoped, Paused, Error = range(4)
    """SignalManger state flags."""

    Waiting, Processing = range(2)
    """SignalManager runtime state flags."""

    stateChanged = Signal(int)
    """Emitted when the state of the signal manager changes."""

    updatesPending = Signal()
    """Emitted when signals are added to the queue."""

    processingStarted = Signal([], [SchemeNode])
    """Emitted right before a `SchemeNode` instance has its inputs
    updated.
    """

    processingFinished = Signal([], [SchemeNode])
    """Emitted right after a `SchemeNode` instance has had its inputs
    updated.
    """

    runtimeStateChanged = Signal(int)
    """Emitted when `SignalManager`'s runtime state changes."""

    def __init__(self, scheme):
        assert scheme
        QObject.__init__(self, scheme)
        self._input_queue = []

        # mapping a node to it's current outputs
        # {node: {channel: {id: signal_value}}}
        self._node_outputs = {}

        self.__state = SignalManager.Running
        self.__runtime_state = SignalManager.Waiting

        # A flag indicating if UpdateRequest event should be rescheduled
        self.__reschedule = False
        self.__update_timer = QTimer(self, interval=100, singleShot=True)
        self.__update_timer.timeout.connect(self.__process_next)

    def _can_process(self):
        """
        Return a bool indicating if the manger can enter the main
        processing loop.

        """
        return self.__state not in [SignalManager.Error, SignalManager.Stoped]

    def scheme(self):
        """
        Return the parent class:`Scheme` instance.
        """
        return self.parent()

    def start(self):
        """
        Start the update loop.

        .. note:: The updates will not happen until the control reaches
                  the Qt event loop.

        """
        if self.__state != SignalManager.Running:
            self.__state = SignalManager.Running
            self.stateChanged.emit(SignalManager.Running)
            self._update()

    def stop(self):
        """
        Stop the update loop.

        .. note:: If the `SignalManager` is currently in `process_queues` it
                  will still update all current pending signals, but will not
                  re-enter until `start()` is called again

        """
        if self.__state != SignalManager.Stoped:
            self.__state = SignalManager.Stoped
            self.stateChanged.emit(SignalManager.Stoped)
            self.__update_timer.stop()

    def pause(self):
        """
        Pause the updates.

        """
        if self.__state != SignalManager.Paused:
            self.__state = SignalManager.Paused
            self.stateChanged.emit(SignalManager.Paused)
            self.__update_timer.stop()

    def resume(self):
        if self.__state == SignalManager.Paused:
            self.__state = SignalManager.Running
            self.stateChanged.emit(self.__state)
            self._update()

    def step(self):
        if self.__state == SignalManager.Paused:
            self.process_queued()

    def state(self):
        """
        Return the current state.
        """
        return self.__state

    def _set_runtime_state(self, state):
        """
        Set the runtime state.

        Should only be called by `SignalManager` implementations.

        """
        if self.__runtime_state != state:
            self.__runtime_state = state
            self.runtimeStateChanged.emit(self.__runtime_state)

    def runtime_state(self):
        """
        Return the runtime state. This can be `SignalManager.Waiting`
        or `SignalManager.Processing`.

        """
        return self.__runtime_state

    def on_node_removed(self, node):
        # remove all pending input signals for node so we don't get
        # stale references in process_node.
        # NOTE: This does not remove output signals for this node. In
        # particular the final 'None' will be delivered to the sink
        # nodes even after the source node is no longer in the scheme.
        log.info("Node %r removed. Removing pending signals.", node.title)
        self.remove_pending_signals(node)

        del self._node_outputs[node]

    def on_node_added(self, node):
        self._node_outputs[node] = defaultdict(dict)

    def link_added(self, link):
        # push all current source values to the sink
        link.set_runtime_state(SchemeLink.Empty)
        if link.enabled:
            log.info("Link added (%s). Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))
            self._update()

        link.enabled_changed.connect(self.link_enabled_changed)

    def link_removed(self, link):
        # purge all values in sink's queue
        log.info("Link removed (%s). Scheduling signal data purge.", link)
        self.purge_link(link)
        link.enabled_changed.disconnect(self.link_enabled_changed)

    def link_enabled_changed(self, enabled):
        if enabled:
            link = self.sender()
            log.info("Link %s enabled. Scheduling signal data update.", link)
            self._schedule(self.signals_on_link(link))

    def signals_on_link(self, link):
        """
        Return _Signal instances representing the current values
        present on the link.

        """
        items = self.link_contents(link)
        signals = []

        for key, value in list(items.items()):
            signals.append(_Signal(link, value, key))

        return signals

    def link_contents(self, link):
        """
        Return the contents on link.
        """
        node, channel = link.source_node, link.source_channel

        if node in self._node_outputs:
            return self._node_outputs[node][channel]
        else:
            # if the the node was already removed it's tracked outputs in
            # _node_outputs are cleared, however the final 'None' signal
            # deliveries for the link are left in the _input_queue.
            pending = [sig for sig in self._input_queue if sig.link is link]
            return {sig.id: sig.value for sig in pending}

    def send(self, node, channel, value, id):
        """
        """
        log.debug("%r sending %r (id: %r) on channel %r", node.title, type(value), id, channel.name)

        scheme = self.scheme()

        self._node_outputs[node][channel][id] = value

        links = scheme.find_links(source_node=node, source_channel=channel)
        links = list(filter(is_enabled, links))

        signals = []
        for link in links:
            signals.append(_Signal(link, value, id))

        self._schedule(signals)

    def purge_link(self, link):
        """
        Purge the link (send None for all ids currently present)
        """
        contents = self.link_contents(link)
        ids = list(contents.keys())
        signals = [_Signal(link, None, id) for id in ids]

        self._schedule(signals)

    def _schedule(self, signals):
        """
        Schedule a list of :class:`_Signal` for delivery.
        """
        self._input_queue.extend(signals)

        for link in {sig.link for sig in signals}:
            # update the SchemeLink's runtime state flags
            contents = self.link_contents(link)
            if any(value is not None for value in contents.values()):
                state = SchemeLink.Active
            else:
                state = SchemeLink.Empty
            link.set_runtime_state(state | SchemeLink.Pending)

        if signals:
            self.updatesPending.emit()

        self._update()

    def _update_link(self, link):
        """
        Schedule update of a single link.
        """
        signals = self.signals_on_link(link)
        self._schedule(signals)

    def process_queued(self, max_nodes=None):
        """
        Process queued signals.

        Take one node node from the pending input queue and deliver
        all scheduled signals.
        """
        if max_nodes is not None or max_nodes != 1:
            warnings.warn(
                "`max_nodes` is deprecated and unused (will always equal 1)", DeprecationWarning, stacklevel=2
            )

        if self.__runtime_state == SignalManager.Processing:
            raise RuntimeError("Cannot re-enter 'process_queued'")

        if not self._can_process():
            raise RuntimeError("Can't process in state %i" % self.__state)

        log.info("SignalManager: Processing queued signals")

        node_update_front = self.node_update_front()
        log.debug("SignalManager: Nodes eligible for update %s", [node.title for node in node_update_front])

        if node_update_front:
            node = node_update_front[0]
            self._set_runtime_state(SignalManager.Processing)
            try:
                self.process_node(node)
            finally:
                self._set_runtime_state(SignalManager.Waiting)

    def process_node(self, node):
        """
        Process pending input signals for `node`.
        """
        signals_in = self.pending_input_signals(node)
        self.remove_pending_signals(node)

        signals_in = self.compress_signals(signals_in)

        log.debug("Processing %r, sending %i signals.", node.title, len(signals_in))
        # Clear the link's pending flag.
        for link in {sig.link for sig in signals_in}:
            link.set_runtime_state(link.runtime_state() & ~SchemeLink.Pending)

        assert {sig.link for sig in self._input_queue}.intersection({sig.link for sig in signals_in}) == set([])
        self.processingStarted.emit()
        self.processingStarted[SchemeNode].emit(node)
        try:
            self.send_to_node(node, signals_in)
        finally:
            self.processingFinished.emit()
            self.processingFinished[SchemeNode].emit(node)

    def compress_signals(self, signals):
        """
        Compress a list of :class:`_Signal` instances to be delivered.

        The base implementation returns the list unmodified.

        """
        return signals

    def send_to_node(self, node, signals):
        """
        Abstract. Reimplement in subclass.

        Send/notify the :class:`SchemeNode` instance (or whatever
        object/instance it is a representation of) that it has new inputs
        as represented by the signals list (list of :class:`_Signal`).

        """
        raise NotImplementedError

    def is_pending(self, node):
        """
        Is `node` (class:`SchemeNode`) scheduled for processing (i.e.
        it has incoming pending signals).

        """
        return node in [signal.link.sink_node for signal in self._input_queue]

    def pending_nodes(self):
        """
        Return a list of pending nodes.

        The nodes are returned in the order they were enqueued for
        signal delivery.

        Returns
        -------
        nodes : List[SchemeNode]
        """
        return list(unique(sig.link.sink_node for sig in self._input_queue))

    def pending_input_signals(self, node):
        """
        Return a list of pending input signals for node.
        """
        return [signal for signal in self._input_queue if node is signal.link.sink_node]

    def remove_pending_signals(self, node):
        """
        Remove pending signals for `node`.
        """
        for signal in self.pending_input_signals(node):
            try:
                self._input_queue.remove(signal)
            except ValueError:
                pass

    def blocking_nodes(self):
        """
        Return a list of nodes in a blocking state.
        """
        scheme = self.scheme()
        return [node for node in scheme.nodes if self.is_blocking(node)]

    def is_blocking(self, node):
        return False

    def node_update_front(self):
        """
        Return a list of nodes on the update front, i.e. nodes scheduled for
        an update that have no ancestor which is either itself scheduled
        for update or is in a blocking state)

        .. note::
            The node's ancestors are only computed over enabled links.

        """
        scheme = self.scheme()

        blocking_nodes = set(self.blocking_nodes())

        dependents = partial(dependent_nodes, scheme)

        blocked_nodes = reduce(set.union, map(dependents, blocking_nodes), set(blocking_nodes))

        pending = self.pending_nodes()
        pending_downstream = reduce(set.union, map(dependents, pending), set())

        log.debug("Pending nodes: %s", pending)
        log.debug("Blocking nodes: %s", blocking_nodes)

        noneligible = pending_downstream | blocked_nodes
        return [node for node in pending if node not in noneligible]

    @Slot()
    def __process_next(self):
        if not self.__state == SignalManager.Running:
            log.debug("Received 'UpdateRequest' while not in 'Running' state")
            return

        if self.__runtime_state == SignalManager.Processing:
            # This happens if someone calls QCoreApplication.processEvents
            # from the signal handlers.
            # A `__process_next` must be rescheduled when exiting
            # process_queued.
            log.warning(
                "Received 'UpdateRequest' while in 'process_queued'. "
                "An update will be re-scheduled when exiting the "
                "current update."
            )
            self.__reschedule = True
            return

        log.info("'UpdateRequest' event, queued signals: %i", len(self._input_queue))
        if self._input_queue:
            self.process_queued()

        if self.__reschedule and self.__state == SignalManager.Running:
            self.__reschedule = False
            log.debug("Rescheduling signal update")
            self.__update_timer.start()

        if self.node_update_front():
            log.debug("More nodes are eligible for an update. " "Scheduling another update.")
            self._update()

    def _update(self):
        """
        Schedule processing at a later time.
        """
        if self.__state == SignalManager.Running and not self.__update_timer.isActive():
            self.__update_timer.start()
예제 #12
0
class OWTimeSlice(widget.OWWidget):
    name = 'Time Slice'
    description = 'Select a slice of measurements on a time interval.'
    icon = 'icons/TimeSlice.svg'
    priority = 550

    inputs = [
        ('Data', Table, 'set_data'),
    ]
    outputs = [('Subset', Table)]

    want_main_area = False

    class Error(widget.OWWidget.Error):
        no_time_variable = widget.Msg('Data contains no time variable')

    MAX_SLIDER_VALUE = 500
    DATE_FORMATS = ('yyyy-MM-dd', 'HH:mm:ss.zzz')
    OVERLAP_AMOUNTS = OrderedDict(
        (('all but one (= shift by one slider value)',
          0), ('6/7 of interval', 6 / 7), ('3/4 of interval', 3 / 4),
         ('1/2 of interval', 1 / 2), ('1/3 of interval',
                                      1 / 3), ('1/5 of interval', 1 / 5)))

    loop_playback = settings.Setting(True)
    steps_overlap = settings.Setting(True)
    overlap_amount = settings.Setting(next(iter(OVERLAP_AMOUNTS)))
    playback_interval = settings.Setting(1000)
    slider_values = settings.Setting((0, .2 * MAX_SLIDER_VALUE))

    def __init__(self):
        super().__init__()
        self._delta = 0
        self.play_timer = QTimer(self,
                                 interval=self.playback_interval,
                                 timeout=self.play_single_step)
        slider = self.slider = Slider(
            Qt.Horizontal,
            self,
            minimum=0,
            maximum=self.MAX_SLIDER_VALUE,
            tracking=False,
            valuesChanged=self.valuesChanged,
            minimumValue=self.slider_values[0],
            maximumValue=self.slider_values[1],
        )
        slider.setShowText(False)
        box = gui.vBox(self.controlArea, 'Time Slice')
        box.layout().addWidget(slider)

        hbox = gui.hBox(box)

        def _dateTimeChanged(editted):
            def handler():
                minTime = self.date_from.dateTime().toMSecsSinceEpoch() / 1000
                maxTime = self.date_to.dateTime().toMSecsSinceEpoch() / 1000
                if minTime > maxTime:
                    minTime = maxTime = minTime if editted == self.date_from else maxTime
                    other = self.date_to if editted == self.date_from else self.date_from
                    with blockSignals(other):
                        other.setDateTime(editted.dateTime())

                with blockSignals(self.slider):
                    self.slider.setValues(self.slider.unscale(minTime),
                                          self.slider.unscale(maxTime))
                self.send_selection(minTime, maxTime)

            return handler

        kwargs = dict(calendarPopup=True,
                      displayFormat=' '.join(self.DATE_FORMATS),
                      timeSpec=Qt.UTC)
        date_from = self.date_from = QDateTimeEdit(self, **kwargs)
        date_to = self.date_to = QDateTimeEdit(self, **kwargs)
        date_from.dateTimeChanged.connect(_dateTimeChanged(date_from))
        date_to.dateTimeChanged.connect(_dateTimeChanged(date_to))
        hbox.layout().addStretch(100)
        hbox.layout().addWidget(date_from)
        hbox.layout().addWidget(QLabel(' – '))
        hbox.layout().addWidget(date_to)
        hbox.layout().addStretch(100)

        vbox = gui.vBox(self.controlArea, 'Step / Play Through')
        gui.checkBox(vbox, self, 'loop_playback', label='Loop playback')
        hbox = gui.hBox(vbox)
        gui.checkBox(hbox,
                     self,
                     'steps_overlap',
                     label='Stepping overlaps by:',
                     toolTip='If enabled, the active interval moves forward '
                     '(backward) by half of the interval at each step.')
        gui.comboBox(hbox,
                     self,
                     'overlap_amount',
                     items=tuple(self.OVERLAP_AMOUNTS.keys()),
                     sendSelectedValue=True)
        gui.spin(vbox,
                 self,
                 'playback_interval',
                 label='Playback delay (msec):',
                 minv=100,
                 maxv=30000,
                 step=200,
                 callback=lambda: self.play_timer.setInterval(
                     self.playback_interval))

        hbox = gui.hBox(vbox)
        self.step_backward = gui.button(
            hbox,
            self,
            '⏮',
            callback=lambda: self.play_single_step(backward=True),
            autoDefault=False)
        self.play_button = gui.button(hbox,
                                      self,
                                      '▶',
                                      callback=self.playthrough,
                                      toggleButton=True,
                                      default=True)
        self.step_forward = gui.button(hbox,
                                       self,
                                       '⏭',
                                       callback=self.play_single_step,
                                       autoDefault=False)

        gui.rubber(self.controlArea)

    def valuesChanged(self, minValue, maxValue):
        self.slider_values = (minValue, maxValue)
        self._delta = max(1, (maxValue - minValue))
        minTime = self.slider.scale(minValue)
        maxTime = self.slider.scale(maxValue)

        from_dt = QDateTime.fromMSecsSinceEpoch(minTime * 1000).toUTC()
        to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC()
        with blockSignals(self.date_from, self.date_to):
            self.date_from.setDateTime(from_dt)
            self.date_to.setDateTime(to_dt)

        self.send_selection(minTime, maxTime)

    def send_selection(self, minTime, maxTime):
        try:
            time_values = self.data.time_values
        except AttributeError:
            return
        indices = (minTime <= time_values) & (time_values <= maxTime)
        self.send('Subset', self.data[indices] if indices.any() else None)

    def playthrough(self):
        playing = self.play_button.isChecked()

        for widget in (self.slider, self.step_forward, self.step_backward):
            widget.setDisabled(playing)

        if playing:
            self.play_timer.start()
            self.play_button.setText('▮▮')
        else:
            self.play_timer.stop()
            self.play_button.setText('▶')

    def play_single_step(self, backward=False):
        op = operator.sub if backward else operator.add
        minValue, maxValue = self.slider.values()
        orig_delta = delta = self._delta

        if self.steps_overlap:
            overlap_amount = self.OVERLAP_AMOUNTS[self.overlap_amount]
            if overlap_amount:
                delta = max(1, int(round(delta * (1 - overlap_amount))))
            else:
                delta = 1  # single slider step (== 1/self.MAX_SLIDER_VALUE)

        if maxValue == self.slider.maximum() and not backward:
            minValue = self.slider.minimum()
            maxValue = minValue + orig_delta

            if not self.loop_playback:
                self.play_button.click()
                assert not self.play_timer.isActive()
                assert not self.play_button.isChecked()

        elif minValue == self.slider.minimum() and backward:
            maxValue = self.slider.maximum()
            minValue = maxValue - orig_delta
        else:
            minValue = op(minValue, delta)
            maxValue = op(maxValue, delta)
        # Blocking signals because we want this to be synchronous to avoid
        # re-setting self._delta
        with blockSignals(self.slider):
            self.slider.setValues(minValue, maxValue)
        self.valuesChanged(self.slider.minimumValue(),
                           self.slider.maximumValue())
        self._delta = orig_delta  # Override valuesChanged handler

    def set_data(self, data):
        slider = self.slider
        self.data = data = None if data is None else Timeseries.from_data_table(
            data)

        def disabled():
            slider.setFormatter(str)
            slider.setHistogram(None)
            slider.setScale(0, 0)
            slider.setValues(0, 0)
            slider.setDisabled(True)
            self.send('Subset', None)

        if data is None:
            disabled()
            return

        if not isinstance(data.time_variable, TimeVariable):
            self.Error.no_time_variable()
            disabled()
            return
        self.Error.clear()
        var = data.time_variable

        time_values = data.time_values

        slider.setDisabled(False)
        slider.setHistogram(time_values)
        slider.setFormatter(var.repr_val)
        slider.setScale(time_values.min(), time_values.max())
        self.valuesChanged(slider.minimumValue(), slider.maximumValue())

        # Update datetime edit fields
        min_dt = QDateTime.fromMSecsSinceEpoch(time_values[0] * 1000).toUTC()
        max_dt = QDateTime.fromMSecsSinceEpoch(time_values[-1] * 1000).toUTC()
        self.date_from.setDateTimeRange(min_dt, max_dt)
        self.date_to.setDateTimeRange(min_dt, max_dt)
        date_format = '   '.join(
            (self.DATE_FORMATS[0] if var.have_date else '',
             self.DATE_FORMATS[1] if var.have_time else '')).strip()
        self.date_from.setDisplayFormat(date_format)
        self.date_to.setDisplayFormat(date_format)
예제 #13
0
class OWTimeSlice(widget.OWWidget):
    name = 'Time Slice'
    description = 'Select a slice of measurements on a time interval.'
    icon = 'icons/TimeSlice.svg'
    priority = 550

    class Inputs:
        data = Input("Data", Table)

    class Outputs:
        subset = Output("Subset", Table)

    want_main_area = False

    class Error(widget.OWWidget.Error):
        no_time_variable = widget.Msg('Data contains no time variable')

    MAX_SLIDER_VALUE = 500
    DATE_FORMATS = ('yyyy-MM-dd', 'HH:mm:ss.zzz')
    OVERLAP_AMOUNTS = OrderedDict((
        ('all but one (= shift by one slider value)', 0),
        ('6/7 of interval', 6/7),
        ('3/4 of interval', 3/4),
        ('1/2 of interval', 1/2),
        ('1/3 of interval', 1/3),
        ('1/5 of interval', 1/5)))

    loop_playback = settings.Setting(True)
    steps_overlap = settings.Setting(True)
    overlap_amount = settings.Setting(next(iter(OVERLAP_AMOUNTS)))
    playback_interval = settings.Setting(1000)
    slider_values = settings.Setting((0, .2 * MAX_SLIDER_VALUE))

    def __init__(self):
        super().__init__()
        self._delta = 0
        self.play_timer = QTimer(self,
                                 interval=self.playback_interval,
                                 timeout=self.play_single_step)
        slider = self.slider = Slider(Qt.Horizontal, self,
                                      minimum=0, maximum=self.MAX_SLIDER_VALUE,
                                      tracking=False,
                                      valuesChanged=self.valuesChanged,
                                      minimumValue=self.slider_values[0],
                                      maximumValue=self.slider_values[1],)
        slider.setShowText(False)
        box = gui.vBox(self.controlArea, 'Time Slice')
        box.layout().addWidget(slider)

        hbox = gui.hBox(box)

        def _dateTimeChanged(editted):
            def handler():
                minTime = self.date_from.dateTime().toMSecsSinceEpoch() / 1000
                maxTime = self.date_to.dateTime().toMSecsSinceEpoch() / 1000
                if minTime > maxTime:
                    minTime = maxTime = minTime if editted == self.date_from else maxTime
                    other = self.date_to if editted == self.date_from else self.date_from
                    with blockSignals(other):
                        other.setDateTime(editted.dateTime())

                with blockSignals(self.slider):
                    self.slider.setValues(self.slider.unscale(minTime),
                                          self.slider.unscale(maxTime))
                self.send_selection(minTime, maxTime)
            return handler

        kwargs = dict(calendarPopup=True,
                      displayFormat=' '.join(self.DATE_FORMATS),
                      timeSpec=Qt.UTC)
        date_from = self.date_from = QDateTimeEdit(self, **kwargs)
        date_to = self.date_to = QDateTimeEdit(self, **kwargs)
        date_from.dateTimeChanged.connect(_dateTimeChanged(date_from))
        date_to.dateTimeChanged.connect(_dateTimeChanged(date_to))
        hbox.layout().addStretch(100)
        hbox.layout().addWidget(date_from)
        hbox.layout().addWidget(QLabel(' – '))
        hbox.layout().addWidget(date_to)
        hbox.layout().addStretch(100)

        vbox = gui.vBox(self.controlArea, 'Step / Play Through')
        gui.checkBox(vbox, self, 'loop_playback',
                     label='Loop playback')
        hbox = gui.hBox(vbox)
        gui.checkBox(hbox, self, 'steps_overlap',
                     label='Stepping overlaps by:',
                     toolTip='If enabled, the active interval moves forward '
                             '(backward) by half of the interval at each step.')
        gui.comboBox(hbox, self, 'overlap_amount',
                     items=tuple(self.OVERLAP_AMOUNTS.keys()),
                     sendSelectedValue=True)
        gui.spin(vbox, self, 'playback_interval',
                 label='Playback delay (msec):',
                 minv=100, maxv=30000, step=200,
                 callback=lambda: self.play_timer.setInterval(self.playback_interval))

        hbox = gui.hBox(vbox)
        self.step_backward = gui.button(hbox, self, '⏮',
                                        callback=lambda: self.play_single_step(backward=True),
                                        autoDefault=False)
        self.play_button = gui.button(hbox, self, '▶',
                                      callback=self.playthrough,
                                      toggleButton=True, default=True)
        self.step_forward = gui.button(hbox, self, '⏭',
                                       callback=self.play_single_step,
                                       autoDefault=False)

        gui.rubber(self.controlArea)
        self._set_disabled(True)

    def valuesChanged(self, minValue, maxValue):
        self.slider_values = (minValue, maxValue)
        self._delta = max(1, (maxValue - minValue))
        minTime = self.slider.scale(minValue)
        maxTime = self.slider.scale(maxValue)

        from_dt = QDateTime.fromMSecsSinceEpoch(minTime * 1000).toUTC()
        to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC()
        with blockSignals(self.date_from,
                          self.date_to):
            self.date_from.setDateTime(from_dt)
            self.date_to.setDateTime(to_dt)

        self.send_selection(minTime, maxTime)

    def send_selection(self, minTime, maxTime):
        try:
            time_values = self.data.time_values
        except AttributeError:
            return
        indices = (minTime <= time_values) & (time_values <= maxTime)
        self.Outputs.subset.send(self.data[indices] if indices.any() else None)

    def playthrough(self):
        playing = self.play_button.isChecked()

        for widget in (self.slider,
                       self.step_forward,
                       self.step_backward):
            widget.setDisabled(playing)

        if playing:
            self.play_timer.start()
            self.play_button.setText('▮▮')
        else:
            self.play_timer.stop()
            self.play_button.setText('▶')

    def play_single_step(self, backward=False):
        op = operator.sub if backward else operator.add
        minValue, maxValue = self.slider.values()
        orig_delta = delta = self._delta

        if self.steps_overlap:
            overlap_amount = self.OVERLAP_AMOUNTS[self.overlap_amount]
            if overlap_amount:
                delta = max(1, int(round(delta * (1 - overlap_amount))))
            else:
                delta = 1  # single slider step (== 1/self.MAX_SLIDER_VALUE)

        if maxValue == self.slider.maximum() and not backward:
            minValue = self.slider.minimum()
            maxValue = minValue + orig_delta

            if not self.loop_playback:
                self.play_button.click()
                assert not self.play_timer.isActive()
                assert not self.play_button.isChecked()

        elif minValue == self.slider.minimum() and backward:
            maxValue = self.slider.maximum()
            minValue = maxValue - orig_delta
        else:
            minValue = op(minValue, delta)
            maxValue = op(maxValue, delta)
        # Blocking signals because we want this to be synchronous to avoid
        # re-setting self._delta
        with blockSignals(self.slider):
            self.slider.setValues(minValue, maxValue)
        self.valuesChanged(self.slider.minimumValue(), self.slider.maximumValue())
        self._delta = orig_delta  # Override valuesChanged handler

    def _set_disabled(self, is_disabled):
        for func in [self.date_from, self.date_to, self.step_backward, self.play_button,
                     self.step_forward, self.controls.loop_playback,
                     self.controls.steps_overlap, self.controls.overlap_amount,
                     self.controls.playback_interval, self.slider]:
            func.setDisabled(is_disabled)

    @Inputs.data
    def set_data(self, data):
        slider = self.slider
        self.data = data = None if data is None else Timeseries.from_data_table(data)

        def disabled():
            slider.setFormatter(str)
            slider.setHistogram(None)
            slider.setScale(0, 0)
            slider.setValues(0, 0)
            self._set_disabled(True)
            self.Outputs.subset.send(None)

        if data is None:
            disabled()
            return

        if not isinstance(data.time_variable, TimeVariable):
            self.Error.no_time_variable()
            disabled()
            return
        self.Error.clear()
        var = data.time_variable

        time_values = data.time_values

        self._set_disabled(False)
        slider.setHistogram(time_values)
        slider.setFormatter(var.repr_val)
        slider.setScale(time_values.min(), time_values.max())
        self.valuesChanged(slider.minimumValue(), slider.maximumValue())

        # Update datetime edit fields
        min_dt = QDateTime.fromMSecsSinceEpoch(time_values[0] * 1000).toUTC()
        max_dt = QDateTime.fromMSecsSinceEpoch(time_values[-1] * 1000).toUTC()
        self.date_from.setDateTimeRange(min_dt, max_dt)
        self.date_to.setDateTimeRange(min_dt, max_dt)
        date_format = '   '.join((self.DATE_FORMATS[0] if var.have_date else '',
                                  self.DATE_FORMATS[1] if var.have_time else '')).strip()
        self.date_from.setDisplayFormat(date_format)
        self.date_to.setDisplayFormat(date_format)
예제 #14
0
class ComboBox(QComboBox):
    """
    A QComboBox subclass extended to support bounded contents width hint.

    Prefer to use this class in place of plain QComboBox when the used
    model will possibly contain many items.
    """
    def __init__(self, parent=None, **kwargs):
        self.__maximumContentsLength = MAXIMUM_CONTENTS_LENGTH
        super().__init__(parent, **kwargs)

        self.__in_mousePressEvent = False
        # Yet Another Mouse Release Ignore Timer
        self.__yamrit = QTimer(self, singleShot=True)

        view = self.view()
        # optimization for displaying large models
        if isinstance(view, QListView):
            view.setUniformItemSizes(True)
        view.viewport().installEventFilter(self)

    def setMaximumContentsLength(self, length):  # type: (int) -> None
        """
        Set the maximum contents length hint.

        The hint specifies the upper bound on the `sizeHint` and
        `minimumSizeHint` width specified in character length.
        Set to 0 or negative value to disable.

        Note
        ----
        This property does not affect the widget's `maximumSize`.
        The widget can still grow depending on its `sizePolicy`.

        Parameters
        ----------
        length : int
            Maximum contents length hint.
        """
        if self.__maximumContentsLength != length:
            self.__maximumContentsLength = length
            self.updateGeometry()

    def maximumContentsLength(self):  # type: () -> int
        """
        Return the maximum contents length hint.
        """
        return self.__maximumContentsLength

    def _get_size_hint(self):
        sh = super().sizeHint()
        if self.__maximumContentsLength > 0:
            width = (
                self.fontMetrics().width("X") * self.__maximumContentsLength
                + self.iconSize().width() + 4
            )
            sh = sh.boundedTo(QSize(width, sh.height()))
        return sh

    def sizeHint(self):  # type: () -> QSize
        # reimplemented
        return self._get_size_hint()

    def minimumSizeHint(self):  # type: () -> QSize
        # reimplemented
        return self._get_size_hint()

    # workaround for QTBUG-67583
    def mousePressEvent(self, event):  # type: (QMouseEvent) -> None
        # reimplemented
        self.__in_mousePressEvent = True
        super().mousePressEvent(event)
        self.__in_mousePressEvent = False

    def showPopup(self):  # type: () -> None
        # reimplemented
        super().showPopup()
        if self.__in_mousePressEvent:
            self.__yamrit.start(QApplication.doubleClickInterval())

    def eventFilter(self, obj, event):
        # type: (QObject, QEvent) -> bool
        if event.type() == QEvent.MouseButtonRelease \
                and event.button() == Qt.LeftButton \
                and obj is self.view().viewport() \
                and self.__yamrit.isActive():
            return True
        else:
            return super().eventFilter(obj, event)
예제 #15
0
class OWTimeSlice(widget.OWWidget):
    name = 'Time Slice'
    description = 'Select a slice of measurements on a time interval.'
    icon = 'icons/TimeSlice.svg'
    priority = 550

    class Inputs:
        data = Input("Data", Table)

    class Outputs:
        subset = Output("Subset", Table)

    settings_version = 2

    want_main_area = False

    class Error(widget.OWWidget.Error):
        no_time_variable = widget.Msg('Data contains no time variable')
        no_time_delta = widget.Msg('Data contains only one time point')

    MAX_SLIDER_VALUE = 500
    DATE_FORMATS = ('yyyy', '-MM', '-dd', '  HH:mm:ss.zzz')
    # only appropriate overlap amounts are shown, but these are all the options
    DELAY_VALUES = (0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30)
    STEP_SIZES = OrderedDict(
        (('1 second', 1), ('5 seconds', 5), ('10 seconds', 10),
         ('15 seconds', 15), ('30 seconds', 30), ('1 minute', 60),
         ('5 minutes', 300), ('10 minutes', 600), ('15 minutes', 900),
         ('30 minutes', 1800), ('1 hour', 3600), ('2 hours', 7200),
         ('3 hours', 10800), ('6 hours', 21600), ('12 hours', 43200),
         ('1 day', 86400), ('1 week', 604800), ('2 weeks', 1209600),
         ('1 month', (1, 'month')), ('2 months', (2, 'month')), ('3 months',
                                                                 (3, 'month')),
         ('6 months', (6, 'month')), ('1 year', (1, 'year')), ('2 years',
                                                               (2, 'year')),
         ('5 years', (5, 'year')), ('10 years', (10, 'year')), ('25 years',
                                                                (25, 'year')),
         ('50 years', (50, 'year')), ('100 years', (100, 'year'))))

    loop_playback = settings.Setting(True)
    custom_step_size = settings.Setting(False)
    step_size = settings.Setting(next(iter(STEP_SIZES)))
    playback_interval = settings.Setting(1)
    slider_values = settings.Setting((0, .2 * MAX_SLIDER_VALUE))

    icons_font = None

    def __init__(self):
        super().__init__()
        self._delta = 0
        self.play_timer = QTimer(self,
                                 interval=1000 * self.playback_interval,
                                 timeout=self.play_single_step)
        slider = self.slider = Slider(Qt.Horizontal,
                                      self,
                                      minimum=0,
                                      maximum=self.MAX_SLIDER_VALUE,
                                      tracking=True,
                                      playbackInterval=1000 *
                                      self.playback_interval,
                                      valuesChanged=self.sliderValuesChanged,
                                      minimumValue=self.slider_values[0],
                                      maximumValue=self.slider_values[1])
        slider.setShowText(False)
        selectBox = gui.vBox(self.controlArea, 'Select a Time Range')
        selectBox.layout().addWidget(slider)

        dtBox = gui.hBox(selectBox)

        kwargs = dict(calendarPopup=True,
                      displayFormat=' '.join(self.DATE_FORMATS),
                      timeSpec=Qt.UTC)
        date_from = self.date_from = QDateTimeEdit(self, **kwargs)
        date_to = self.date_to = QDateTimeEdit(self, **kwargs)

        def datetime_edited(dt_edit):
            minTime = self.date_from.dateTime().toMSecsSinceEpoch() / 1000
            maxTime = self.date_to.dateTime().toMSecsSinceEpoch() / 1000
            if minTime > maxTime:
                minTime = maxTime = minTime if dt_edit == self.date_from else maxTime
                other = self.date_to if dt_edit == self.date_from else self.date_from
                with blockSignals(other):
                    other.setDateTime(dt_edit.dateTime())

            self.dteditValuesChanged(minTime, maxTime)

        date_from.dateTimeChanged.connect(lambda: datetime_edited(date_from))
        date_to.dateTimeChanged.connect(lambda: datetime_edited(date_to))

        # hotfix, does not repaint on click of arrow
        date_from.calendarWidget().currentPageChanged.connect(
            lambda: date_from.calendarWidget().repaint())
        date_to.calendarWidget().currentPageChanged.connect(
            lambda: date_to.calendarWidget().repaint())

        dtBox.layout().addStretch(100)
        dtBox.layout().addWidget(date_from)
        dtBox.layout().addWidget(QLabel(' – '))
        dtBox.layout().addWidget(date_to)
        dtBox.layout().addStretch(100)

        hCenterBox = gui.hBox(self.controlArea)
        gui.rubber(hCenterBox)
        vControlsBox = gui.vBox(hCenterBox)

        stepThroughBox = gui.vBox(vControlsBox, 'Step/Play Through')
        gui.rubber(stepThroughBox)
        gui.checkBox(stepThroughBox,
                     self,
                     'loop_playback',
                     label='Loop playback')
        customStepBox = gui.hBox(stepThroughBox)
        gui.checkBox(
            customStepBox,
            self,
            'custom_step_size',
            label='Custom step size: ',
            toolTip='If not chosen, the active interval moves forward '
            '(backward), stepping in increments of its own size.')
        self.stepsize_combobox = gui.comboBox(customStepBox,
                                              self,
                                              'step_size',
                                              items=tuple(
                                                  self.STEP_SIZES.keys()),
                                              sendSelectedValue=True)
        playBox = gui.hBox(stepThroughBox)
        gui.rubber(playBox)
        gui.rubber(stepThroughBox)

        if self.icons_font is None:
            self.icons_font = load_icons_font()

        self.step_backward = gui.button(
            playBox,
            self,
            '⏪',
            callback=lambda: self.play_single_step(backward=True),
            autoDefault=False)
        self.step_backward.setFont(self.icons_font)
        self.play_button = gui.button(playBox,
                                      self,
                                      '▶️',
                                      callback=self.playthrough,
                                      toggleButton=True,
                                      default=True)
        self.play_button.setFont(self.icons_font)
        self.step_forward = gui.button(playBox,
                                       self,
                                       '⏩',
                                       callback=self.play_single_step,
                                       autoDefault=False)
        self.step_forward.setFont(self.icons_font)

        gui.rubber(playBox)
        intervalBox = gui.vBox(vControlsBox, 'Playback/Tracking interval')
        intervalBox.setToolTip(
            'In milliseconds, set the delay for playback and '
            'for sending data upon manually moving the interval.')

        def set_intervals():
            self.play_timer.setInterval(1000 * self.playback_interval)
            self.slider.tracking_timer.setInterval(1000 *
                                                   self.playback_interval)

        gui.valueSlider(intervalBox,
                        self,
                        'playback_interval',
                        label='Delay:',
                        labelFormat='%.2g sec',
                        values=self.DELAY_VALUES,
                        callback=set_intervals)

        gui.rubber(hCenterBox)
        gui.rubber(self.controlArea)
        self._set_disabled(True)

    def sliderValuesChanged(self, minValue, maxValue):
        self._delta = max(1, (maxValue - minValue))
        minTime = self.slider.scale(minValue)
        maxTime = self.slider.scale(maxValue)

        from_dt = QDateTime.fromMSecsSinceEpoch(minTime * 1000).toUTC()
        to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC()
        if self.date_from.dateTime() != from_dt:
            with blockSignals(self.date_from):
                self.date_from.setDateTime(from_dt)
        if self.date_from.dateTime() != to_dt:
            with blockSignals(self.date_to):
                self.date_to.setDateTime(to_dt)

        self.send_selection(minTime, maxTime)

    def dteditValuesChanged(self, minTime, maxTime):
        minValue = self.slider.unscale(minTime)
        maxValue = self.slider.unscale(maxTime)
        if minValue == maxValue:
            # maxValue's range is minValue's range shifted by one
            maxValue += 1
            maxTime = self.slider.scale(maxValue)
            to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC()
            with blockSignals(self.date_to):
                self.date_to.setDateTime(to_dt)

        self._delta = max(1, (maxValue - minValue))

        if self.slider_values != (minValue, maxValue):
            self.slider_values = (minValue, maxValue)
            with blockSignals(self.slider):
                self.slider.setValues(minValue, maxValue)

        self.send_selection(minTime, maxTime)

    def send_selection(self, minTime, maxTime):
        try:
            time_values = self.data.time_values
        except AttributeError:
            return
        indices = (minTime <= time_values) & (time_values < maxTime)
        self.Outputs.subset.send(self.data[indices] if indices.any() else None)

    def playthrough(self):
        playing = self.play_button.isChecked()

        for widget in (self.slider, self.step_forward, self.step_backward):
            widget.setDisabled(playing)

        for widget in (self.date_from, self.date_to):
            widget.setReadOnly(playing)

        if playing:
            self.play_timer.start()
            self.play_button.setText('⏸')
        else:
            self.play_timer.stop()
            self.play_button.setText('▶️')

        # hotfix
        self.repaint()

    def play_single_step(self, backward=False):
        minValue, maxValue = self.slider.values()
        orig_delta = delta = self._delta

        def new_value(value):
            if self.custom_step_size:
                step_amount = self.STEP_SIZES[self.step_size]
                time = datetime.datetime.fromtimestamp(
                    self.slider.scale(value), tz=datetime.timezone.utc)
                newTime = add_time(time, step_amount, -1 if backward else 1)
                return self.slider.unscale(newTime.timestamp())
            return value + (-delta if backward else delta)

        if maxValue == self.slider.maximum() and not backward:
            minValue = self.slider.minimum()
            maxValue = self.slider.minimum() + delta

            if not self.loop_playback:
                self.play_button.click()
                assert not self.play_timer.isActive()
                assert not self.play_button.isChecked()

        elif minValue == self.slider.minimum() and backward:
            maxValue = self.slider.maximum()
            minValue = min(self.slider.maximum(), new_value(maxValue))
        else:
            minValue = min(new_value(minValue), self.slider.maximum())
            maxValue = min(new_value(maxValue), self.slider.maximum())
        # Blocking signals because we want this to be synchronous to avoid
        # re-setting self._delta
        with blockSignals(self.slider):
            self.slider.setValues(minValue, maxValue)
        self.sliderValuesChanged(self.slider.minimumValue(),
                                 self.slider.maximumValue())
        self._delta = orig_delta  # Override valuesChanged handler

        # hotfix
        self.slider.repaint()

    def _set_disabled(self, is_disabled):
        if is_disabled and self.play_timer.isActive():
            self.play_button.click()
            assert not self.play_timer.isActive()
            assert not self.play_button.isChecked()

        for func in [
                self.date_from, self.date_to, self.step_backward,
                self.play_button, self.step_forward,
                self.controls.loop_playback, self.controls.step_size,
                self.controls.playback_interval, self.slider
        ]:
            func.setDisabled(is_disabled)

    @Inputs.data
    def set_data(self, data):
        slider = self.slider
        self.data = data = None if data is None else Timeseries.from_data_table(
            data)

        def disabled():
            slider.setFormatter(str)
            slider.setHistogram(None)
            slider.setScale(0, 0, None)
            slider.setValues(0, 0)
            self._set_disabled(True)
            self.Outputs.subset.send(None)

        if data is None:
            disabled()
            return

        if not isinstance(data.time_variable, TimeVariable):
            self.Error.no_time_variable()
            disabled()
            return
        if not data.time_delta.deltas:
            self.Error.no_time_delta()
            disabled()
            return
        self.Error.clear()
        var = data.time_variable

        time_values = data.time_values

        min_dt = datetime.datetime.fromtimestamp(round(time_values.min()),
                                                 tz=datetime.timezone.utc)
        max_dt = datetime.datetime.fromtimestamp(round(time_values.max()),
                                                 tz=datetime.timezone.utc)

        # Depending on time delta:
        #   - set slider maximum (granularity)
        #   - set range for end dt (+ 1 timedelta)
        #   - set date format
        #   - set time overlap options
        delta = data.time_delta.gcd
        range = max_dt - min_dt
        if isinstance(delta, Number):
            maximum = round(range.total_seconds() / delta)

            timedelta = datetime.timedelta(milliseconds=delta * 1000)
            min_dt2 = min_dt + timedelta
            max_dt2 = max_dt + timedelta

            if delta >= 86400:  # more than a day
                date_format = ''.join(self.DATE_FORMATS[0:3])
            else:
                date_format = ''.join(self.DATE_FORMATS)

            for k, n in [(k, n) for k, n in self.STEP_SIZES.items()
                         if isinstance(n, Number)]:
                if delta <= n:
                    min_overlap = k
                    break
            else:
                min_overlap = '1 day'
        else:  # isinstance(delta, tuple)
            if delta[1] == 'month':
                months = (max_dt.year - min_dt.year) * 12 + \
                         (max_dt.month - min_dt.month)
                maximum = months / delta[0]

                if min_dt.month < 12 - delta[0]:
                    min_dt2 = min_dt.replace(month=min_dt.month + delta[0])
                else:
                    min_dt2 = min_dt.replace(year=min_dt.year + 1,
                                             month=12 - min_dt.month +
                                             delta[0])
                if max_dt.month < 12 - delta[0]:
                    max_dt2 = max_dt.replace(month=max_dt.month + delta[0])
                else:
                    max_dt2 = max_dt.replace(year=max_dt.year + 1,
                                             month=12 - min_dt.month +
                                             delta[0])

                date_format = ''.join(self.DATE_FORMATS[0:2])

                for k, (i, u) in [(k, v) for k, v in self.STEP_SIZES.items()
                                  if isinstance(v, tuple) and v[1] == 'month']:
                    if delta[0] <= i:
                        min_overlap = k
                        break
                else:
                    min_overlap = '1 year'
            else:  # elif delta[1] == 'year':
                years = max_dt.year - min_dt.year
                maximum = years / delta[0]

                min_dt2 = min_dt.replace(year=min_dt.year + delta[0], )
                max_dt2 = max_dt.replace(year=max_dt.year + delta[0], )

                date_format = self.DATE_FORMATS[0]

                for k, (i, u) in [(k, v) for k, v in self.STEP_SIZES.items()
                                  if isinstance(v, tuple) and v[1] == 'year']:
                    if delta[0] <= i:
                        min_overlap = k
                        break
                else:
                    raise Exception('Timedelta larger than 100 years')

        # find max sensible time overlap
        upper_overlap_limit = range / 2
        for k, overlap in self.STEP_SIZES.items():
            if isinstance(overlap, Number):
                if upper_overlap_limit.total_seconds() <= overlap:
                    max_overlap = k
                    break
            else:
                i, u = overlap
                if u == 'month':
                    month_diff = (max_dt.year - min_dt.year) * 12 \
                                 + max(0, max_dt.month - min_dt.month)
                    if month_diff / 2 <= i:
                        max_overlap = k
                        break
                else:  # if u == 'year':
                    year_diff = max_dt.year - min_dt.year
                    if year_diff / 2 <= i:
                        max_overlap = k
                        break
        else:
            # last item in step sizes
            *_, max_overlap = self.STEP_SIZES.keys()

        self.stepsize_combobox.clear()
        dict_iter = iter(self.STEP_SIZES.keys())
        next_item = next(dict_iter)
        while next_item != min_overlap:
            next_item = next(dict_iter)
        self.stepsize_combobox.addItem(next_item)
        self.step_size = next_item
        while next_item != max_overlap:
            next_item = next(dict_iter)
            self.stepsize_combobox.addItem(next_item)

        slider.setMinimum(0)
        slider.setMaximum(maximum + 1)

        self._set_disabled(False)
        slider.setHistogram(time_values)
        slider.setFormatter(var.repr_val)
        slider.setScale(time_values.min(), time_values.max(),
                        data.time_delta.gcd)
        self.sliderValuesChanged(slider.minimumValue(), slider.maximumValue())

        def utc_dt(dt):
            qdt = QDateTime(dt)
            qdt.setTimeZone(QTimeZone.utc())
            return qdt

        self.date_from.setDateTimeRange(utc_dt(min_dt), utc_dt(max_dt))
        self.date_to.setDateTimeRange(utc_dt(min_dt2), utc_dt(max_dt2))
        self.date_from.setDisplayFormat(date_format)
        self.date_to.setDisplayFormat(date_format)

        def format_time(i):
            dt = QDateTime.fromMSecsSinceEpoch(i * 1000).toUTC()
            return dt.toString(date_format)

        self.slider.setFormatter(format_time)

    @classmethod
    def migrate_settings(cls, settings_, version):
        if version < 2:
            interval = settings_["playback_interval"] / 1000
            if interval in cls.DELAY_VALUES:
                settings_["playback_interval"] = interval
            else:
                settings_["playback_interval"] = 1
예제 #16
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        QGraphicsView.__init__(self, *args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

    def setScene(self, scene):
        QGraphicsView.setScene(self, scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        QGraphicsView.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()

        QGraphicsView.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()

        return QGraphicsView.mouseReleaseEvent(self, event)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        QGraphicsView.drawBackground(self, painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
예제 #17
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.grabGesture(Qt.PinchGesture)
        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"),
            self,
            objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(self.tr("Zoom out"),
                                       self,
                                       objectName="action-zoom-out",
                                       shortcut=QKeySequence.ZoomOut,
                                       triggered=self.zoomOut)
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"),
            self,
            objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0))

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def __should_scroll_horizontally(self, event: QWheelEvent):
        if event.source() != Qt.MouseEventNotSynthesized:
            return False
        if (event.modifiers() & Qt.ShiftModifier and sys.platform == 'darwin'
                or event.modifiers() & Qt.AltModifier
                and sys.platform != 'darwin'):
            return True
        if event.angleDelta().x() == 0:
            vBar = self.verticalScrollBar()
            yDelta = event.angleDelta().y()
            direction = yDelta >= 0
            edgeVBarValue = vBar.minimum() if direction else vBar.maximum()
            return vBar.value() == edgeVBarValue
        return False

    def wheelEvent(self, event: QWheelEvent):
        # Zoom
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        # Scroll horizontally
        elif self.__should_scroll_horizontally(event):
            x, y = event.angleDelta().x(), event.angleDelta().y()
            sign_value = x if x != 0 else y
            sign = 1 if sign_value >= 0 else -1
            new_angle_delta = QPoint(sign * max(abs(x), abs(y), sign_value), 0)
            new_pixel_delta = QPoint(0, 0)
            new_modifiers = event.modifiers() & ~(Qt.ShiftModifier
                                                  | Qt.AltModifier)
            new_event = QWheelEvent(event.pos(), event.globalPos(),
                                    new_pixel_delta, new_angle_delta,
                                    event.buttons(), new_modifiers,
                                    event.phase(), event.inverted(),
                                    event.source())
            event.accept()
            super().wheelEvent(new_event)
        else:
            super().wheelEvent(event)

    def gestureEvent(self, event: QGestureEvent):
        gesture = event.gesture(Qt.PinchGesture)
        if gesture is None:
            return
        if gesture.state() == Qt.GestureStarted:
            event.accept(gesture)
        elif gesture.changeFlags() & QPinchGesture.ScaleFactorChanged:
            anchor = gesture.centerPoint().toPoint()
            anchor = self.mapToScene(anchor)
            self.__setZoomLevel(self.__zoomLevel * gesture.scaleFactor(),
                                anchor=anchor)
            event.accept()
        elif gesture.state() == Qt.GestureFinished:
            event.accept()

    def event(self, event: QEvent) -> bool:
        if event.type() == QEvent.Gesture:
            self.gestureEvent(cast(QGestureEvent, event))
        return super().event(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale, anchor=None):
        # type: (float, Optional[QPointF]) -> None
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            if anchor is not None:
                anchor = self.mapFromScene(anchor)
            self.setTransform(transform)
            if anchor is not None:
                center = self.viewport().rect().center()
                diff = self.mapToScene(center) - self.mapToScene(anchor)
                self.centerOn(anchor + diff)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(float,
                          zoomLevel,
                          setZoomLevel,
                          notify=zoomLevelChanged)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
예제 #18
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"),
            self,
            objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(self.tr("Zoom out"),
                                       self,
                                       objectName="action-zoom-out",
                                       shortcut=QKeySequence.ZoomOut,
                                       triggered=self.zoomOut)
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"),
            self,
            objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0))

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def wheelEvent(self, event: QWheelEvent):
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        else:
            super().wheelEvent(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale):
        # type: (float) -> None
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            self.setTransform(transform)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(float,
                          zoomLevel,
                          setZoomLevel,
                          notify=zoomLevelChanged)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
예제 #19
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """

    def __init__(self, *args):
        QGraphicsView.__init__(self, *args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

    def setScene(self, scene):
        QGraphicsView.setScene(self, scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        QGraphicsView.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()

        QGraphicsView.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()

        return QGraphicsView.mouseReleaseEvent(self, event)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        QGraphicsView.drawBackground(self, painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(
                vrect.size().toSize().boundedTo(QSize(200, 200))
            )
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
class WidgetManager(QObject):
    """
    WidgetManager class is responsible for creation, tracking and deletion
    of UI elements constituting an interactive workflow.

    It does so by reacting to changes in the underlying workflow model,
    creating and destroying the components when needed.

    This is an abstract class, subclassed MUST reimplement at least
    :func:`create_widget_for_node` and :func:`delete_widget_for_node`.

    The widgets created with :func:`create_widget_for_node` will automatically
    receive dispatched events:

        * :data:`WorkflowEvent.InputLinkAdded` - when a new input link is
          added to the workflow.
        * :data:`LinkEvent.InputLinkRemoved` - when a input link is removed.
        * :data:`LinkEvent.OutputLinkAdded` - when a new output link is
          added to the workflow.
        * :data:`LinkEvent.InputLinkRemoved` - when a output link is removed.
        * :data:`LinkEvent.InputLinkStateChanged` - when the input link's
          runtime state changes.
        * :data:`LinkEvent.OutputLinkStateChanged` - when the output link's
          runtime state changes.
        * :data:`WorkflowEnvEvent.WorkflowEnvironmentChanged` - when the
          workflow environment changes.

    .. seealso:: :func:`.Scheme.add_link()`, :func:`Scheme.remove_link`,
                 :func:`.Scheme.runtime_env`
    """
    #: A new QWidget was created and added by the manager.
    widget_for_node_added = Signal(SchemeNode, QWidget)

    #: A QWidget was removed, hidden and will be deleted when appropriate.
    widget_for_node_removed = Signal(SchemeNode, QWidget)

    class CreationPolicy(enum.Enum):
        """
        Widget Creation Policy.
        """
        #: Widgets are scheduled to be created from the event loop, or when
        #: first accessed with `widget_for_node`
        Normal = "Normal"
        #: Widgets are created immediately when a node is added to the
        #: workflow model.
        Immediate = "Immediate"
        #: Widgets are created only when first accessed with `widget_for_node`
        #: (e.g. when activated in the view).
        OnDemand = "OnDemand"

    Normal = CreationPolicy.Normal
    Immediate = CreationPolicy.Immediate
    OnDemand = CreationPolicy.OnDemand

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__workflow = None  # type: Optional[Scheme]
        self.__creation_policy = WidgetManager.Normal
        self.__float_widgets_on_top = False

        self.__item_for_node = {}  # type: Dict[SchemeNode, Item]
        self.__item_for_widget = {}  # type: Dict[QWidget, Item]

        self.__init_queue = deque()  # type: Deque[SchemeNode]

        self.__init_timer = QTimer(self, singleShot=True)
        self.__init_timer.timeout.connect(self.__process_init_queue)

        self.__activation_monitor = ActivationMonitor(self)
        self.__activation_counter = itertools.count()
        self.__activation_monitor.activated.connect(self.__mark_activated)

    def set_workflow(self, workflow):
        # type: (Scheme) -> None
        """
        Set the workflow.
        """
        if workflow is self.__workflow:
            return

        if self.__workflow is not None:
            # cleanup
            for node in self.__workflow.nodes:
                self.__remove_node(node)
            self.__workflow.node_added.disconnect(self.__on_node_added)
            self.__workflow.node_removed.disconnect(self.__on_node_removed)
            self.__workflow.link_added.disconnect(self.__on_link_added)
            self.__workflow.link_removed.disconnect(self.__on_link_removed)
            self.__workflow.runtime_env_changed.disconnect(
                self.__on_env_changed)
            self.__workflow.removeEventFilter(self)

        self.__workflow = workflow

        workflow.node_added.connect(self.__on_node_added, Qt.UniqueConnection)
        workflow.node_removed.connect(self.__on_node_removed,
                                      Qt.UniqueConnection)
        workflow.link_added.connect(self.__on_link_added, Qt.UniqueConnection)
        workflow.link_removed.connect(self.__on_link_removed,
                                      Qt.UniqueConnection)
        workflow.runtime_env_changed.connect(self.__on_env_changed,
                                             Qt.UniqueConnection)
        workflow.installEventFilter(self)
        for node in workflow.nodes:
            self.__add_node(node)

    def workflow(self):
        # type: () -> Optional[Workflow]
        return self.__workflow

    scheme = workflow
    set_scheme = set_workflow

    def set_creation_policy(self, policy):
        # type: (CreationPolicy) -> None
        """
        Set the widget creation policy.
        """
        if self.__creation_policy != policy:
            self.__creation_policy = policy
            if self.__creation_policy == WidgetManager.Immediate:
                self.__init_timer.stop()
                # create all
                if self.__workflow is not None:
                    for node in self.__workflow.nodes:
                        self.ensure_created(node)
            elif self.__creation_policy == WidgetManager.Normal:
                if not self.__init_timer.isActive() and self.__init_queue:
                    self.__init_timer.start()
            elif self.__creation_policy == WidgetManager.OnDemand:
                self.__init_timer.stop()
            else:
                assert False

    def creation_policy(self):
        """
        Return the current widget creation policy.
        """
        return self.__creation_policy

    def create_widget_for_node(self, node):
        # type: (SchemeNode) -> QWidget
        """
        Create and initialize a widget for node.

        This is an abstract method. Subclasses must reimplemented it.
        """
        raise NotImplementedError()

    def delete_widget_for_node(self, node, widget):
        # type: (SchemeNode, QWidget) -> None
        """
        Remove and delete widget for node.

        This is an abstract method. Subclasses must reimplemented it.
        """
        raise NotImplementedError()

    def node_for_widget(self, widget):
        # type: (QWidget) -> Optional[SchemeNode]
        """
        Return the node for widget.
        """
        item = self.__item_for_widget.get(widget)
        if item is not None:
            return item.node
        else:
            return None

    def widget_for_node(self, node):
        # type: (SchemeNode) -> Optional[QWidget]
        """
        Return the widget for node.
        """
        self.ensure_created(node)
        item = self.__item_for_node.get(node)
        return item.widget if item is not None else None

    def __add_widget_for_node(self, node):
        # type: (SchemeNode) -> None
        item = self.__item_for_node.get(node)
        if item is not None:
            return
        if self.__workflow is None:
            return

        if node not in self.__workflow.nodes:
            return

        if node in self.__init_queue:
            self.__init_queue.remove(node)

        item = Item(node, None, -1)
        # Insert on the node -> item mapping.
        self.__item_for_node[node] = item
        log.debug("Creating widget for node %s", node)
        try:
            w = self.create_widget_for_node(node)
        except Exception:  # pylint: disable=broad-except
            log.critical("", exc_info=True)
            lines = traceback.format_exception(*sys.exc_info())
            text = "".join(lines)
            errorwidget = QLabel(textInteractionFlags=Qt.TextSelectableByMouse,
                                 wordWrap=True,
                                 objectName="widgetmanager-error-placeholder",
                                 text="<pre>" + escape(text) + "</pre>")
            item.errorwidget = errorwidget
            node.set_state_message(UserMessage(text, UserMessage.Error, ""))
            raise
        else:
            item.widget = w
            self.__item_for_widget[w] = item

        self.__set_float_on_top_flag(w)

        if w.windowIcon().isNull():
            desc = node.description
            w.setWindowIcon(icon_loader.from_description(desc).get(desc.icon))
        if not w.windowTitle():
            w.setWindowTitle(node.title)

        w.installEventFilter(self.__activation_monitor)
        raise_canvas = QAction(
            self.tr("Raise Canvas to Front"),
            w,
            objectName="action-canvas-raise-canvas",
            toolTip=self.tr("Raise containing canvas workflow window"),
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Up))
        raise_canvas.triggered.connect(self.__on_activate_parent)
        raise_descendants = QAction(
            self.tr("Raise Descendants"),
            w,
            objectName="action-canvas-raise-descendants",
            toolTip=self.tr("Raise all immediate descendants of this node"),
            shortcut=QKeySequence(Qt.ControlModifier | Qt.ShiftModifier
                                  | Qt.Key_Right))
        raise_descendants.triggered.connect(
            partial(self.__on_raise_descendants, node))
        raise_ancestors = QAction(
            self.tr("Raise Ancestors"),
            w,
            objectName="action-canvas-raise-ancestors",
            toolTip=self.tr("Raise all immediate ancestors of this node"),
            shortcut=QKeySequence(Qt.ControlModifier | Qt.ShiftModifier
                                  | Qt.Key_Left))
        raise_ancestors.triggered.connect(
            partial(self.__on_raise_ancestors, node))
        w.addActions([raise_canvas, raise_descendants, raise_ancestors])

        # send all the post creation notification events
        workflow = self.__workflow
        assert workflow is not None
        inputs = workflow.find_links(sink_node=node)
        for link in inputs:
            ev = LinkEvent(LinkEvent.InputLinkAdded, link)
            QCoreApplication.sendEvent(w, ev)
        outputs = workflow.find_links(source_node=node)
        for link in outputs:
            ev = LinkEvent(LinkEvent.OutputLinkAdded, link)
            QCoreApplication.sendEvent(w, ev)

        self.widget_for_node_added.emit(node, w)

    def ensure_created(self, node):
        # type: (SchemeNode) -> None
        """
        Ensure that the widget for node is created.
        """
        if self.__workflow is None:
            return
        if node not in self.__workflow.nodes:
            return
        item = self.__item_for_node.get(node)
        if item is None:
            self.__add_widget_for_node(node)

    def __on_node_added(self, node):
        # type: (SchemeNode) -> None
        assert self.__workflow is not None
        assert node in self.__workflow.nodes
        assert node not in self.__item_for_node
        self.__add_node(node)

    def __add_node(self, node):
        # type: (SchemeNode) -> None
        # add node for tracking
        node.installEventFilter(self)
        if self.__creation_policy == WidgetManager.Immediate:
            self.ensure_created(node)
        else:
            self.__init_queue.append(node)
            if self.__creation_policy == WidgetManager.Normal:
                self.__init_timer.start()

    def __on_node_removed(self, node):  # type: (SchemeNode) -> None
        assert self.__workflow is not None
        assert node not in self.__workflow.nodes
        self.__remove_node(node)

    def __remove_node(self, node):  # type: (SchemeNode) -> None
        # remove the node and its widget from tracking.
        node.removeEventFilter(self)
        if node in self.__init_queue:
            self.__init_queue.remove(node)
        item = self.__item_for_node.get(node)

        if item is not None and item.widget is not None:
            widget = item.widget
            assert widget in self.__item_for_widget
            del self.__item_for_widget[widget]
            widget.removeEventFilter(self.__activation_monitor)
            item.widget = None
            self.widget_for_node_removed.emit(node, widget)
            self.delete_widget_for_node(node, widget)

        if item is not None:
            del self.__item_for_node[node]

    @Slot()
    def __process_init_queue(self):
        if self.__init_queue:
            node = self.__init_queue.popleft()
            assert self.__workflow is not None
            assert node in self.__workflow.nodes
            log.debug("__process_init_queue: '%s'", node.title)
            try:
                self.ensure_created(node)
            finally:
                if self.__init_queue:
                    self.__init_timer.start()

    def __on_link_added(self, link):  # type: (SchemeLink) -> None
        assert self.__workflow is not None
        assert link.source_node in self.__workflow.nodes
        assert link.sink_node in self.__workflow.nodes
        source = self.__item_for_node.get(link.source_node)
        sink = self.__item_for_node.get(link.sink_node)
        # notify the node gui of an added link
        if source is not None and source.widget is not None:
            ev = LinkEvent(LinkEvent.OutputLinkAdded, link)
            QCoreApplication.sendEvent(source.widget, ev)
        if sink is not None and sink.widget is not None:
            ev = LinkEvent(LinkEvent.InputLinkAdded, link)
            QCoreApplication.sendEvent(sink.widget, ev)

    def __on_link_removed(self, link):  # type: (SchemeLink) -> None
        assert self.__workflow is not None
        assert link.source_node in self.__workflow.nodes
        assert link.sink_node in self.__workflow.nodes
        source = self.__item_for_node.get(link.source_node)
        sink = self.__item_for_node.get(link.sink_node)
        # notify the node gui of an removed link
        if source is not None and source.widget is not None:
            ev = LinkEvent(LinkEvent.OutputLinkRemoved, link)
            QCoreApplication.sendEvent(source.widget, ev)
        if sink is not None and sink.widget is not None:
            ev = LinkEvent(LinkEvent.InputLinkRemoved, link)
            QCoreApplication.sendEvent(sink.widget, ev)

    def __mark_activated(self, widget):  # type: (QWidget) ->  None
        # Update the tracked stacking order for `widget`
        item = self.__item_for_widget.get(widget)
        if item is not None:
            item.activation_order = next(self.__activation_counter)

    def activate_widget_for_node(self, node, widget):
        # type: (SchemeNode, QWidget) -> None
        """
        Activate the widget for node (show and raise above other)
        """
        if widget.windowState() == Qt.WindowMinimized:
            widget.showNormal()
        widget.setVisible(True)
        widget.raise_()
        widget.activateWindow()

    def activate_window_group(self, group):
        # type: (Scheme.WindowGroup) -> None
        self.restore_window_state(group.state)

    def raise_widgets_to_front(self):
        """
        Raise all current visible widgets to the front.

        The widgets will be stacked by activation order.
        """
        workflow = self.__workflow
        if workflow is None:
            return

        items = filter(
            lambda item:
            (item.widget.isVisible()
             if item is not None and item.widget is not None else False),
            map(self.__item_for_node.get, workflow.nodes))
        self.__raise_and_activate(items)

    def set_float_widgets_on_top(self, float_on_top):
        """
        Set `Float Widgets on Top` flag on all widgets.
        """
        self.__float_widgets_on_top = float_on_top
        for item in self.__item_for_node.values():
            if item.widget is not None:
                self.__set_float_on_top_flag(item.widget)

    def save_window_state(self):
        # type: () -> List[Tuple[SchemeNode, bytes]]
        """
        Save current open window arrangement.
        """
        if self.__workflow is None:
            return []

        workflow = self.__workflow  # type: Scheme
        state = []
        for node in workflow.nodes:  # type: SchemeNode
            item = self.__item_for_node.get(node, None)
            if item is None:
                continue
            stackorder = item.activation_order
            if item.widget is not None and not item.widget.isHidden():
                data = self.save_widget_geometry(node, item.widget)
                state.append((stackorder, node, data))

        return [(node, data)
                for _, node, data in sorted(state, key=lambda t: t[0])]

    def restore_window_state(self, state):
        # type: (List[Tuple[Node, bytes]]) -> None
        """
        Restore the window state.
        """
        assert self.__workflow is not None
        workflow = self.__workflow  # type: Scheme
        visible = {node for node, _ in state}
        # first hide all other widgets
        for node in workflow.nodes:
            if node not in visible:
                # avoid creating widgets if not needed
                item = self.__item_for_node.get(node, None)
                if item is not None and item.widget is not None:
                    item.widget.hide()
        allnodes = set(workflow.nodes)
        # restore state for visible group; windows are stacked as they appear
        # in the state list.
        w = None
        for node, node_state in filter(lambda t: t[0] in allnodes, state):
            w = self.widget_for_node(node)  # also create it if needed
            if w is not None:
                w.show()
                self.restore_widget_geometry(node, w, node_state)
                w.raise_()
                self.__mark_activated(w)

        # activate (give focus to) the last window
        if w is not None:
            w.activateWindow()

    def save_widget_geometry(self, node, widget):
        # type: (SchemeNode, QWidget) -> bytes
        """
        Save and return the current geometry and state for node.
        """
        return b''

    def restore_widget_geometry(self, node, widget, state):
        # type: (SchemeNode, QWidget, bytes) -> bool
        """
        Restore the widget geometry and state for node.

        Return True if the geometry was restored successfully.

        The default implementation does nothing.
        """
        return False

    @Slot(SchemeNode)
    def __on_raise_ancestors(self, node):
        # type: (SchemeNode) -> None
        """
        Raise all the ancestor widgets of `widget`.
        """
        item = self.__item_for_node.get(node)
        if item is not None:
            scheme = self.scheme()
            assert scheme is not None
            ancestors = [
                self.__item_for_node.get(p) for p in scheme.parents(item.node)
            ]
            self.__raise_and_activate(filter(None, reversed(ancestors)))

    @Slot(SchemeNode)
    def __on_raise_descendants(self, node):
        # type: (SchemeNode) -> None
        """
        Raise all the descendants widgets of `widget`.
        """
        item = self.__item_for_node.get(node)
        if item is not None:
            scheme = self.scheme()
            assert scheme is not None
            descendants = [
                self.__item_for_node.get(p) for p in scheme.children(item.node)
            ]
            self.__raise_and_activate(filter(None, reversed(descendants)))

    def __raise_and_activate(self, items):
        # type: (Iterable[Item]) -> None
        """Show and raise a set of widgets."""
        # preserve the tracked stacking order
        items = sorted(items, key=lambda item: item.activation_order)
        w = None
        for item in items:
            if item.widget is not None:
                w = item.widget
            elif item.errorwidget is not None:
                w = item.errorwidget
            else:
                continue
            w.show()
            w.raise_()
        if w is not None:
            # give focus to the last activated top window
            w.activateWindow()

    def __activate_widget_for_node(self, node):  # type: (SchemeNode) -> None
        # activate the widget for the node.
        self.ensure_created(node)
        item = self.__item_for_node.get(node)
        if item is None:
            return
        if item.widget is not None:
            self.activate_widget_for_node(node, item.widget)
        elif item.errorwidget is not None:
            item.errorwidget.show()
            item.errorwidget.raise_()
            item.errorwidget.activateWindow()

    def __on_activate_parent(self):
        event = WorkflowEvent(WorkflowEvent.ActivateParentRequest)
        QCoreApplication.sendEvent(self.scheme(), event)

    def eventFilter(self, recv, event):
        # type: (QObject, QEvent) -> bool
        if isinstance(recv, SchemeNode):
            if event.type() == NodeEvent.NodeActivateRequest:
                self.__activate_widget_for_node(recv)
            self.__dispatch_events(recv, event)
        return False

    def __dispatch_events(self, node: Node, event: QEvent) -> None:
        """
        Dispatch relevant workflow events to the GUI widget
        """
        if event.type() in (
                WorkflowEvent.InputLinkAdded,
                WorkflowEvent.InputLinkRemoved,
                WorkflowEvent.InputLinkStateChange,
                WorkflowEvent.OutputLinkAdded,
                WorkflowEvent.OutputLinkRemoved,
                WorkflowEvent.OutputLinkStateChange,
                WorkflowEvent.NodeStateChange,
                WorkflowEvent.WorkflowEnvironmentChange,
        ):
            item = self.__item_for_node.get(node)
            if item is not None and item.widget is not None:
                QCoreApplication.sendEvent(item.widget, event)

    def __set_float_on_top_flag(self, widget):
        # type: (QWidget) -> None
        """Set or unset widget's float on top flag"""
        should_float_on_top = self.__float_widgets_on_top
        float_on_top = bool(widget.windowFlags() & Qt.WindowStaysOnTopHint)

        if float_on_top == should_float_on_top:
            return

        widget_was_visible = widget.isVisible()
        if should_float_on_top:
            widget.setWindowFlags(widget.windowFlags()
                                  | Qt.WindowStaysOnTopHint)
        else:
            widget.setWindowFlags(widget.windowFlags()
                                  & ~Qt.WindowStaysOnTopHint)

        # Changing window flags hid the widget
        if widget_was_visible:
            widget.show()

    def __on_env_changed(self, key, newvalue, oldvalue):
        # Notify widgets of a runtime environment change
        for item in self.__item_for_node.values():
            if item.widget is not None:
                ev = WorkflowEnvChanged(key, newvalue, oldvalue)
                QCoreApplication.sendEvent(item.widget, ev)

    def actions_for_context_menu(self, node):
        # type: (SchemeNode) -> List[QAction]
        """
        Return a list of extra actions that can be inserted into context
        menu in the workflow editor.

        Subclasses can reimplement this method to extend the default context
        menu.

        Parameters
        ----------
        node: SchemeNode
            The node for which the context menu is requested.

        Return
        ------
        actions: List[QAction]
            Actions that are appended to the default menu.
        """
        return []
예제 #21
0
class TrialTimeline(BaseWidget):

    COL_MSGTYPE = 0
    COL_PCTIME = 1
    COL_INITTIME = 2
    COL_FINALTIME = 3
    COL_MSG = 4
    COL_INFO = 5

    def __init__(self, session: Session):
        BaseWidget.__init__(self, session.name)
        self.set_margin(5)

        self.session = session

        self._reload = ControlButton('Reload')
        self._graph = ControlMatplotlib('Value')

        self._timer = QTimer()
        self._timer.timeout.connect(self.update)

        self._read = 0
        self._deltas = None

        self._last_trial_end = None

        self._states_dict = {}
        self._trials_list = []

        self.formset = ['_graph']
        self.colors = list(mcolors.CSS4_COLORS.values())

        self._graph.on_draw = self.__on_draw_evt
        self._reload.value = self.__reload_evt

    def __reload_evt(self):
        if self._timer.isActive():
            self._timer.stop()
        else:
            self._timer.start(conf.TRIALTIMELINE_PLUGIN_REFRESH_RATE)

    def show(self, detached=False):
        if self.session.is_running and self.session.setup.detached:
            return

        # Prevent the call to be recursive because of the mdi_area
        if not detached:
            if hasattr(self, '_show_called'):
                BaseWidget.show(self)
                return
            self._show_called = True
            self.mainwindow.mdi_area += self
            del self._show_called
        else:
            BaseWidget.show(self)

        self._stop = False  # flag used to close the gui in the middle of a loading
        if not self._stop and self.session.is_running:
            self._timer.start(conf.TRIALTIMELINE_PLUGIN_REFRESH_RATE)

        self.update()

    def hide(self):
        self._timer.stop()
        self._stop = True

    def read_data(self):

        res = self.session.data.query(
            "TYPE in ['END-TRIAL', 'STATE'] or (TYPE == 'INFO' and MSG in ['SESSION-ENDED','TRIAL-BPOD-TIME'])"
        )

        for index, msg in res.iterrows():
            if index <= self._read: continue

            if msg[self.COL_MSGTYPE] == EndTrial.MESSAGE_TYPE_ALIAS:

                if self._deltas is not None:
                    self._trials_list.append(self._deltas)
                self._deltas = {}

            elif msg[self.COL_MSGTYPE] == StateOccurrence.MESSAGE_TYPE_ALIAS:

                state = msg[self.COL_MSG]
                delta = float(msg[self.COL_FINALTIME]) - float(
                    msg[self.COL_INITTIME])

                if state not in self._deltas:
                    # count, delta sum, min delta, max delta
                    self._deltas[state] = [delta]
                    self._states_dict[state] = True
                else:
                    self._deltas[state].append(delta)

            elif msg[self.COL_MSGTYPE] == SessionInfo.MESSAGE_TYPE_ALIAS and \
                 msg[self.COL_MSG]     == APISession.INFO_SESSION_ENDED:

                if self._deltas is not None:
                    self._trials_list.append(self._deltas)

            elif msg[self.COL_MSGTYPE] == SessionInfo.MESSAGE_TYPE_ALIAS and \
                 msg[self.COL_MSG] == APISession.INFO_TRIAL_BPODTIME:

                trial_start = msg[self.COL_INITTIME]
                trial_end = msg[self.COL_FINALTIME]

                if self._last_trial_end is not None:
                    delta = float(trial_start) - float(self._last_trial_end)
                    self._deltas['Init lagging'] = [delta]
                    self._states_dict['Init lagging'] = True

                self._last_trial_end = trial_end

            self._read = index

    def __on_draw_evt(self, figure):

        try:
            axes = figure.add_subplot(111)
            axes.clear()

            trials_labels = []
            states_labels = list(self._states_dict.keys())

            num_states = len(self._states_dict)
            num_trials = len(self._trials_list)
            data = np.zeros((num_states, num_trials))
            errors = np.zeros((num_states, num_trials))

            for i, states in enumerate(self._trials_list):
                trials_labels.append('Trial {0}'.format(i))

                for j, state_label in enumerate(states_labels):

                    state_data = states.get(state_label, None)

                    if state_data is not None:
                        data[j][i] = np.mean(state_data)
                        errors[j][i] = np.std(state_data)

            colors = {}
            offset = np.zeros(len(trials_labels))
            trials_indexes = np.array(range(len(trials_labels)))

            for i, states_data in enumerate(data):
                if len(states_data) == 0: continue

                state_label = states_labels[i]
                if state_label not in colors:
                    colors[state_label] = self.colors[len(colors)]

                axes.barh(trials_indexes,
                          states_data,
                          height=0.8,
                          color=colors[state_label],
                          left=offset,
                          label=state_label,
                          yerr=errors[i])
                offset = offset + states_data

            axes.set_yticks(trials_indexes)
            axes.set_yticklabels(trials_labels)
            axes.set_ylabel('Trials')
            axes.set_xlabel('Time (sec)')
            axes.legend(loc="upper right")
            self._graph.repaint()
        except:
            self.critical(traceback.format_exc(), 'An error occurred')

    '''Takes care of all the session data and transforms it in a graph to be shown in the GUI'''

    def update(self):
        if not self.session.is_running:
            self._timer.stop()
        self.read_data()
        self._graph.draw()

    @property
    def mainwindow(self):
        return self.session.mainwindow

    @property
    def title(self):
        return BaseWidget.title.fget(self)

    @title.setter
    def title(self, value):
        BaseWidget.title.fset(self, 'Trial Timeline: {0}'.format(value))