source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11487:912e57db9317

Revision 11487:912e57db9317, 20.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Finalise the WidgetsScheme on a 'Close' event.

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