source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11641:be21939d9bed

Revision 11641:be21939d9bed, 25.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 9 months ago (diff)

Removed the forced wait loop for widgets in blocking state.

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    def save_to(self, stream, pretty=True, pickle_fallback=False):
90        """
91        Reimplemented from :func:`Scheme.save_to`.
92        """
93        self.sync_node_properties()
94        Scheme.save_to(self, stream, pretty, pickle_fallback)
95
96
97class WidgetManager(QObject):
98    """
99    OWWidget instance manager class.
100
101    This class handles the lifetime of OWWidget instances in a
102    :class:`WidgetsScheme`.
103
104    """
105    #: A new OWWidget was created and added by the manager.
106    widget_for_node_added = Signal(SchemeNode, QWidget)
107
108    #: An OWWidget was removed, hidden and will be deleted when appropriate.
109    widget_for_node_removed = Signal(SchemeNode, QWidget)
110
111    #: Widget processing state flags:
112    #:   * InputUpdate - signal manager is updating/setting the
113    #:     widget's inputs
114    #:   * BlockingUpdate - widget has entered a blocking state
115    #:   * ProcessingUpdate - widget has entered processing state
116    InputUpdate, BlockingUpdate, ProcessingUpdate = 1, 2, 4
117
118    def __init__(self, parent):
119        QObject.__init__(self, parent)
120        self.__scheme = None
121        self.__signal_manager = None
122        self.__widgets = []
123        self.__widget_for_node = {}
124        self.__node_for_widget = {}
125
126        # Widgets that were 'removed' from the scheme but were at
127        # the time in an input update loop and could not be deleted
128        # immediately
129        self.__delay_delete = set()
130
131        # processing state flags for all nodes (including the ones
132        # in __delay_delete).
133        self.__widget_processing_state = {}
134
135        # Tracks the widget in the update loop by the SignalManager
136        self.__updating_widget = None
137
138    def set_scheme(self, scheme):
139        """
140        Set the :class:`WidgetsScheme` instance to manage.
141        """
142        self.__scheme = scheme
143        self.__signal_manager = scheme.findChild(SignalManager)
144
145        self.__signal_manager.processingStarted[SchemeNode].connect(
146            self.__on_processing_started
147        )
148        self.__signal_manager.processingFinished[SchemeNode].connect(
149            self.__on_processing_finished
150        )
151        scheme.node_added.connect(self.add_widget_for_node)
152        scheme.node_removed.connect(self.remove_widget_for_node)
153        scheme.installEventFilter(self)
154
155    def scheme(self):
156        """
157        Return the scheme instance on which this manager is installed.
158        """
159        return self.__scheme
160
161    def signal_manager(self):
162        """
163        Return the signal manager in use on the :func:`scheme`.
164        """
165        return self.__signal_manager
166
167    def widget_for_node(self, node):
168        """
169        Return the OWWidget instance for the scheme node.
170        """
171        return self.__widget_for_node[node]
172
173    def node_for_widget(self, widget):
174        """
175        Return the SchemeNode instance for the OWWidget.
176
177        Raise a KeyError if the widget does not map to a node in the scheme.
178        """
179        return self.__node_for_widget[widget]
180
181    def add_widget_for_node(self, node):
182        """
183        Create a new OWWidget instance for the corresponding scheme node.
184        """
185        widget = self.create_widget_instance(node)
186
187        self.__widgets.append(widget)
188        self.__widget_for_node[node] = widget
189        self.__node_for_widget[widget] = node
190
191        self.widget_for_node_added.emit(node, widget)
192
193    def remove_widget_for_node(self, node):
194        """
195        Remove the OWWidget instance for node.
196        """
197        widget = self.widget_for_node(node)
198
199        self.__widgets.remove(widget)
200        del self.__widget_for_node[node]
201        del self.__node_for_widget[widget]
202
203        self.widget_for_node_removed.emit(node, widget)
204
205        self._delete_widget(widget)
206
207    def _delete_widget(self, widget):
208        """
209        Delete the OWBaseWidget instance.
210        """
211        widget.close()
212
213        # Save settings to user global settings.
214        if not widget._settingsFromSchema:
215            widget.saveSettings()
216
217        # Notify the widget it will be deleted.
218        widget.onDeleteWidget()
219
220        if self.__widget_processing_state[widget] != 0:
221            # If the widget is in an update loop and/or blocking we
222            # delay the scheduled deletion until the widget is done.
223            self.__delay_delete.add(widget)
224        else:
225            widget.deleteLater()
226
227    def create_widget_instance(self, node):
228        """
229        Create a OWWidget instance for the node.
230        """
231        desc = node.description
232        klass = name_lookup(desc.qualified_name)
233
234        log.info("Creating %r instance.", klass)
235        widget = klass.__new__(
236            klass,
237            _owInfo=True,
238            _owWarning=True,
239            _owError=True,
240            _owShowStatus=True,
241            _useContexts=True,
242            _category=desc.category,
243            _settingsFromSchema=node.properties
244        )
245
246        # Init the node/widget mapping and state before calling __init__
247        # Some OWWidgets might already send data in the constructor
248        # (should this be forbidden? Raise a warning?) triggering the signal
249        # manager which would request the widget => node mapping or state
250        self.__widget_for_node[node] = widget
251        self.__node_for_widget[widget] = node
252        self.__widget_processing_state[widget] = 0
253
254        widget.__init__(None, self.signal_manager())
255        widget.setCaption(node.title)
256        widget.widgetInfo = desc
257
258        widget.setWindowIcon(
259            icon_loader.from_description(desc).get(desc.icon)
260        )
261
262        widget.setVisible(node.properties.get("visible", False))
263
264        node.title_changed.connect(widget.setCaption)
265
266        # Widget's info/warning/error messages.
267        widget.widgetStateChanged.connect(self.__on_widget_state_changed)
268
269        # Widget's progress bar value state.
270        widget.progressBarValueChanged.connect(node.set_progress)
271
272        # Widget processing state (progressBarInit/Finished)
273        # and the blocking state.
274        widget.processingStateChanged.connect(
275            self.__on_processing_state_changed
276        )
277        self.connect(widget,
278                     SIGNAL("blockingStateChanged(bool)"),
279                     self.__on_blocking_state_changed)
280
281        # 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 receiver is self.__scheme and event.type() == QEvent.Close:
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 receiver is self.scheme() and event.type() == QEvent.DeferredDelete:
722            if self.runtime_state() == SignalManager.Processing:
723                log.info("Deferring a 'DeferredDelete' event for the Scheme "
724                         "instance until SignalManager exits the current "
725                         "update loop.")
726                event.setAccepted(False)
727                self.processingFinished.connect(self.scheme().deleteLater)
728                self.__scheme_deleted = True
729                return True
730
731        return SignalManager.eventFilter(self, receiver, event)
732
733    def __on_scheme_destroyed(self, obj):
734        self.__scheme_deleted = True
735
736
737class SignalLink(object):
738    """
739    Back compatibility with old orngSignalManager, do not use.
740    """
741    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
742                 enabled=True, dynamic=False):
743        self.widgetFrom = widgetFrom
744        self.widgetTo = widgetTo
745
746        self.outputSignal = outputSignal
747        self.inputSignal = inputSignal
748
749        self.dynamic = dynamic
750
751        self.enabled = enabled
752
753        self.signalNameFrom = self.outputSignal.name
754        self.signalNameTo = self.inputSignal.name
755
756    def canEnableDynamic(self, obj):
757        """
758        Can dynamic signal link be enabled for `obj`?
759        """
760        return isinstance(obj, name_lookup(self.inputSignal.type))
761
762
763class SignalWrapper(object):
764    """
765    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
766    This disables (freezes) the widget's signal manager when slots are
767    invoked from GUI signals. Not sure if this is still needed, could instead
768    just set the blocking flag on the widget itself.
769
770    """
771    def __init__(self, widget, method):
772        self.widget = widget
773        self.method = method
774
775    def __call__(self, *args):
776        manager = self.widget.signalManager
777        if manager:
778            with manager.freeze(self.method):
779                self.method(*args)
780        else:
781            # Might be running stand alone without a manager.
782            self.method(*args)
Note: See TracBrowser for help on using the repository browser.