source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11757:312265a59fcc

Revision 11757:312265a59fcc, 25.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Fixed use of 'node_for_widget' method.

(fixes #1342)

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