source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11563:2eb1d8d58768

Revision 11563:2eb1d8d58768, 21.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Let 'sys.excepthook' handle exceptions from widget.

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