source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11657:f3470aa4f755

Revision 11657:f3470aa4f755, 25.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Added an default 'sync_node_properties' method to base Scheme class.

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