source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11470:164497e2fd9a

Revision 11470:164497e2fd9a, 20.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Check if widget is still a part of the scheme.

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
263        if widget not in scheme.node_for_widget:
264            # The Node/Widget was already removed from the scheme
265            return
266
267        node = scheme.node_for_widget[widget]
268
269        try:
270            channel = node.output_channel(channelname)
271        except ValueError:
272            log.error("%r is not valid signal name for %r",
273                      channelname, node.description.name)
274            return
275
276        SignalManager.send(self, node, channel, value, id)
277
278    def is_blocking(self, node):
279        return self.scheme().widget_for_node[node].isBlocking()
280
281    def send_to_node(self, node, signals):
282        """
283        Implementation of `SignalManager.send_to_node`. Deliver data signals
284        to OWBaseWidget instance.
285
286        """
287        if node in self.scheme().widget_for_node:
288            widget = self.scheme().widget_for_node[node]
289        else:
290            widget = self._widget_backup[node]
291
292        self._active_node = node
293        self.process_signals_for_widget(node, widget, signals)
294        self._active_node = None
295
296        if widget in self._widgets_to_delete:
297            # If this node/widget was removed during the
298            # 'process_signals_for_widget'
299            self._widgets_to_delete.remove(widget)
300            widget.deleteLater()
301
302    def compress_signals(self, signals):
303        return compress_signals(signals)
304
305    def process_queued(self, max_nodes=None):
306        SignalManager.process_queued(self, max_nodes=max_nodes)
307
308        # Remove node->widgets backup mapping no longer needed.
309        nodes_removed = set(self._widget_backup.keys())
310        sources_remaining = set(signal.link.source_node for
311                                signal in self._input_queue)
312
313        nodes_to_remove = nodes_removed - sources_remaining
314        for node in nodes_to_remove:
315            del self._widget_backup[node]
316
317    def process_signals_for_widget(self, node, widget, signals):
318        """
319        Process new signals for a OWBaseWidget.
320        """
321        # This replaces the old OWBaseWidget.processSignals method
322
323        if sip.isdeleted(widget):
324            log.critical("Widget %r was deleted. Cannot process signals",
325                         widget)
326            return
327
328        if widget.processingHandler:
329            widget.processingHandler(widget, 1)
330
331        scheme = self.scheme()
332        app = QCoreApplication.instance()
333
334        for signal in signals:
335            link = signal.link
336            value = signal.value
337
338            # Check and update the dynamic link state
339            if link.is_dynamic():
340                link.dynamic_enabled = can_enable_dynamic(link, value)
341                if not link.dynamic_enabled:
342                    # Send None instead
343                    value = None
344
345            handler = link.sink_channel.handler
346            if handler.startswith("self."):
347                handler = handler.split(".", 1)[1]
348
349            handler = getattr(widget, handler)
350
351            if link.sink_channel.single:
352                args = (value,)
353            else:
354                source_node = link.source_node
355                source_name = link.source_channel.name
356
357                if source_node in scheme.widget_for_node:
358                    source_widget = scheme.widget_for_node[source_node]
359                else:
360                    # Node is no longer in the scheme.
361                    source_widget = self._widget_backup[source_node]
362
363                # The old OWBaseWidget.processSignals sends the source widget
364                # instance along.
365                # TODO: Does any widget actually use it, or could it be
366                # removed (replaced with a unique id)?
367                args = (value, (source_widget, source_name, signal.id))
368
369            log.debug("Process signals: calling %s.%s (from %s with id:%s)",
370                      type(widget).__name__, handler.__name__, link, signal.id)
371
372            app.setOverrideCursor(Qt.WaitCursor)
373            try:
374                handler(*args)
375            except Exception:
376                log.exception("Error")
377            finally:
378                app.restoreOverrideCursor()
379
380        app.setOverrideCursor(Qt.WaitCursor)
381        try:
382            widget.handleNewSignals()
383        except Exception:
384            log.exception("Error")
385        finally:
386            app.restoreOverrideCursor()
387
388        # TODO: Test if async processing works, then remove this
389        while widget.isBlocking():
390            self.thread().msleep(50)
391            app.processEvents()
392
393        if widget.processingHandler:
394            widget.processingHandler(widget, 0)
395
396    def scheduleSignalProcessing(self, widget=None):
397        """
398        Back compatibility with old orngSignalManager.
399        """
400        self._update()
401
402    def processNewSignals(self, widget=None):
403        """
404        Back compatibility with old orngSignalManager.
405
406        .. todo:: The old signal manager would update immediately, but
407                  this only schedules the update. Is this a problem?
408
409        """
410        self._update()
411
412    def addEvent(self, strValue, object=None, eventVerbosity=1):
413        """
414        Back compatibility with old orngSignalManager module's logging.
415        """
416        if not isinstance(strValue, basestring):
417            info = str(strValue)
418        else:
419            info = strValue
420
421        if object is not None:
422            info += ". Token type = %s. Value = %s" % \
423                    (str(type(object)), str(object)[:100])
424
425        if eventVerbosity > 0:
426            log.debug(info)
427        else:
428            log.info(info)
429
430    def getLinks(self, widgetFrom=None, widgetTo=None,
431                 signalNameFrom=None, signalNameTo=None):
432        """
433        Back compatibility with old orngSignalManager. Some widget look if
434        they have any output connections, so this is still needed, but should
435        be deprecated in the future.
436
437        """
438        scheme = self.scheme()
439
440        source_node = sink_node = None
441
442        if widgetFrom is not None:
443            source_node = scheme.node_for_widget[widgetFrom]
444        if widgetTo is not None:
445            sink_node = scheme.node_for_widget[widgetTo]
446
447        candidates = scheme.find_links(source_node=source_node,
448                                       sink_node=sink_node)
449
450        def signallink(link):
451            """
452            Construct SignalLink from an SchemeLink.
453            """
454            w1 = scheme.widget_for_node[link.source_node]
455            w2 = scheme.widget_for_node[link.sink_node]
456
457            # Input/OutputSignal are reused from description. Interface
458            # is almost the same as it was in orngSignalManager
459            return SignalLink(w1, link.source_channel,
460                              w2, link.sink_channel,
461                              link.enabled)
462
463        links = []
464        for link in candidates:
465            if (signalNameFrom is None or \
466                    link.source_channel.name == signalNameFrom) and \
467                    (signalNameTo is None or \
468                     link.sink_channel.name == signalNameTo):
469
470                links.append(signallink(link))
471        return links
472
473    def setFreeze(self, freeze, startWidget=None):
474        """
475        Freeze/unfreeze signal processing. If freeze >= 1 no signal will be
476        processed until freeze is set back to 0.
477
478        """
479        self.freezing = max(freeze, 0)
480        if freeze > 0:
481            log.debug("Freezing signal processing (value:%r set by %r)",
482                      freeze, startWidget)
483        elif freeze == 0:
484            log.debug("Unfreezing signal processing (cleared by %r)",
485                      startWidget)
486
487        if self._input_queue:
488            self._update()
489
490    def freeze(self, widget=None):
491        """
492        Return a context manager that freezes the signal processing.
493        """
494        manager = self
495
496        class freezer(object):
497            def __enter__(self):
498                self.push()
499                return self
500
501            def __exit__(self, *args):
502                self.pop()
503
504            def push(self):
505                manager.setFreeze(manager.freezing + 1, widget)
506
507            def pop(self):
508                manager.setFreeze(manager.freezing - 1, widget)
509
510        return freezer()
511
512    def event(self, event):
513        if event.type() == QEvent.UpdateRequest:
514            if self.freezing > 0:
515                log.debug("received UpdateRequest while signal processing "
516                          "is frozen")
517                event.setAccepted(False)
518                return False
519
520            if self.__scheme_deleted:
521                log.debug("Scheme has been/is being deleted. No more "
522                          "signals will be delivered to any nodes.")
523                event.setAccepted(True)
524                return True
525        # Retain a reference to the scheme until the 'process_queued' finishes
526        # in SignalManager.event.
527        scheme = self.scheme()
528        return SignalManager.event(self, event)
529
530    def eventFilter(self, receiver, event):
531        if receiver is self.scheme() and event.type() == QEvent.DeferredDelete:
532            if self.runtime_state() == SignalManager.Processing:
533                log.info("Deferring a 'DeferredDelete' event for the Scheme "
534                         "instance until SignalManager exits the current "
535                         "update loop.")
536                event.setAccepted(False)
537                self.processingFinished.connect(self.scheme().deleteLater)
538                self.__scheme_deleted = True
539                return True
540        elif receiver in self._widgets_to_delete and \
541                event.type() == QEvent.DeferredDelete:
542            if self._widget_backup.get(self._active_node, None) is receiver:
543                # The widget is still being updated. We need to keep it alive,
544                # it will be deleted in `send_to_node`.
545                log.info("Deferring a 'DeferredDelete' until widget exits "
546                         "the 'process_signals_for_widget'.")
547                event.setAccepted(False)
548                return True
549
550        return SignalManager.eventFilter(self, receiver, event)
551
552    def __on_scheme_destroyed(self, obj):
553        self.__scheme_deleted = True
554
555
556class SignalLink(object):
557    """
558    Back compatibility with old orngSignalManager, do not use.
559    """
560    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
561                 enabled=True, dynamic=False):
562        self.widgetFrom = widgetFrom
563        self.widgetTo = widgetTo
564
565        self.outputSignal = outputSignal
566        self.inputSignal = inputSignal
567
568        self.dynamic = dynamic
569
570        self.enabled = enabled
571
572        self.signalNameFrom = self.outputSignal.name
573        self.signalNameTo = self.inputSignal.name
574
575    def canEnableDynamic(self, obj):
576        """
577        Can dynamic signal link be enabled for `obj`?
578        """
579        return isinstance(obj, name_lookup(self.inputSignal.type))
580
581
582class SignalWrapper(object):
583    """
584    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
585    This disables (freezes) the widget's signal manager when slots are
586    invoked from GUI signals. Not sure if this is still needed, could instead
587    just set the blocking flag on the widget itself.
588
589    """
590    def __init__(self, widget, method):
591        self.widget = widget
592        self.method = method
593
594    def __call__(self, *args):
595        manager = self.widget.signalManager
596        if manager:
597            with manager.freeze(self.method):
598                self.method(*args)
599        else:
600            # Might be running stand alone without a manager.
601            self.method(*args)
Note: See TracBrowser for help on using the repository browser.