source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11726:f1cafef0bde2

Revision 11726:f1cafef0bde2, 25.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Added a menu action to show the Report view.

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