source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11614:a4aa7f9b09fd

Revision 11614:a4aa7f9b09fd, 22.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Refactored widget state messages handling.

Moved the resposibility for handling messages (OWBaseWidget.widgetStateChanged)
to WidgetsScheme.

Added support for messages in base SchemeNode class.

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 QShortcut, QKeySequence, QWhatsThisClickedEvent
24from PyQt4.QtCore import Qt, QCoreApplication, QEvent, SIGNAL
25
26from .signalmanager import SignalManager, compress_signals, can_enable_dynamic
27from .scheme import Scheme, SchemeNode
28from .node import UserMessage
29from .utils import name_lookup, check_arg, check_type
30from ..resources import icon_loader
31from ..config import rc
32
33log = logging.getLogger(__name__)
34
35
36class WidgetsScheme(Scheme):
37    """
38    A Scheme containing Orange Widgets managed with a `WidgetsSignalManager`
39    instance.
40
41    Extends the base `Scheme` class to handle the lifetime
42    (creation/deletion, etc.) of `OWBaseWidget` instances corresponding to
43    the nodes in the scheme. It also delegates the interwidget signal
44    propagation to an instance of `WidgetsSignalManager`.
45
46    """
47    def __init__(self, parent=None, title=None, description=None):
48        Scheme.__init__(self, parent, title, description)
49
50        self.widgets = []
51        self.widget_for_node = {}
52        self.node_for_widget = {}
53        self.signal_manager = WidgetsSignalManager(self)
54        self.signal_manager.processingStarted[SchemeNode].connect(
55            self.__on_processing_started
56        )
57        self.signal_manager.processingFinished[SchemeNode].connect(
58            self.__on_processing_finished
59        )
60
61    def add_node(self, node):
62        """
63        Add a `SchemeNode` instance to the scheme and create/initialize the
64        OWBaseWidget instance for it.
65
66        """
67        check_arg(node not in self.nodes, "Node already in scheme.")
68        check_type(node, SchemeNode)
69
70        # Create the widget before a call to Scheme.add_node in
71        # case someone connected to node_added already expects
72        # widget_for_node, etc. to be up to date.
73        widget = self.create_widget_instance(node)
74        Scheme.add_node(self, node)
75
76        self.widgets.append(widget)
77
78    def remove_node(self, node):
79        Scheme.remove_node(self, node)
80        widget = self.widget_for_node[node]
81
82        self.signal_manager.on_node_removed(node)
83
84        del self.widget_for_node[node]
85        del self.node_for_widget[widget]
86
87        widget.close()
88
89        # Save settings to user global settings.
90        if not widget._settingsFromSchema:
91            widget.saveSettings()
92
93        # Notify the widget it will be deleted.
94        widget.onDeleteWidget()
95        # And schedule it for deletion.
96        widget.deleteLater()
97
98    def add_link(self, link):
99        Scheme.add_link(self, link)
100        self.signal_manager.link_added(link)
101
102    def remove_link(self, link):
103        Scheme.remove_link(self, link)
104        self.signal_manager.link_removed(link)
105
106    def create_widget_instance(self, node):
107        """
108        Create a OWBaseWidget instance for the node.
109        """
110        desc = node.description
111        klass = name_lookup(desc.qualified_name)
112
113        log.info("Creating %r instance.", klass)
114        widget = klass.__new__(
115            klass,
116            _owInfo=rc.get("canvas.show-state-info", True),
117            _owWarning=rc.get("canvas.show-state-warning", True),
118            _owError=rc.get("canvas.show-state-error", True),
119            _owShowStatus=rc.get("OWWidget.show-status", True),
120            _useContexts=rc.get("OWWidget.use-contexts", True),
121            _category=desc.category,
122            _settingsFromSchema=node.properties
123        )
124
125        # Add the node/widget mapping s before calling __init__
126        # Some OWWidgets might already send data in the constructor
127        # (should this be forbidden? Raise a warning?)
128        self.signal_manager.on_node_added(node)
129
130        self.widget_for_node[node] = widget
131        self.node_for_widget[widget] = node
132
133        widget.__init__(None, self.signal_manager)
134        widget.setCaption(node.title)
135        widget.widgetInfo = desc
136
137        widget.setWindowIcon(
138            icon_loader.from_description(desc).get(desc.icon)
139        )
140
141        widget.setVisible(node.properties.get("visible", False))
142
143        node.title_changed.connect(widget.setCaption)
144
145        # Bind widgets progress/processing state back to the node's properties
146        widget.progressBarValueChanged.connect(node.set_progress)
147        widget.processingStateChanged.connect(node.set_processing_state)
148        widget.widgetStateChanged.connect(self.__on_widget_state_changed)
149        self.connect(widget,
150                     SIGNAL("blockingStateChanged(bool)"),
151                     self.signal_manager._update)
152
153        # Install a help shortcut on the widget
154        help_shortcut = QShortcut(QKeySequence("F1"), widget)
155        help_shortcut.activated.connect(self.__on_help_request)
156        return widget
157
158    def widget_settings(self):
159        """Return a list of dictionaries with widget settings.
160        """
161        return [self.widget_for_node[node].getSettings(alsoContexts=False)
162                for node in self.nodes]
163
164    def __on_widget_state_changed(self, message_type, message_id,
165                                  message_value):
166        """
167        The OWBaseWidget info/warning/error state has changed.
168
169        message_type is one of "Info", "Warning" or "Error" string depending
170        of which method (information, warning, error) was called. message_id
171        is the first int argument if supplied, and message_value the message
172        text.
173
174        """
175        widget = self.sender()
176        node = self.node_for_widget.get(widget)
177        if node is not None:
178            message_type = str(message_type)
179            if message_type == "Info":
180                contents = widget.widgetStateToHtml(True, False, False)
181                level = UserMessage.Info
182            elif message_type == "Warning":
183                contents = widget.widgetStateToHtml(False, True, False)
184                level = UserMessage.Warning
185            elif message_type == "Error":
186                contents = widget.widgetStateToHtml(False, False, True)
187                level = UserMessage.Error
188            else:
189                raise ValueError("Invalid message_type: %r" % message_type)
190
191            if not contents:
192                contents = None
193
194            message = UserMessage(contents, severity=level,
195                                  message_id=message_type,
196                                  data={"contents-type": "text/html"})
197            node.set_state_message(message)
198
199    def sync_node_properties(self):
200        """Sync the widget settings/properties with the SchemeNode.properties.
201        Return True if there were any changes in the properties (i.e. if the
202        new node.properties differ from the old value) and False otherwise.
203
204        .. note:: this should hopefully be removed in the feature, when the
205            widget can notify a changed setting property.
206
207        """
208        changed = False
209        for node in self.nodes:
210            widget = self.widget_for_node[node]
211            settings = widget.getSettings(alsoContexts=False)
212            if settings != node.properties:
213                node.properties = settings
214                changed = True
215        log.debug("Scheme node properties sync (changed: %s)", changed)
216        return changed
217
218    def save_to(self, stream, pretty=True, pickle_fallback=False):
219        self.sync_node_properties()
220        Scheme.save_to(self, stream, pretty, pickle_fallback)
221
222    def event(self, event):
223        """
224        Reimplemented from `QObject.event`.
225
226        Responds to QEvent.Close event by stopping signal processing and
227        closing all widgets.
228
229        """
230        if event.type() == QEvent.Close:
231            self.signal_manager.stop()
232
233            # Notify the widget instances.
234            for widget in self.widget_for_node.values():
235                widget.close()
236
237                if not widget._settingsFromSchema:
238                    # First save global settings if necessary.
239                    widget.saveSettings()
240
241                widget.onDeleteWidget()
242
243            event.accept()
244            return True
245        else:
246            return Scheme.event(self, event)
247
248    def __on_help_request(self):
249        """
250        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` and
251        hope someone responds to it.
252
253        """
254        # Sender is the QShortcut, and parent the OWBaseWidget
255        widget = self.sender().parent()
256        node = self.node_for_widget.get(widget)
257        if node:
258            url = "help://search?id={0}".format(node.description.id)
259            event = QWhatsThisClickedEvent(url)
260            QCoreApplication.sendEvent(self, event)
261
262    def __on_processing_started(self, node):
263        node.set_processing_state(1)
264
265    def __on_processing_finished(self, node):
266        node.set_processing_state(0)
267
268
269class WidgetsSignalManager(SignalManager):
270    def __init__(self, scheme):
271        SignalManager.__init__(self, scheme)
272
273        scheme.installEventFilter(self)
274        # We keep a mapping from node->widget after the node/widget has been
275        # removed from the scheme until we also process all the outgoing signal
276        # updates. The reason is the old OWBaseWidget's MULTI channel protocol
277        # where the actual source widget instance is passed to the signal
278        # handler, and in the delayed update the mapping in `scheme()` is no
279        # longer available.
280        self._widget_backup = {}
281        self._widgets_to_delete = set()
282        self._active_node = None
283        self.freezing = 0
284
285        self.__scheme_deleted = False
286        scheme.destroyed.connect(self.__on_scheme_destroyed)
287
288    def on_node_removed(self, node):
289        widget = self.scheme().widget_for_node[node]
290
291        assert not self.scheme().find_links(sink_node=node), \
292            "Node removed but still has input links"
293
294        signals = self.compress_signals(self.pending_input_signals(node))
295        if not all(signal.value is None for signal in signals):
296            log.error("Non 'None' signals pending for a removed node %r",
297                         node.title)
298
299        SignalManager.on_node_removed(self, node)
300
301        if self.runtime_state() == SignalManager.Processing and \
302                node is self._active_node or self.is_blocking(node):
303            # Delay the widget delete until it finishes.
304            # Keep a reference to the widget and install a filter.
305            self._widgets_to_delete.add(widget)
306            widget.installEventFilter(self)
307
308        # Store the node->widget mapping for possible delayed signal id.
309        # It will be removed in `process_queued` when all signals
310        # originating from this widget are delivered.
311        self._widget_backup[node] = widget
312
313    def send(self, widget, channelname, value, id):
314        """
315        send method compatible with OWBaseWidget.
316        """
317        scheme = self.scheme()
318
319        if widget not in scheme.node_for_widget:
320            # The Node/Widget was already removed from the scheme
321            return
322
323        node = scheme.node_for_widget[widget]
324
325        try:
326            channel = node.output_channel(channelname)
327        except ValueError:
328            log.error("%r is not valid signal name for %r",
329                      channelname, node.description.name)
330            return
331
332        SignalManager.send(self, node, channel, value, id)
333
334    def is_blocking(self, node):
335        return self.scheme().widget_for_node[node].isBlocking()
336
337    def send_to_node(self, node, signals):
338        """
339        Implementation of `SignalManager.send_to_node`. Deliver data signals
340        to OWBaseWidget instance.
341
342        """
343        if node in self.scheme().widget_for_node:
344            widget = self.scheme().widget_for_node[node]
345        else:
346            widget = self._widget_backup[node]
347
348        self._active_node = node
349        self.process_signals_for_widget(node, widget, signals)
350        self._active_node = None
351
352        if widget in self._widgets_to_delete:
353            # If this node/widget was removed during the
354            # 'process_signals_for_widget'
355            self._widgets_to_delete.remove(widget)
356            widget.deleteLater()
357
358    def compress_signals(self, signals):
359        return compress_signals(signals)
360
361    def process_queued(self, max_nodes=None):
362        SignalManager.process_queued(self, max_nodes=max_nodes)
363
364        # Remove node->widgets backup mapping no longer needed.
365        nodes_removed = set(self._widget_backup.keys())
366        sources_remaining = set(signal.link.source_node for
367                                signal in self._input_queue)
368
369        nodes_to_remove = nodes_removed - sources_remaining
370        for node in nodes_to_remove:
371            del self._widget_backup[node]
372
373    def process_signals_for_widget(self, node, widget, signals):
374        """
375        Process new signals for a OWBaseWidget.
376        """
377        # This replaces the old OWBaseWidget.processSignals method
378
379        if sip.isdeleted(widget):
380            log.critical("Widget %r was deleted. Cannot process signals",
381                         widget)
382            return
383
384        if widget.processingHandler:
385            widget.processingHandler(widget, 1)
386
387        scheme = self.scheme()
388        app = QCoreApplication.instance()
389
390        for signal in signals:
391            link = signal.link
392            value = signal.value
393
394            # Check and update the dynamic link state
395            if link.is_dynamic():
396                link.dynamic_enabled = can_enable_dynamic(link, value)
397                if not link.dynamic_enabled:
398                    # Send None instead
399                    value = None
400
401            handler = link.sink_channel.handler
402            if handler.startswith("self."):
403                handler = handler.split(".", 1)[1]
404
405            handler = getattr(widget, handler)
406
407            if link.sink_channel.single:
408                args = (value,)
409            else:
410                source_node = link.source_node
411                source_name = link.source_channel.name
412
413                if source_node in scheme.widget_for_node:
414                    source_widget = scheme.widget_for_node[source_node]
415                else:
416                    # Node is no longer in the scheme.
417                    source_widget = self._widget_backup[source_node]
418
419                # The old OWBaseWidget.processSignals sends the source widget
420                # instance along.
421                # TODO: Does any widget actually use it, or could it be
422                # removed (replaced with a unique id)?
423                args = (value, (source_widget, source_name, signal.id))
424
425            log.debug("Process signals: calling %s.%s (from %s with id:%s)",
426                      type(widget).__name__, handler.__name__, link, signal.id)
427
428            app.setOverrideCursor(Qt.WaitCursor)
429            try:
430                handler(*args)
431            except Exception:
432                sys.excepthook(*sys.exc_info())
433                log.exception("Error calling '%s' of '%s'",
434                              handler.__name__, node.title)
435            finally:
436                app.restoreOverrideCursor()
437
438        app.setOverrideCursor(Qt.WaitCursor)
439        try:
440            widget.handleNewSignals()
441        except Exception:
442            sys.excepthook(*sys.exc_info())
443            log.exception("Error calling 'handleNewSignals()' of '%s'",
444                          node.title)
445        finally:
446            app.restoreOverrideCursor()
447
448        # TODO: Test if async processing works, then remove this
449        while widget.isBlocking():
450            self.thread().msleep(50)
451            app.processEvents()
452
453        if widget.processingHandler:
454            widget.processingHandler(widget, 0)
455
456    def scheduleSignalProcessing(self, widget=None):
457        """
458        Back compatibility with old orngSignalManager.
459        """
460        self._update()
461
462    def processNewSignals(self, widget=None):
463        """
464        Back compatibility with old orngSignalManager.
465
466        .. todo:: The old signal manager would update immediately, but
467                  this only schedules the update. Is this a problem?
468
469        """
470        self._update()
471
472    def addEvent(self, strValue, object=None, eventVerbosity=1):
473        """
474        Back compatibility with old orngSignalManager module's logging.
475        """
476        if not isinstance(strValue, basestring):
477            info = str(strValue)
478        else:
479            info = strValue
480
481        if object is not None:
482            info += ". Token type = %s. Value = %s" % \
483                    (str(type(object)), str(object)[:100])
484
485        if eventVerbosity > 0:
486            log.debug(info)
487        else:
488            log.info(info)
489
490    def getLinks(self, widgetFrom=None, widgetTo=None,
491                 signalNameFrom=None, signalNameTo=None):
492        """
493        Back compatibility with old orngSignalManager. Some widget look if
494        they have any output connections, so this is still needed, but should
495        be deprecated in the future.
496
497        """
498        scheme = self.scheme()
499
500        source_node = sink_node = None
501
502        if widgetFrom is not None:
503            source_node = scheme.node_for_widget[widgetFrom]
504        if widgetTo is not None:
505            sink_node = scheme.node_for_widget[widgetTo]
506
507        candidates = scheme.find_links(source_node=source_node,
508                                       sink_node=sink_node)
509
510        def signallink(link):
511            """
512            Construct SignalLink from an SchemeLink.
513            """
514            w1 = scheme.widget_for_node[link.source_node]
515            w2 = scheme.widget_for_node[link.sink_node]
516
517            # Input/OutputSignal are reused from description. Interface
518            # is almost the same as it was in orngSignalManager
519            return SignalLink(w1, link.source_channel,
520                              w2, link.sink_channel,
521                              link.enabled)
522
523        links = []
524        for link in candidates:
525            if (signalNameFrom is None or \
526                    link.source_channel.name == signalNameFrom) and \
527                    (signalNameTo is None or \
528                     link.sink_channel.name == signalNameTo):
529
530                links.append(signallink(link))
531        return links
532
533    def setFreeze(self, freeze, startWidget=None):
534        """
535        Freeze/unfreeze signal processing. If freeze >= 1 no signal will be
536        processed until freeze is set back to 0.
537
538        """
539        self.freezing = max(freeze, 0)
540        if freeze > 0:
541            log.debug("Freezing signal processing (value:%r set by %r)",
542                      freeze, startWidget)
543        elif freeze == 0:
544            log.debug("Unfreezing signal processing (cleared by %r)",
545                      startWidget)
546
547        if self._input_queue:
548            self._update()
549
550    def freeze(self, widget=None):
551        """
552        Return a context manager that freezes the signal processing.
553        """
554        manager = self
555
556        class freezer(object):
557            def __enter__(self):
558                self.push()
559                return self
560
561            def __exit__(self, *args):
562                self.pop()
563
564            def push(self):
565                manager.setFreeze(manager.freezing + 1, widget)
566
567            def pop(self):
568                manager.setFreeze(manager.freezing - 1, widget)
569
570        return freezer()
571
572    def event(self, event):
573        if event.type() == QEvent.UpdateRequest:
574            if self.freezing > 0:
575                log.debug("received UpdateRequest while signal processing "
576                          "is frozen")
577                event.setAccepted(False)
578                return False
579
580            if self.__scheme_deleted:
581                log.debug("Scheme has been/is being deleted. No more "
582                          "signals will be delivered to any nodes.")
583                event.setAccepted(True)
584                return True
585        # Retain a reference to the scheme until the 'process_queued' finishes
586        # in SignalManager.event.
587        scheme = self.scheme()
588        return SignalManager.event(self, event)
589
590    def eventFilter(self, receiver, event):
591        if receiver is self.scheme() and event.type() == QEvent.DeferredDelete:
592            if self.runtime_state() == SignalManager.Processing:
593                log.info("Deferring a 'DeferredDelete' event for the Scheme "
594                         "instance until SignalManager exits the current "
595                         "update loop.")
596                event.setAccepted(False)
597                self.processingFinished.connect(self.scheme().deleteLater)
598                self.__scheme_deleted = True
599                return True
600        elif receiver in self._widgets_to_delete and \
601                event.type() == QEvent.DeferredDelete:
602            if self._widget_backup.get(self._active_node, None) is receiver:
603                # The widget is still being updated. We need to keep it alive,
604                # it will be deleted in `send_to_node`.
605                log.info("Deferring a 'DeferredDelete' until widget exits "
606                         "the 'process_signals_for_widget'.")
607                event.setAccepted(False)
608                return True
609
610        return SignalManager.eventFilter(self, receiver, event)
611
612    def __on_scheme_destroyed(self, obj):
613        self.__scheme_deleted = True
614
615
616class SignalLink(object):
617    """
618    Back compatibility with old orngSignalManager, do not use.
619    """
620    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
621                 enabled=True, dynamic=False):
622        self.widgetFrom = widgetFrom
623        self.widgetTo = widgetTo
624
625        self.outputSignal = outputSignal
626        self.inputSignal = inputSignal
627
628        self.dynamic = dynamic
629
630        self.enabled = enabled
631
632        self.signalNameFrom = self.outputSignal.name
633        self.signalNameTo = self.inputSignal.name
634
635    def canEnableDynamic(self, obj):
636        """
637        Can dynamic signal link be enabled for `obj`?
638        """
639        return isinstance(obj, name_lookup(self.inputSignal.type))
640
641
642class SignalWrapper(object):
643    """
644    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
645    This disables (freezes) the widget's signal manager when slots are
646    invoked from GUI signals. Not sure if this is still needed, could instead
647    just set the blocking flag on the widget itself.
648
649    """
650    def __init__(self, widget, method):
651        self.widget = widget
652        self.method = method
653
654    def __call__(self, *args):
655        manager = self.widget.signalManager
656        if manager:
657            with manager.freeze(self.method):
658                self.method(*args)
659        else:
660            # Might be running stand alone without a manager.
661            self.method(*args)
Note: See TracBrowser for help on using the repository browser.