source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11640:1c517c9b2c3f

Revision 11640:1c517c9b2c3f, 25.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 9 months ago (diff)

Added 'processingState' to tracked widget state.

Line 
1"""
2Widgets Scheme
3==============
4
5A Scheme for Orange Widgets Scheme (.ows).
6
7This is a subclass of the general :class:`Scheme`. It is responsible for
8the construction and management of OWBaseWidget instances corresponding
9to the scheme nodes, as well as delegating the signal propagation to a
10companion :class:`WidgetsSignalManager` class.
11
12.. autoclass:: WidgetsScheme
13   :bases:
14
15.. autoclass:: WidgetsSignalManager
16  :bases:
17
18"""
19import sys
20import logging
21
22import sip
23from PyQt4.QtGui import (
24    QShortcut, QKeySequence, QWhatsThisClickedEvent, QWidget
25)
26
27from PyQt4.QtCore import Qt, QObject, QCoreApplication, QEvent, SIGNAL
28from PyQt4.QtCore import pyqtSignal as Signal
29
30from .signalmanager import SignalManager, compress_signals, can_enable_dynamic
31from .scheme import Scheme, SchemeNode
32from .node import UserMessage
33from ..utils import name_lookup
34from ..resources import icon_loader
35
36log = logging.getLogger(__name__)
37
38
39class WidgetsScheme(Scheme):
40    """
41    A Scheme containing Orange Widgets managed with a `WidgetsSignalManager`
42    instance.
43
44    Extends the base `Scheme` class to handle the lifetime
45    (creation/deletion, etc.) of `OWBaseWidget` instances corresponding to
46    the nodes in the scheme. It also delegates the interwidget signal
47    propagation to an instance of `WidgetsSignalManager`.
48
49    """
50    def __init__(self, parent=None, title=None, description=None):
51        Scheme.__init__(self, parent, title, description)
52
53        self.signal_manager = WidgetsSignalManager(self)
54        self.widget_manager = WidgetManager(self)
55        self.widget_manager.set_scheme(self)
56
57    def widget_for_node(self, node):
58        """
59        Return the OWWidget instance for a `node`
60        """
61        return self.widget_manager.widget_for_node(node)
62
63    def node_for_widget(self, widget):
64        """
65        Return the SchemeNode instance for the `widget`.
66        """
67        return self.widget_manager.node_for_widget(widget)
68
69    def sync_node_properties(self):
70        """
71        Sync the widget settings/properties with the SchemeNode.properties.
72        Return True if there were any changes in the properties (i.e. if the
73        new node.properties differ from the old value) and False otherwise.
74
75        .. note:: this should hopefully be removed in the feature, when the
76            widget can notify a changed setting property.
77
78        """
79        changed = False
80        for node in self.nodes:
81            widget = self.widget_for_node(node)
82            settings = widget.getSettings(alsoContexts=False)
83            if settings != node.properties:
84                node.properties = settings
85                changed = True
86        log.debug("Scheme node properties sync (changed: %s)", changed)
87        return changed
88
89    def save_to(self, stream, pretty=True, pickle_fallback=False):
90        """
91        Reimplemented from :func:`Scheme.save_to`.
92        """
93        self.sync_node_properties()
94        Scheme.save_to(self, stream, pretty, pickle_fallback)
95
96
97class WidgetManager(QObject):
98    """
99    OWWidget instance manager class.
100
101    This class handles the lifetime of OWWidget instances in a
102    :class:`WidgetsScheme`.
103
104    """
105    #: A new OWWidget was created and added by the manager.
106    widget_for_node_added = Signal(SchemeNode, QWidget)
107
108    #: An OWWidget was removed, hidden and will be deleted when appropriate.
109    widget_for_node_removed = Signal(SchemeNode, QWidget)
110
111    #: Widget processing state flags:
112    #:   * InputUpdate - signal manager is updating/setting the
113    #:     widget's inputs
114    #:   * BlockingUpdate - widget has entered a blocking state
115    #:   * ProcessingUpdate - widget has entered processing state
116    InputUpdate, BlockingUpdate, ProcessingUpdate = 1, 2, 4
117
118    def __init__(self, parent):
119        QObject.__init__(self, parent)
120        self.__scheme = None
121        self.__signal_manager = None
122        self.__widgets = []
123        self.__widget_for_node = {}
124        self.__node_for_widget = {}
125
126        # Widgets that were 'removed' from the scheme but were at
127        # the time in an input update loop and could not be deleted
128        # immediately
129        self.__delay_delete = set()
130
131        # processing state flags for all nodes (including the ones
132        # in __delay_delete).
133        self.__widget_processing_state = {}
134
135        # Tracks the widget in the update loop by the SignalManager
136        self.__updating_widget = None
137
138    def set_scheme(self, scheme):
139        """
140        Set the :class:`WidgetsScheme` instance to manage.
141        """
142        self.__scheme = scheme
143        self.__signal_manager = scheme.findChild(SignalManager)
144
145        self.__signal_manager.processingStarted[SchemeNode].connect(
146            self.__on_processing_started
147        )
148        self.__signal_manager.processingFinished[SchemeNode].connect(
149            self.__on_processing_finished
150        )
151        scheme.node_added.connect(self.add_widget_for_node)
152        scheme.node_removed.connect(self.remove_widget_for_node)
153        scheme.installEventFilter(self)
154
155    def scheme(self):
156        """
157        Return the scheme instance on which this manager is installed.
158        """
159        return self.__scheme
160
161    def signal_manager(self):
162        """
163        Return the signal manager in use on the :func:`scheme`.
164        """
165        return self.__signal_manager
166
167    def widget_for_node(self, node):
168        """
169        Return the OWWidget instance for the scheme node.
170        """
171        return self.__widget_for_node[node]
172
173    def node_for_widget(self, widget):
174        """
175        Return the SchemeNode instance for the OWWidget.
176
177        Raise a KeyError if the widget does not map to a node in the scheme.
178        """
179        return self.__node_for_widget[widget]
180
181    def add_widget_for_node(self, node):
182        """
183        Create a new OWWidget instance for the corresponding scheme node.
184        """
185        widget = self.create_widget_instance(node)
186
187        self.__widgets.append(widget)
188        self.__widget_for_node[node] = widget
189        self.__node_for_widget[widget] = node
190
191        self.widget_for_node_added.emit(node, widget)
192
193    def remove_widget_for_node(self, node):
194        """
195        Remove the OWWidget instance for node.
196        """
197        widget = self.widget_for_node(node)
198
199        self.__widgets.remove(widget)
200        del self.__widget_for_node[node]
201        del self.__node_for_widget[widget]
202
203        self.widget_for_node_removed.emit(node, widget)
204
205        self._delete_widget(widget)
206
207    def _delete_widget(self, widget):
208        """
209        Delete the OWBaseWidget instance.
210        """
211        widget.close()
212
213        # Save settings to user global settings.
214        if not widget._settingsFromSchema:
215            widget.saveSettings()
216
217        # Notify the widget it will be deleted.
218        widget.onDeleteWidget()
219
220        if self.__widget_processing_state[widget] != 0:
221            # If the widget is in an update loop and/or blocking we
222            # delay the scheduled deletion until the widget is done.
223            self.__delay_delete.add(widget)
224        else:
225            widget.deleteLater()
226
227    def create_widget_instance(self, node):
228        """
229        Create a OWWidget instance for the node.
230        """
231        desc = node.description
232        klass = name_lookup(desc.qualified_name)
233
234        log.info("Creating %r instance.", klass)
235        widget = klass.__new__(
236            klass,
237            _owInfo=True,
238            _owWarning=True,
239            _owError=True,
240            _owShowStatus=True,
241            _useContexts=True,
242            _category=desc.category,
243            _settingsFromSchema=node.properties
244        )
245
246        # Init the node/widget mapping and state before calling __init__
247        # Some OWWidgets might already send data in the constructor
248        # (should this be forbidden? Raise a warning?) triggering the signal
249        # manager which would request the widget => node mapping or state
250        self.__widget_for_node[node] = widget
251        self.__node_for_widget[widget] = node
252        self.__widget_processing_state[widget] = 0
253
254        widget.__init__(None, self.signal_manager())
255        widget.setCaption(node.title)
256        widget.widgetInfo = desc
257
258        widget.setWindowIcon(
259            icon_loader.from_description(desc).get(desc.icon)
260        )
261
262        widget.setVisible(node.properties.get("visible", False))
263
264        node.title_changed.connect(widget.setCaption)
265
266        # Widget's info/warning/error messages.
267        widget.widgetStateChanged.connect(self.__on_widget_state_changed)
268
269        # Widget's progress bar value state.
270        widget.progressBarValueChanged.connect(node.set_progress)
271
272        # Widget processing state (progressBarInit/Finished)
273        # and the blocking state.
274        widget.processingStateChanged.connect(
275            self.__on_processing_state_changed
276        )
277        self.connect(widget,
278                     SIGNAL("blockingStateChanged(bool)"),
279                     self.__on_blocking_state_changed)
280
281        # Install a help shortcut on the widget
282        help_shortcut = QShortcut(QKeySequence("F1"), widget)
283        help_shortcut.activated.connect(self.__on_help_request)
284
285        return widget
286
287    def node_processing_state(self, node):
288        """
289        Return the processing state flags for the node.
290
291        Same as `manager.node_processing_state(manger.widget_for_node(node))`
292
293        """
294        widget = self.widget_for_node(node)
295        return self.__widget_processing_state[widget]
296
297    def widget_processing_state(self, widget):
298        """
299        Return the processing state flags for the widget.
300
301        The state is an bitwise or of `InputUpdate` and `BlockingUpdate`.
302
303        """
304        return self.__widget_processing_state[widget]
305
306    def eventFilter(self, receiver, event):
307        if receiver is self.__scheme and event.type() == QEvent.Close:
308            self.signal_manager().stop()
309
310            # Notify the widget instances.
311            for widget in self.__widget_for_node.values():
312                widget.close()
313
314                if not widget._settingsFromSchema:
315                    # First save global settings if necessary.
316                    widget.saveSettings()
317
318                widget.onDeleteWidget()
319
320            event.accept()
321            return True
322
323        return QObject.eventFilter(self, receiver, event)
324
325    def __on_help_request(self):
326        """
327        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to
328        the scheme and hope someone responds to it.
329
330        """
331        # Sender is the QShortcut, and parent the OWBaseWidget
332        widget = self.sender().parent()
333        try:
334            node = self.node_for_widget(widget)
335        except KeyError:
336            pass
337        else:
338            url = "help://search?id={0}".format(node.description.id)
339            event = QWhatsThisClickedEvent(url)
340            QCoreApplication.sendEvent(self.scheme(), event)
341
342    def __on_widget_state_changed(self, message_type, message_id,
343                                  message_value):
344        """
345        The OWBaseWidget info/warning/error state has changed.
346
347        message_type is one of "Info", "Warning" or "Error" string depending
348        of which method (information, warning, error) was called. message_id
349        is the first int argument if supplied, and message_value the message
350        text.
351
352        """
353        widget = self.sender()
354        try:
355            node = self.node_for_widget(widget)
356        except KeyError:
357            pass
358        else:
359            message_type = str(message_type)
360            if message_type == "Info":
361                contents = widget.widgetStateToHtml(True, False, False)
362                level = UserMessage.Info
363            elif message_type == "Warning":
364                contents = widget.widgetStateToHtml(False, True, False)
365                level = UserMessage.Warning
366            elif message_type == "Error":
367                contents = widget.widgetStateToHtml(False, False, True)
368                level = UserMessage.Error
369            else:
370                raise ValueError("Invalid message_type: %r" % message_type)
371
372            if not contents:
373                contents = None
374
375            message = UserMessage(contents, severity=level,
376                                  message_id=message_type,
377                                  data={"content-type": "text/html"})
378            node.set_state_message(message)
379
380    def __on_processing_state_changed(self, state):
381        """
382        A widget processing state has changed (progressBarInit/Finished)
383        """
384        widget = self.sender()
385        try:
386            node = self.node_for_widget(widget)
387        except KeyError:
388            return
389
390        if state:
391            self.__widget_processing_state[widget] |= self.ProcessingUpdate
392        else:
393            self.__widget_processing_state[widget] &= ~self.ProcessingUpdate
394        self.__update_node_processing_state(node)
395
396    def __on_processing_started(self, node):
397        """
398        Signal manager entered the input update loop for the node.
399        """
400        widget = self.widget_for_node(node)
401        # Remember the widget instance. The node and the node->widget mapping
402        # can be removed between this and __on_processing_finished.
403        self.__updating_widget = widget
404        self.__widget_processing_state[widget] |= self.InputUpdate
405        self.__update_node_processing_state(node)
406
407    def __on_processing_finished(self, node):
408        """
409        Signal manager exited the input update loop for the node.
410        """
411        widget = self.__updating_widget
412        self.__widget_processing_state[widget] &= ~self.InputUpdate
413
414        if widget in self.__node_for_widget:
415            self.__update_node_processing_state(node)
416        elif widget in self.__delay_delete:
417            self.__try_delete(widget)
418        else:
419            raise ValueError("%r is not managed" % widget)
420
421        self.__updating_widget = None
422
423    def __on_blocking_state_changed(self, state):
424        """
425        OWWidget blocking state has changed.
426        """
427        if not state:
428            # schedule an update pass.
429            self.signal_manager()._update()
430
431        widget = self.sender()
432        if state:
433            self.__widget_processing_state[widget] |= self.BlockingUpdate
434        else:
435            self.__widget_processing_state[widget] &= ~self.BlockingUpdate
436
437        if widget in self.__node_for_widget:
438            node = self.node_for_widget(widget)
439            self.__update_node_processing_state(node)
440
441        elif widget in self.__delay_delete:
442            self.__try_delete(widget)
443
444    def __update_node_processing_state(self, node):
445        """
446        Update the `node.processing_state` to reflect the widget state.
447        """
448        state = self.node_processing_state(node)
449        node.set_processing_state(1 if state else 0)
450
451    def __try_delete(self, widget):
452        if self.__widget_processing_state[widget] == 0:
453            self.__delay_delete.remove(widget)
454            widget.deleteLater()
455            del self.__widget_processing_state[widget]
456
457
458class WidgetsSignalManager(SignalManager):
459    """
460    A signal manager for a WidgetsScheme.
461    """
462    def __init__(self, scheme):
463        SignalManager.__init__(self, scheme)
464
465        scheme.installEventFilter(self)
466
467        self.freezing = 0
468
469        self.__scheme_deleted = False
470
471        scheme.destroyed.connect(self.__on_scheme_destroyed)
472        scheme.node_added.connect(self.on_node_added)
473        scheme.node_removed.connect(self.on_node_removed)
474        scheme.link_added.connect(self.link_added)
475        scheme.link_removed.connect(self.link_removed)
476
477    def send(self, widget, channelname, value, signal_id):
478        """
479        send method compatible with OWBaseWidget.
480        """
481        scheme = self.scheme()
482        try:
483            node = scheme.node_for_widget(widget)
484        except KeyError:
485            # The Node/Widget was already removed from the scheme.
486            log.debug("Node for %r is not in the scheme.", widget)
487            return
488
489        try:
490            channel = node.output_channel(channelname)
491        except ValueError:
492            log.error("%r is not valid signal name for %r",
493                      channelname, node.description.name)
494            return
495
496        # Expand the signal_id with the unique widget id and the
497        # channel name. This is needed for OWBaseWidget's input
498        # handlers (Multiple flag).
499        signal_id = (widget.widgetId, channelname, signal_id)
500
501        SignalManager.send(self, node, channel, value, signal_id)
502
503    def is_blocking(self, node):
504        return self.scheme().widget_manager.node_processing_state(node) != 0
505
506    def send_to_node(self, node, signals):
507        """
508        Implementation of `SignalManager.send_to_node`.
509
510        Deliver input signals to an OWBaseWidget instance.
511
512        """
513        widget = self.scheme().widget_for_node(node)
514        self.process_signals_for_widget(node, widget, signals)
515
516    def compress_signals(self, signals):
517        """
518        Reimplemented from :func:`SignalManager.compress_signals`.
519        """
520        return compress_signals(signals)
521
522    def process_signals_for_widget(self, node, widget, signals):
523        """
524        Process new signals for the OWBaseWidget.
525        """
526        # This replaces the old OWBaseWidget.processSignals method
527
528        if sip.isdeleted(widget):
529            log.critical("Widget %r was deleted. Cannot process signals",
530                         widget)
531            return
532
533        if widget.processingHandler:
534            widget.processingHandler(widget, 1)
535
536        app = QCoreApplication.instance()
537
538        for signal in signals:
539            link = signal.link
540            value = signal.value
541
542            # Check and update the dynamic link state
543            if link.is_dynamic():
544                link.dynamic_enabled = can_enable_dynamic(link, value)
545                if not link.dynamic_enabled:
546                    # Send None instead
547                    value = None
548
549            handler = link.sink_channel.handler
550            if handler.startswith("self."):
551                handler = handler.split(".", 1)[1]
552
553            handler = getattr(widget, handler)
554
555            if link.sink_channel.single:
556                args = (value,)
557            else:
558                args = (value, signal.id)
559
560            log.debug("Process signals: calling %s.%s (from %s with id:%s)",
561                      type(widget).__name__, handler.__name__, link, signal.id)
562
563            app.setOverrideCursor(Qt.WaitCursor)
564            try:
565                handler(*args)
566            except Exception:
567                sys.excepthook(*sys.exc_info())
568                log.exception("Error calling '%s' of '%s'",
569                              handler.__name__, node.title)
570            finally:
571                app.restoreOverrideCursor()
572
573        app.setOverrideCursor(Qt.WaitCursor)
574        try:
575            widget.handleNewSignals()
576        except Exception:
577            sys.excepthook(*sys.exc_info())
578            log.exception("Error calling 'handleNewSignals()' of '%s'",
579                          node.title)
580        finally:
581            app.restoreOverrideCursor()
582
583        # TODO: Test if async processing works, then remove this
584        while widget.isBlocking():
585            self.thread().msleep(50)
586            app.processEvents()
587
588        if widget.processingHandler:
589            widget.processingHandler(widget, 0)
590
591    def scheduleSignalProcessing(self, widget=None):
592        """
593        Back compatibility with old orngSignalManager.
594        """
595        self._update()
596
597    def processNewSignals(self, widget=None):
598        """
599        Back compatibility with old orngSignalManager.
600
601        .. todo:: The old signal manager would update immediately, but
602                  this only schedules the update. Is this a problem?
603
604        """
605        self._update()
606
607    def addEvent(self, strValue, object=None, eventVerbosity=1):
608        """
609        Back compatibility with old orngSignalManager module's logging.
610        """
611        if not isinstance(strValue, basestring):
612            info = str(strValue)
613        else:
614            info = strValue
615
616        if object is not None:
617            info += ". Token type = %s. Value = %s" % \
618                    (str(type(object)), str(object)[:100])
619
620        if eventVerbosity > 0:
621            log.debug(info)
622        else:
623            log.info(info)
624
625    def getLinks(self, widgetFrom=None, widgetTo=None,
626                 signalNameFrom=None, signalNameTo=None):
627        """
628        Back compatibility with old orngSignalManager. Some widget look if
629        they have any output connections, so this is still needed, but should
630        be deprecated in the future.
631
632        """
633        scheme = self.scheme()
634
635        source_node = sink_node = None
636
637        if widgetFrom is not None:
638            source_node = scheme.node_for_widget[widgetFrom]
639        if widgetTo is not None:
640            sink_node = scheme.node_for_widget[widgetTo]
641
642        candidates = scheme.find_links(source_node=source_node,
643                                       sink_node=sink_node)
644
645        def signallink(link):
646            """
647            Construct SignalLink from an SchemeLink.
648            """
649            w1 = scheme.widget_for_node(link.source_node)
650            w2 = scheme.widget_for_node(link.sink_node)
651
652            # Input/OutputSignal are reused from description. Interface
653            # is almost the same as it was in orngSignalManager
654            return SignalLink(w1, link.source_channel,
655                              w2, link.sink_channel,
656                              link.enabled)
657
658        links = []
659        for link in candidates:
660            if (signalNameFrom is None or \
661                    link.source_channel.name == signalNameFrom) and \
662                    (signalNameTo is None or \
663                     link.sink_channel.name == signalNameTo):
664
665                links.append(signallink(link))
666        return links
667
668    def setFreeze(self, freeze, startWidget=None):
669        """
670        Freeze/unfreeze signal processing. If freeze >= 1 no signal will be
671        processed until freeze is set back to 0.
672
673        """
674        self.freezing = max(freeze, 0)
675        if freeze > 0:
676            log.debug("Freezing signal processing (value:%r set by %r)",
677                      freeze, startWidget)
678        elif freeze == 0:
679            log.debug("Unfreezing signal processing (cleared by %r)",
680                      startWidget)
681
682        if self._input_queue:
683            self._update()
684
685    def freeze(self, widget=None):
686        """
687        Return a context manager that freezes the signal processing.
688        """
689        manager = self
690
691        class freezer(object):
692            def __enter__(self):
693                self.push()
694                return self
695
696            def __exit__(self, *args):
697                self.pop()
698
699            def push(self):
700                manager.setFreeze(manager.freezing + 1, widget)
701
702            def pop(self):
703                manager.setFreeze(manager.freezing - 1, widget)
704
705        return freezer()
706
707    def event(self, event):
708        if event.type() == QEvent.UpdateRequest:
709            if self.freezing > 0:
710                log.debug("received UpdateRequest while signal processing "
711                          "is frozen")
712                event.setAccepted(False)
713                return False
714
715            if self.__scheme_deleted:
716                log.debug("Scheme has been/is being deleted. No more "
717                          "signals will be delivered to any nodes.")
718                event.setAccepted(True)
719                return True
720        # Retain a reference to the scheme until the 'process_queued' finishes
721        # in SignalManager.event.
722        scheme = self.scheme()
723        return SignalManager.event(self, event)
724
725    def eventFilter(self, receiver, event):
726        if receiver is self.scheme() and event.type() == QEvent.DeferredDelete:
727            if self.runtime_state() == SignalManager.Processing:
728                log.info("Deferring a 'DeferredDelete' event for the Scheme "
729                         "instance until SignalManager exits the current "
730                         "update loop.")
731                event.setAccepted(False)
732                self.processingFinished.connect(self.scheme().deleteLater)
733                self.__scheme_deleted = True
734                return True
735
736        return SignalManager.eventFilter(self, receiver, event)
737
738    def __on_scheme_destroyed(self, obj):
739        self.__scheme_deleted = True
740
741
742class SignalLink(object):
743    """
744    Back compatibility with old orngSignalManager, do not use.
745    """
746    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
747                 enabled=True, dynamic=False):
748        self.widgetFrom = widgetFrom
749        self.widgetTo = widgetTo
750
751        self.outputSignal = outputSignal
752        self.inputSignal = inputSignal
753
754        self.dynamic = dynamic
755
756        self.enabled = enabled
757
758        self.signalNameFrom = self.outputSignal.name
759        self.signalNameTo = self.inputSignal.name
760
761    def canEnableDynamic(self, obj):
762        """
763        Can dynamic signal link be enabled for `obj`?
764        """
765        return isinstance(obj, name_lookup(self.inputSignal.type))
766
767
768class SignalWrapper(object):
769    """
770    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
771    This disables (freezes) the widget's signal manager when slots are
772    invoked from GUI signals. Not sure if this is still needed, could instead
773    just set the blocking flag on the widget itself.
774
775    """
776    def __init__(self, widget, method):
777        self.widget = widget
778        self.method = method
779
780    def __call__(self, *args):
781        manager = self.widget.signalManager
782        if manager:
783            with manager.freeze(self.method):
784                self.method(*args)
785        else:
786            # Might be running stand alone without a manager.
787            self.method(*args)
Note: See TracBrowser for help on using the repository browser.