source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11465:5c3834e1d2ea

Revision 11465:5c3834e1d2ea, 20.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Delay the deletion of the Scheme and/or OWBaseWidget until SignalManager finishes the current update.

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