source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11772:dbcc370d0505

Revision 11772:dbcc370d0505, 25.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Disconnect the SchemeNode from the widget when it has been removed.

Line 
1"""
2Widgets Scheme
3==============
4
5A Scheme for Orange Widgets Scheme (.ows).
6
7This is a subclass of the general :class:`Scheme`. It is responsible for
8the construction and management of OWBaseWidget instances corresponding
9to the scheme nodes, as well as delegating the signal propagation to a
10companion :class:`WidgetsSignalManager` class.
11
12.. autoclass:: WidgetsScheme
13   :bases:
14
15.. autoclass:: WidgetsSignalManager
16  :bases:
17
18"""
19import sys
20import logging
21
22import sip
23from PyQt4.QtGui import (
24    QShortcut, QKeySequence, QWhatsThisClickedEvent, QWidget
25)
26
27from PyQt4.QtCore import Qt, QObject, QCoreApplication, QEvent, SIGNAL
28from PyQt4.QtCore import pyqtSignal as Signal
29
30from .signalmanager import SignalManager, compress_signals, can_enable_dynamic
31from .scheme import Scheme, SchemeNode
32from .node import UserMessage
33from ..utils import name_lookup
34from ..resources import icon_loader
35
36log = logging.getLogger(__name__)
37
38
39class WidgetsScheme(Scheme):
40    """
41    A Scheme containing Orange Widgets managed with a `WidgetsSignalManager`
42    instance.
43
44    Extends the base `Scheme` class to handle the lifetime
45    (creation/deletion, etc.) of `OWBaseWidget` instances corresponding to
46    the nodes in the scheme. It also delegates the interwidget signal
47    propagation to an instance of `WidgetsSignalManager`.
48
49    """
50    def __init__(self, parent=None, title=None, description=None):
51        Scheme.__init__(self, parent, title, description)
52
53        self.signal_manager = WidgetsSignalManager(self)
54        self.widget_manager = WidgetManager()
55        self.widget_manager.set_scheme(self)
56
57    def widget_for_node(self, node):
58        """
59        Return the OWWidget instance for a `node`
60        """
61        return self.widget_manager.widget_for_node(node)
62
63    def node_for_widget(self, widget):
64        """
65        Return the SchemeNode instance for the `widget`.
66        """
67        return self.widget_manager.node_for_widget(widget)
68
69    def sync_node_properties(self):
70        """
71        Sync the widget settings/properties with the SchemeNode.properties.
72        Return True if there were any changes in the properties (i.e. if the
73        new node.properties differ from the old value) and False otherwise.
74
75        """
76        changed = False
77        for node in self.nodes:
78            widget = self.widget_for_node(node)
79            settings = widget.getSettings(alsoContexts=True)
80            if settings != node.properties:
81                node.properties = settings
82                changed = True
83        log.debug("Scheme node properties sync (changed: %s)", changed)
84        return changed
85
86    def show_report_view(self):
87        from OWReport import get_instance
88        inst = get_instance()
89        inst.show()
90        inst.raise_()
91
92
93class WidgetManager(QObject):
94    """
95    OWWidget instance manager class.
96
97    This class handles the lifetime of OWWidget instances in a
98    :class:`WidgetsScheme`.
99
100    """
101    #: A new OWWidget was created and added by the manager.
102    widget_for_node_added = Signal(SchemeNode, QWidget)
103
104    #: An OWWidget was removed, hidden and will be deleted when appropriate.
105    widget_for_node_removed = Signal(SchemeNode, QWidget)
106
107    #: Widget processing state flags:
108    #:   * InputUpdate - signal manager is updating/setting the
109    #:     widget's inputs
110    #:   * BlockingUpdate - widget has entered a blocking state
111    #:   * ProcessingUpdate - widget has entered processing state
112    InputUpdate, BlockingUpdate, ProcessingUpdate = 1, 2, 4
113
114    def __init__(self, parent=None):
115        QObject.__init__(self, parent)
116        self.__scheme = None
117        self.__signal_manager = None
118        self.__widgets = []
119        self.__widget_for_node = {}
120        self.__node_for_widget = {}
121
122        # Widgets that were 'removed' from the scheme but were at
123        # the time in an input update loop and could not be deleted
124        # immediately
125        self.__delay_delete = set()
126
127        # processing state flags for all nodes (including the ones
128        # in __delay_delete).
129        self.__widget_processing_state = {}
130
131        # Tracks the widget in the update loop by the SignalManager
132        self.__updating_widget = None
133
134    def set_scheme(self, scheme):
135        """
136        Set the :class:`WidgetsScheme` instance to manage.
137        """
138        self.__scheme = scheme
139        self.__signal_manager = scheme.findChild(SignalManager)
140
141        self.__signal_manager.processingStarted[SchemeNode].connect(
142            self.__on_processing_started
143        )
144        self.__signal_manager.processingFinished[SchemeNode].connect(
145            self.__on_processing_finished
146        )
147        scheme.node_added.connect(self.add_widget_for_node)
148        scheme.node_removed.connect(self.remove_widget_for_node)
149        scheme.installEventFilter(self)
150
151    def scheme(self):
152        """
153        Return the scheme instance on which this manager is installed.
154        """
155        return self.__scheme
156
157    def signal_manager(self):
158        """
159        Return the signal manager in use on the :func:`scheme`.
160        """
161        return self.__signal_manager
162
163    def widget_for_node(self, node):
164        """
165        Return the OWWidget instance for the scheme node.
166        """
167        return self.__widget_for_node[node]
168
169    def node_for_widget(self, widget):
170        """
171        Return the SchemeNode instance for the OWWidget.
172
173        Raise a KeyError if the widget does not map to a node in the scheme.
174        """
175        return self.__node_for_widget[widget]
176
177    def add_widget_for_node(self, node):
178        """
179        Create a new OWWidget instance for the corresponding scheme node.
180        """
181        widget = self.create_widget_instance(node)
182
183        self.__widgets.append(widget)
184        self.__widget_for_node[node] = widget
185        self.__node_for_widget[widget] = node
186
187        self.widget_for_node_added.emit(node, widget)
188
189    def remove_widget_for_node(self, node):
190        """
191        Remove the OWWidget instance for node.
192        """
193        widget = self.widget_for_node(node)
194
195        self.__widgets.remove(widget)
196
197        del self.__widget_for_node[node]
198        del self.__node_for_widget[widget]
199
200        node.title_changed.disconnect(widget.setCaption)
201        widget.progressBarValueChanged.disconnect(node.set_progress)
202
203        self.widget_for_node_removed.emit(node, widget)
204
205        self._delete_widget(widget)
206
207    def _delete_widget(self, widget):
208        """
209        Delete the OWBaseWidget instance.
210        """
211        widget.close()
212
213        # Save settings to user global settings.
214        if not widget._settingsFromSchema:
215            widget.saveSettings()
216
217        # Notify the widget it will be deleted.
218        widget.onDeleteWidget()
219
220        if self.__widget_processing_state[widget] != 0:
221            # If the widget is in an update loop and/or blocking we
222            # delay the scheduled deletion until the widget is done.
223            self.__delay_delete.add(widget)
224        else:
225            widget.deleteLater()
226
227    def create_widget_instance(self, node):
228        """
229        Create a OWWidget instance for the node.
230        """
231        desc = node.description
232        klass = name_lookup(desc.qualified_name)
233
234        log.info("Creating %r instance.", klass)
235        widget = klass.__new__(
236            klass,
237            _owInfo=True,
238            _owWarning=True,
239            _owError=True,
240            _owShowStatus=True,
241            _useContexts=True,
242            _category=desc.category,
243            _settingsFromSchema=node.properties
244        )
245
246        # Init the node/widget mapping and state before calling __init__
247        # Some OWWidgets might already send data in the constructor
248        # (should this be forbidden? Raise a warning?) triggering the signal
249        # manager which would request the widget => node mapping or state
250        self.__widget_for_node[node] = widget
251        self.__node_for_widget[widget] = node
252        self.__widget_processing_state[widget] = 0
253
254        widget.__init__(None, self.signal_manager())
255        widget.setCaption(node.title)
256        widget.widgetInfo = desc
257
258        widget.setWindowIcon(
259            icon_loader.from_description(desc).get(desc.icon)
260        )
261
262        widget.setVisible(node.properties.get("visible", False))
263
264        node.title_changed.connect(widget.setCaption)
265
266        # Widget's info/warning/error messages.
267        widget.widgetStateChanged.connect(self.__on_widget_state_changed)
268
269        # Widget's progress bar value state.
270        widget.progressBarValueChanged.connect(node.set_progress)
271
272        # Widget processing state (progressBarInit/Finished)
273        # and the blocking state.
274        widget.processingStateChanged.connect(
275            self.__on_processing_state_changed
276        )
277        self.connect(widget,
278                     SIGNAL("blockingStateChanged(bool)"),
279                     self.__on_blocking_state_changed)
280
281        if widget.isBlocking():
282            # A widget can already enter blocking state in __init__
283            self.__widget_processing_state[widget] |= self.BlockingUpdate
284
285        # Install a help shortcut on the widget
286        help_shortcut = QShortcut(QKeySequence("F1"), widget)
287        help_shortcut.activated.connect(self.__on_help_request)
288
289        return widget
290
291    def node_processing_state(self, node):
292        """
293        Return the processing state flags for the node.
294
295        Same as `manager.node_processing_state(manger.widget_for_node(node))`
296
297        """
298        widget = self.widget_for_node(node)
299        return self.__widget_processing_state[widget]
300
301    def widget_processing_state(self, widget):
302        """
303        Return the processing state flags for the widget.
304
305        The state is an bitwise or of `InputUpdate` and `BlockingUpdate`.
306
307        """
308        return self.__widget_processing_state[widget]
309
310    def eventFilter(self, receiver, event):
311        if event.type() == QEvent.Close and receiver is self.__scheme:
312            self.signal_manager().stop()
313
314            # Notify the widget instances.
315            for widget in self.__widget_for_node.values():
316                widget.close()
317
318                if not widget._settingsFromSchema:
319                    # First save global settings if necessary.
320                    widget.saveSettings()
321
322                widget.onDeleteWidget()
323
324            event.accept()
325            return True
326
327        return QObject.eventFilter(self, receiver, event)
328
329    def __on_help_request(self):
330        """
331        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to
332        the scheme and hope someone responds to it.
333
334        """
335        # Sender is the QShortcut, and parent the OWBaseWidget
336        widget = self.sender().parent()
337        try:
338            node = self.node_for_widget(widget)
339        except KeyError:
340            pass
341        else:
342            url = "help://search?id={0}".format(node.description.id)
343            event = QWhatsThisClickedEvent(url)
344            QCoreApplication.sendEvent(self.scheme(), event)
345
346    def __on_widget_state_changed(self, message_type, message_id,
347                                  message_value):
348        """
349        The OWBaseWidget info/warning/error state has changed.
350
351        message_type is one of "Info", "Warning" or "Error" string depending
352        of which method (information, warning, error) was called. message_id
353        is the first int argument if supplied, and message_value the message
354        text.
355
356        """
357        widget = self.sender()
358        try:
359            node = self.node_for_widget(widget)
360        except KeyError:
361            pass
362        else:
363            message_type = str(message_type)
364            if message_type == "Info":
365                contents = widget.widgetStateToHtml(True, False, False)
366                level = UserMessage.Info
367            elif message_type == "Warning":
368                contents = widget.widgetStateToHtml(False, True, False)
369                level = UserMessage.Warning
370            elif message_type == "Error":
371                contents = widget.widgetStateToHtml(False, False, True)
372                level = UserMessage.Error
373            else:
374                raise ValueError("Invalid message_type: %r" % message_type)
375
376            if not contents:
377                contents = None
378
379            message = UserMessage(contents, severity=level,
380                                  message_id=message_type,
381                                  data={"content-type": "text/html"})
382            node.set_state_message(message)
383
384    def __on_processing_state_changed(self, state):
385        """
386        A widget processing state has changed (progressBarInit/Finished)
387        """
388        widget = self.sender()
389        try:
390            node = self.node_for_widget(widget)
391        except KeyError:
392            return
393
394        if state:
395            self.__widget_processing_state[widget] |= self.ProcessingUpdate
396        else:
397            self.__widget_processing_state[widget] &= ~self.ProcessingUpdate
398        self.__update_node_processing_state(node)
399
400    def __on_processing_started(self, node):
401        """
402        Signal manager entered the input update loop for the node.
403        """
404        widget = self.widget_for_node(node)
405        # Remember the widget instance. The node and the node->widget mapping
406        # can be removed between this and __on_processing_finished.
407        self.__updating_widget = widget
408        self.__widget_processing_state[widget] |= self.InputUpdate
409        self.__update_node_processing_state(node)
410
411    def __on_processing_finished(self, node):
412        """
413        Signal manager exited the input update loop for the node.
414        """
415        widget = self.__updating_widget
416        self.__widget_processing_state[widget] &= ~self.InputUpdate
417
418        if widget in self.__node_for_widget:
419            self.__update_node_processing_state(node)
420        elif widget in self.__delay_delete:
421            self.__try_delete(widget)
422        else:
423            raise ValueError("%r is not managed" % widget)
424
425        self.__updating_widget = None
426
427    def __on_blocking_state_changed(self, state):
428        """
429        OWWidget blocking state has changed.
430        """
431        if not state:
432            # schedule an update pass.
433            self.signal_manager()._update()
434
435        widget = self.sender()
436        if state:
437            self.__widget_processing_state[widget] |= self.BlockingUpdate
438        else:
439            self.__widget_processing_state[widget] &= ~self.BlockingUpdate
440
441        if widget in self.__node_for_widget:
442            node = self.node_for_widget(widget)
443            self.__update_node_processing_state(node)
444
445        elif widget in self.__delay_delete:
446            self.__try_delete(widget)
447
448    def __update_node_processing_state(self, node):
449        """
450        Update the `node.processing_state` to reflect the widget state.
451        """
452        state = self.node_processing_state(node)
453        node.set_processing_state(1 if state else 0)
454
455    def __try_delete(self, widget):
456        if self.__widget_processing_state[widget] == 0:
457            self.__delay_delete.remove(widget)
458            widget.deleteLater()
459            del self.__widget_processing_state[widget]
460
461
462class WidgetsSignalManager(SignalManager):
463    """
464    A signal manager for a WidgetsScheme.
465    """
466    def __init__(self, scheme):
467        SignalManager.__init__(self, scheme)
468
469        scheme.installEventFilter(self)
470
471        self.freezing = 0
472
473        self.__scheme_deleted = False
474
475        scheme.destroyed.connect(self.__on_scheme_destroyed)
476        scheme.node_added.connect(self.on_node_added)
477        scheme.node_removed.connect(self.on_node_removed)
478        scheme.link_added.connect(self.link_added)
479        scheme.link_removed.connect(self.link_removed)
480
481    def send(self, widget, channelname, value, signal_id):
482        """
483        send method compatible with OWBaseWidget.
484        """
485        scheme = self.scheme()
486        try:
487            node = scheme.node_for_widget(widget)
488        except KeyError:
489            # The Node/Widget was already removed from the scheme.
490            log.debug("Node for %r is not in the scheme.", widget)
491            return
492
493        try:
494            channel = node.output_channel(channelname)
495        except ValueError:
496            log.error("%r is not valid signal name for %r",
497                      channelname, node.description.name)
498            return
499
500        # Expand the signal_id with the unique widget id and the
501        # channel name. This is needed for OWBaseWidget's input
502        # handlers (Multiple flag).
503        signal_id = (widget.widgetId, channelname, signal_id)
504
505        SignalManager.send(self, node, channel, value, signal_id)
506
507    def is_blocking(self, node):
508        return self.scheme().widget_manager.node_processing_state(node) != 0
509
510    def send_to_node(self, node, signals):
511        """
512        Implementation of `SignalManager.send_to_node`.
513
514        Deliver input signals to an OWBaseWidget instance.
515
516        """
517        widget = self.scheme().widget_for_node(node)
518        self.process_signals_for_widget(node, widget, signals)
519
520    def compress_signals(self, signals):
521        """
522        Reimplemented from :func:`SignalManager.compress_signals`.
523        """
524        return compress_signals(signals)
525
526    def process_signals_for_widget(self, node, widget, signals):
527        """
528        Process new signals for the OWBaseWidget.
529        """
530        # This replaces the old OWBaseWidget.processSignals method
531
532        if sip.isdeleted(widget):
533            log.critical("Widget %r was deleted. Cannot process signals",
534                         widget)
535            return
536
537        if widget.processingHandler:
538            widget.processingHandler(widget, 1)
539
540        app = QCoreApplication.instance()
541
542        for signal in signals:
543            link = signal.link
544            value = signal.value
545
546            # Check and update the dynamic link state
547            if link.is_dynamic():
548                link.dynamic_enabled = can_enable_dynamic(link, value)
549                if not link.dynamic_enabled:
550                    # Send None instead
551                    value = None
552
553            handler = link.sink_channel.handler
554            if handler.startswith("self."):
555                handler = handler.split(".", 1)[1]
556
557            handler = getattr(widget, handler)
558
559            if link.sink_channel.single:
560                args = (value,)
561            else:
562                args = (value, signal.id)
563
564            log.debug("Process signals: calling %s.%s (from %s with id:%s)",
565                      type(widget).__name__, handler.__name__, link, signal.id)
566
567            app.setOverrideCursor(Qt.WaitCursor)
568            try:
569                handler(*args)
570            except Exception:
571                sys.excepthook(*sys.exc_info())
572                log.exception("Error calling '%s' of '%s'",
573                              handler.__name__, node.title)
574            finally:
575                app.restoreOverrideCursor()
576
577        app.setOverrideCursor(Qt.WaitCursor)
578        try:
579            widget.handleNewSignals()
580        except Exception:
581            sys.excepthook(*sys.exc_info())
582            log.exception("Error calling 'handleNewSignals()' of '%s'",
583                          node.title)
584        finally:
585            app.restoreOverrideCursor()
586
587        if widget.processingHandler:
588            widget.processingHandler(widget, 0)
589
590    def scheduleSignalProcessing(self, widget=None):
591        """
592        Back compatibility with old orngSignalManager.
593        """
594        self._update()
595
596    def processNewSignals(self, widget=None):
597        """
598        Back compatibility with old orngSignalManager.
599
600        .. todo:: The old signal manager would update immediately, but
601                  this only schedules the update. Is this a problem?
602
603        """
604        self._update()
605
606    def addEvent(self, strValue, object=None, eventVerbosity=1):
607        """
608        Back compatibility with old orngSignalManager module's logging.
609        """
610        if not isinstance(strValue, basestring):
611            info = str(strValue)
612        else:
613            info = strValue
614
615        if object is not None:
616            info += ". Token type = %s. Value = %s" % \
617                    (str(type(object)), str(object)[:100])
618
619        if eventVerbosity > 0:
620            log.debug(info)
621        else:
622            log.info(info)
623
624    def getLinks(self, widgetFrom=None, widgetTo=None,
625                 signalNameFrom=None, signalNameTo=None):
626        """
627        Back compatibility with old orngSignalManager. Some widget look if
628        they have any output connections, so this is still needed, but should
629        be deprecated in the future.
630
631        """
632        scheme = self.scheme()
633
634        source_node = sink_node = None
635
636        if widgetFrom is not None:
637            source_node = scheme.node_for_widget(widgetFrom)
638        if widgetTo is not None:
639            sink_node = scheme.node_for_widget(widgetTo)
640
641        candidates = scheme.find_links(source_node=source_node,
642                                       sink_node=sink_node)
643
644        def signallink(link):
645            """
646            Construct SignalLink from an SchemeLink.
647            """
648            w1 = scheme.widget_for_node(link.source_node)
649            w2 = scheme.widget_for_node(link.sink_node)
650
651            # Input/OutputSignal are reused from description. Interface
652            # is almost the same as it was in orngSignalManager
653            return SignalLink(w1, link.source_channel,
654                              w2, link.sink_channel,
655                              link.enabled)
656
657        links = []
658        for link in candidates:
659            if (signalNameFrom is None or \
660                    link.source_channel.name == signalNameFrom) and \
661                    (signalNameTo is None or \
662                     link.sink_channel.name == signalNameTo):
663
664                links.append(signallink(link))
665        return links
666
667    def setFreeze(self, freeze, startWidget=None):
668        """
669        Freeze/unfreeze signal processing. If freeze >= 1 no signal will be
670        processed until freeze is set back to 0.
671
672        """
673        self.freezing = max(freeze, 0)
674        if freeze > 0:
675            log.debug("Freezing signal processing (value:%r set by %r)",
676                      freeze, startWidget)
677        elif freeze == 0:
678            log.debug("Unfreezing signal processing (cleared by %r)",
679                      startWidget)
680
681        if self._input_queue:
682            self._update()
683
684    def freeze(self, widget=None):
685        """
686        Return a context manager that freezes the signal processing.
687        """
688        manager = self
689
690        class freezer(object):
691            def __enter__(self):
692                self.push()
693                return self
694
695            def __exit__(self, *args):
696                self.pop()
697
698            def push(self):
699                manager.setFreeze(manager.freezing + 1, widget)
700
701            def pop(self):
702                manager.setFreeze(manager.freezing - 1, widget)
703
704        return freezer()
705
706    def event(self, event):
707        if event.type() == QEvent.UpdateRequest:
708            if self.freezing > 0:
709                log.debug("received UpdateRequest while signal processing "
710                          "is frozen")
711                event.setAccepted(False)
712                return False
713
714            if self.__scheme_deleted:
715                log.debug("Scheme has been/is being deleted. No more "
716                          "signals will be delivered to any nodes.")
717                event.setAccepted(True)
718                return True
719        # Retain a reference to the scheme until the 'process_queued' finishes
720        # in SignalManager.event.
721        scheme = self.scheme()
722        return SignalManager.event(self, event)
723
724    def eventFilter(self, receiver, event):
725        if event.type() == QEvent.DeferredDelete and receiver is self.scheme():
726            try:
727                state = self.runtime_state()
728            except AttributeError:
729                # If the scheme (which is a parent of this object) is
730                # already being deleted the SignalManager can also be in
731                # the process of destruction (noticeable by its __dict__
732                # being empty). There is nothing really to do in this
733                # case.
734                state = None
735
736            if state == SignalManager.Processing:
737                log.info("Deferring a 'DeferredDelete' event for the Scheme "
738                         "instance until SignalManager exits the current "
739                         "update loop.")
740                event.setAccepted(False)
741                self.processingFinished.connect(self.scheme().deleteLater)
742                self.__scheme_deleted = True
743                return True
744
745        return SignalManager.eventFilter(self, receiver, event)
746
747    def __on_scheme_destroyed(self, obj):
748        self.__scheme_deleted = True
749
750
751class SignalLink(object):
752    """
753    Back compatibility with old orngSignalManager, do not use.
754    """
755    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
756                 enabled=True, dynamic=False):
757        self.widgetFrom = widgetFrom
758        self.widgetTo = widgetTo
759
760        self.outputSignal = outputSignal
761        self.inputSignal = inputSignal
762
763        self.dynamic = dynamic
764
765        self.enabled = enabled
766
767        self.signalNameFrom = self.outputSignal.name
768        self.signalNameTo = self.inputSignal.name
769
770    def canEnableDynamic(self, obj):
771        """
772        Can dynamic signal link be enabled for `obj`?
773        """
774        return isinstance(obj, name_lookup(self.inputSignal.type))
775
776
777class SignalWrapper(object):
778    """
779    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
780    This disables (freezes) the widget's signal manager when slots are
781    invoked from GUI signals. Not sure if this is still needed, could instead
782    just set the blocking flag on the widget itself.
783
784    """
785    def __init__(self, widget, method):
786        self.widget = widget
787        self.method = method
788
789    def __call__(self, *args):
790        manager = self.widget.signalManager
791        if manager:
792            with manager.freeze(self.method):
793                self.method(*args)
794        else:
795            # Might be running stand alone without a manager.
796            self.method(*args)
Note: See TracBrowser for help on using the repository browser.