source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11540:7a1e9c31d414

Revision 11540:7a1e9c31d414, 20.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Close the widget before saving it's settings.

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