source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11773:41d75ac844c9

Revision 11773:41d75ac844c9, 26.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Initialize the SchemeNode state messages on widget initialization.

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