source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11486:d25adb4ed735

Revision 11486:d25adb4ed735, 20.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Don't save the widget settings to the global settings file if loaded from a scheme file.

RevLine 
[11269]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"""
[11135]19import logging
[11101]20
[11269]21import sip
[11296]22from PyQt4.QtGui import QShortcut, QKeySequence, QWhatsThisClickedEvent
[11297]23from PyQt4.QtCore import Qt, QCoreApplication, QEvent, SIGNAL
[11101]24
[11269]25from .signalmanager import SignalManager, compress_signals, can_enable_dynamic
26from .scheme import Scheme, SchemeNode
27from .utils import name_lookup, check_arg, check_type
[11297]28from ..resources import icon_loader
[11101]29from ..config import rc
30
31log = logging.getLogger(__name__)
32
33
34class WidgetsScheme(Scheme):
[11269]35    """
36    A Scheme containing Orange Widgets managed with a `WidgetsSignalManager`
[11101]37    instance.
[11297]38
39    Extends the base `Scheme` class to handle the lifetime
40    (creation/deletion, etc.) of `OWBaseWidget` instances corresponding to
41    the nodes in the scheme. It also delegates the interwidget signal
42    propagation to an instance of `WidgetsSignalManager`.
[11101]43
44    """
45    def __init__(self, parent=None, title=None, description=None):
46        Scheme.__init__(self, parent, title, description)
47
48        self.widgets = []
49        self.widget_for_node = {}
[11269]50        self.node_for_widget = {}
51        self.signal_manager = WidgetsSignalManager(self)
[11411]52        self.signal_manager.processingStarted[SchemeNode].connect(
53            self.__on_processing_started
54        )
55        self.signal_manager.processingFinished[SchemeNode].connect(
56            self.__on_processing_finished
57        )
[11101]58
59    def add_node(self, node):
[11269]60        """
61        Add a `SchemeNode` instance to the scheme and create/initialize the
62        OWBaseWidget instance for it.
63
64        """
65        check_arg(node not in self.nodes, "Node already in scheme.")
66        check_type(node, SchemeNode)
67
68        # Create the widget before a call to Scheme.add_node in
69        # case someone connected to node_added already expects
70        # widget_for_node, etc. to be up to date.
[11101]71        widget = self.create_widget_instance(node)
[11269]72        Scheme.add_node(self, node)
[11134]73
[11101]74        self.widgets.append(widget)
75
76    def remove_node(self, node):
77        Scheme.remove_node(self, node)
78        widget = self.widget_for_node[node]
[11269]79
80        self.signal_manager.on_node_removed(node)
81
[11101]82        del self.widget_for_node[node]
[11269]83        del self.node_for_widget[widget]
[11101]84
85        # Save settings to user global settings.
[11486]86        if not widget._settingsFromSchema:
87            widget.saveSettings()
[11101]88
89        # Notify the widget it will be deleted.
90        widget.onDeleteWidget()
91        # And schedule it for deletion.
92        widget.deleteLater()
93
94    def add_link(self, link):
95        Scheme.add_link(self, link)
[11269]96        self.signal_manager.link_added(link)
[11135]97
[11101]98    def remove_link(self, link):
99        Scheme.remove_link(self, link)
[11269]100        self.signal_manager.link_removed(link)
[11101]101
102    def create_widget_instance(self, node):
[11269]103        """
104        Create a OWBaseWidget instance for the node.
105        """
[11101]106        desc = node.description
107        klass = name_lookup(desc.qualified_name)
108
109        log.info("Creating %r instance.", klass)
110        widget = klass.__new__(
111            klass,
112            _owInfo=rc.get("canvas.show-state-info", True),
113            _owWarning=rc.get("canvas.show-state-warning", True),
114            _owError=rc.get("canvas.show-state-error", True),
115            _owShowStatus=rc.get("OWWidget.show-status", True),
116            _useContexts=rc.get("OWWidget.use-contexts", True),
117            _category=desc.category,
118            _settingsFromSchema=node.properties
119        )
120
[11269]121        # Add the node/widget mapping s before calling __init__
122        # Some OWWidgets might already send data in the constructor
[11297]123        # (should this be forbidden? Raise a warning?)
[11269]124        self.signal_manager.on_node_added(node)
[11297]125
[11269]126        self.widget_for_node[node] = widget
127        self.node_for_widget[widget] = node
128
[11101]129        widget.__init__(None, self.signal_manager)
130        widget.setCaption(node.title)
131        widget.widgetInfo = desc
132
[11297]133        widget.setWindowIcon(
134            icon_loader.from_description(desc).get(desc.icon)
135        )
136
[11101]137        widget.setVisible(node.properties.get("visible", False))
138
139        node.title_changed.connect(widget.setCaption)
[11297]140
[11101]141        # Bind widgets progress/processing state back to the node's properties
142        widget.progressBarValueChanged.connect(node.set_progress)
143        widget.processingStateChanged.connect(node.set_processing_state)
[11297]144        self.connect(widget,
145                     SIGNAL("blockingStateChanged(bool)"),
146                     self.signal_manager._update)
[11101]147
[11296]148        # Install a help shortcut on the widget
149        help_shortcut = QShortcut(QKeySequence("F1"), widget)
150        help_shortcut.activated.connect(self.__on_help_request)
[11101]151        return widget
152
153    def close_all_open_widgets(self):
154        for widget in self.widget_for_node.values():
155            widget.close()
156
[11222]157    def widget_settings(self):
158        """Return a list of dictionaries with widget settings.
159        """
160        return [self.widget_for_node[node].getSettings(alsoContexts=False)
161                for node in self.nodes]
162
[11226]163    def save_widget_settings(self):
164        """Save all widget settings to their global settings file.
165        """
166        for node in self.nodes:
167            widget = self.widget_for_node[node]
[11486]168            if not widget._settingsFromSchema:
169                widget.saveSettings()
[11226]170
[11101]171    def sync_node_properties(self):
172        """Sync the widget settings/properties with the SchemeNode.properties.
173        Return True if there were any changes in the properties (i.e. if the
174        new node.properties differ from the old value) and False otherwise.
175
176        .. note:: this should hopefully be removed in the feature, when the
177            widget can notify a changed setting property.
178
179        """
180        changed = False
181        for node in self.nodes:
182            widget = self.widget_for_node[node]
[11204]183            settings = widget.getSettings(alsoContexts=False)
[11101]184            if settings != node.properties:
185                node.properties = settings
186                changed = True
187        log.debug("Scheme node properties sync (changed: %s)", changed)
188        return changed
189
[11391]190    def save_to(self, stream, pretty=True, pickle_fallback=False):
[11101]191        self.sync_node_properties()
[11391]192        Scheme.save_to(self, stream, pretty, pickle_fallback)
[11269]193
[11296]194    def __on_help_request(self):
195        """
196        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` and
197        hope someone responds to it.
198
199        """
200        # Sender is the QShortcut, and parent the OWBaseWidget
201        widget = self.sender().parent()
202        node = self.node_for_widget.get(widget)
203        if node:
204            url = "help://search?id={0}".format(node.description.id)
205            event = QWhatsThisClickedEvent(url)
206            QCoreApplication.sendEvent(self, event)
207
[11411]208    def __on_processing_started(self, node):
209        node.set_processing_state(1)
210
211    def __on_processing_finished(self, node):
212        node.set_processing_state(0)
213
[11269]214
215class WidgetsSignalManager(SignalManager):
216    def __init__(self, scheme):
217        SignalManager.__init__(self, scheme)
218
[11465]219        scheme.installEventFilter(self)
[11269]220        # We keep a mapping from node->widget after the node/widget has been
221        # removed from the scheme until we also process all the outgoing signal
222        # updates. The reason is the old OWBaseWidget's MULTI channel protocol
223        # where the actual source widget instance is passed to the signal
[11465]224        # handler, and in the delayed update the mapping in `scheme()` is no
[11269]225        # longer available.
226        self._widget_backup = {}
[11465]227        self._widgets_to_delete = set()
228        self._active_node = None
229        self.freezing = 0
[11269]230
[11465]231        self.__scheme_deleted = False
232        scheme.destroyed.connect(self.__on_scheme_destroyed)
[11269]233
234    def on_node_removed(self, node):
235        widget = self.scheme().widget_for_node[node]
[11465]236
237        assert not self.scheme().find_links(sink_node=node), \
238            "Node removed but still has input links"
239
240        signals = self.compress_signals(self.pending_input_signals(node))
241        if not all(signal.value is None for signal in signals):
242            log.error("Non 'None' signals pending for a removed node %r",
243                         node.title)
244
[11269]245        SignalManager.on_node_removed(self, node)
246
[11465]247        if self.runtime_state() == SignalManager.Processing and \
248                node is self._active_node or self.is_blocking(node):
249            # Delay the widget delete until it finishes.
250            # Keep a reference to the widget and install a filter.
251            self._widgets_to_delete.add(widget)
252            widget.installEventFilter(self)
253
[11269]254        # Store the node->widget mapping for possible delayed signal id.
[11465]255        # It will be removed in `process_queued` when all signals
[11269]256        # originating from this widget are delivered.
257        self._widget_backup[node] = widget
258
259    def send(self, widget, channelname, value, id):
260        """
261        send method compatible with OWBaseWidget.
262        """
263        scheme = self.scheme()
[11470]264
265        if widget not in scheme.node_for_widget:
266            # The Node/Widget was already removed from the scheme
267            return
268
[11269]269        node = scheme.node_for_widget[widget]
270
271        try:
272            channel = node.output_channel(channelname)
273        except ValueError:
274            log.error("%r is not valid signal name for %r",
275                      channelname, node.description.name)
276            return
277
278        SignalManager.send(self, node, channel, value, id)
279
280    def is_blocking(self, node):
281        return self.scheme().widget_for_node[node].isBlocking()
282
283    def send_to_node(self, node, signals):
284        """
285        Implementation of `SignalManager.send_to_node`. Deliver data signals
286        to OWBaseWidget instance.
287
288        """
[11465]289        if node in self.scheme().widget_for_node:
290            widget = self.scheme().widget_for_node[node]
291        else:
292            widget = self._widget_backup[node]
293
294        self._active_node = node
[11269]295        self.process_signals_for_widget(node, widget, signals)
[11465]296        self._active_node = None
297
298        if widget in self._widgets_to_delete:
299            # If this node/widget was removed during the
300            # 'process_signals_for_widget'
301            self._widgets_to_delete.remove(widget)
302            widget.deleteLater()
[11269]303
304    def compress_signals(self, signals):
305        return compress_signals(signals)
306
307    def process_queued(self, max_nodes=None):
308        SignalManager.process_queued(self, max_nodes=max_nodes)
309
310        # Remove node->widgets backup mapping no longer needed.
311        nodes_removed = set(self._widget_backup.keys())
312        sources_remaining = set(signal.link.source_node for
313                                signal in self._input_queue)
314
315        nodes_to_remove = nodes_removed - sources_remaining
316        for node in nodes_to_remove:
317            del self._widget_backup[node]
318
319    def process_signals_for_widget(self, node, widget, signals):
320        """
321        Process new signals for a OWBaseWidget.
322        """
323        # This replaces the old OWBaseWidget.processSignals method
324
325        if sip.isdeleted(widget):
326            log.critical("Widget %r was deleted. Cannot process signals",
327                         widget)
328            return
329
330        if widget.processingHandler:
331            widget.processingHandler(widget, 1)
332
333        scheme = self.scheme()
334        app = QCoreApplication.instance()
335
336        for signal in signals:
337            link = signal.link
338            value = signal.value
339
340            # Check and update the dynamic link state
341            if link.is_dynamic():
342                link.dynamic_enabled = can_enable_dynamic(link, value)
343                if not link.dynamic_enabled:
344                    # Send None instead
345                    value = None
346
347            handler = link.sink_channel.handler
348            if handler.startswith("self."):
349                handler = handler.split(".", 1)[1]
350
351            handler = getattr(widget, handler)
352
353            if link.sink_channel.single:
354                args = (value,)
355            else:
356                source_node = link.source_node
357                source_name = link.source_channel.name
358
359                if source_node in scheme.widget_for_node:
360                    source_widget = scheme.widget_for_node[source_node]
361                else:
362                    # Node is no longer in the scheme.
363                    source_widget = self._widget_backup[source_node]
364
365                # The old OWBaseWidget.processSignals sends the source widget
366                # instance along.
367                # TODO: Does any widget actually use it, or could it be
368                # removed (replaced with a unique id)?
369                args = (value, (source_widget, source_name, signal.id))
370
371            log.debug("Process signals: calling %s.%s (from %s with id:%s)",
372                      type(widget).__name__, handler.__name__, link, signal.id)
373
374            app.setOverrideCursor(Qt.WaitCursor)
375            try:
376                handler(*args)
377            except Exception:
378                log.exception("Error")
379            finally:
380                app.restoreOverrideCursor()
381
382        app.setOverrideCursor(Qt.WaitCursor)
383        try:
384            widget.handleNewSignals()
385        except Exception:
386            log.exception("Error")
387        finally:
388            app.restoreOverrideCursor()
389
390        # TODO: Test if async processing works, then remove this
391        while widget.isBlocking():
392            self.thread().msleep(50)
393            app.processEvents()
394
395        if widget.processingHandler:
[11465]396            widget.processingHandler(widget, 0)
[11269]397
398    def scheduleSignalProcessing(self, widget=None):
399        """
400        Back compatibility with old orngSignalManager.
401        """
402        self._update()
403
404    def processNewSignals(self, widget=None):
405        """
406        Back compatibility with old orngSignalManager.
407
408        .. todo:: The old signal manager would update immediately, but
409                  this only schedules the update. Is this a problem?
410
411        """
412        self._update()
413
414    def addEvent(self, strValue, object=None, eventVerbosity=1):
415        """
416        Back compatibility with old orngSignalManager module's logging.
417        """
418        if not isinstance(strValue, basestring):
419            info = str(strValue)
420        else:
421            info = strValue
422
423        if object is not None:
424            info += ". Token type = %s. Value = %s" % \
425                    (str(type(object)), str(object)[:100])
426
427        if eventVerbosity > 0:
428            log.debug(info)
429        else:
430            log.info(info)
431
432    def getLinks(self, widgetFrom=None, widgetTo=None,
433                 signalNameFrom=None, signalNameTo=None):
434        """
435        Back compatibility with old orngSignalManager. Some widget look if
436        they have any output connections, so this is still needed, but should
437        be deprecated in the future.
438
439        """
440        scheme = self.scheme()
441
442        source_node = sink_node = None
443
444        if widgetFrom is not None:
445            source_node = scheme.node_for_widget[widgetFrom]
446        if widgetTo is not None:
447            sink_node = scheme.node_for_widget[widgetTo]
448
449        candidates = scheme.find_links(source_node=source_node,
450                                       sink_node=sink_node)
451
452        def signallink(link):
453            """
454            Construct SignalLink from an SchemeLink.
455            """
456            w1 = scheme.widget_for_node[link.source_node]
457            w2 = scheme.widget_for_node[link.sink_node]
458
[11297]459            # Input/OutputSignal are reused from description. Interface
460            # is almost the same as it was in orngSignalManager
[11269]461            return SignalLink(w1, link.source_channel,
462                              w2, link.sink_channel,
463                              link.enabled)
464
465        links = []
466        for link in candidates:
467            if (signalNameFrom is None or \
468                    link.source_channel.name == signalNameFrom) and \
469                    (signalNameTo is None or \
470                     link.sink_channel.name == signalNameTo):
471
472                links.append(signallink(link))
473        return links
474
475    def setFreeze(self, freeze, startWidget=None):
476        """
477        Freeze/unfreeze signal processing. If freeze >= 1 no signal will be
478        processed until freeze is set back to 0.
479
480        """
481        self.freezing = max(freeze, 0)
482        if freeze > 0:
483            log.debug("Freezing signal processing (value:%r set by %r)",
484                      freeze, startWidget)
485        elif freeze == 0:
486            log.debug("Unfreezing signal processing (cleared by %r)",
487                      startWidget)
488
489        if self._input_queue:
490            self._update()
491
492    def freeze(self, widget=None):
493        """
494        Return a context manager that freezes the signal processing.
495        """
496        manager = self
497
498        class freezer(object):
499            def __enter__(self):
500                self.push()
501                return self
502
503            def __exit__(self, *args):
504                self.pop()
505
506            def push(self):
507                manager.setFreeze(manager.freezing + 1, widget)
508
509            def pop(self):
510                manager.setFreeze(manager.freezing - 1, widget)
511
512        return freezer()
513
514    def event(self, event):
515        if event.type() == QEvent.UpdateRequest:
516            if self.freezing > 0:
517                log.debug("received UpdateRequest while signal processing "
518                          "is frozen")
519                event.setAccepted(False)
520                return False
521
[11465]522            if self.__scheme_deleted:
523                log.debug("Scheme has been/is being deleted. No more "
524                          "signals will be delivered to any nodes.")
525                event.setAccepted(True)
526                return True
527        # Retain a reference to the scheme until the 'process_queued' finishes
528        # in SignalManager.event.
529        scheme = self.scheme()
[11269]530        return SignalManager.event(self, event)
531
[11465]532    def eventFilter(self, receiver, event):
533        if receiver is self.scheme() and event.type() == QEvent.DeferredDelete:
534            if self.runtime_state() == SignalManager.Processing:
[11470]535                log.info("Deferring a 'DeferredDelete' event for the Scheme "
536                         "instance until SignalManager exits the current "
537                         "update loop.")
[11465]538                event.setAccepted(False)
539                self.processingFinished.connect(self.scheme().deleteLater)
540                self.__scheme_deleted = True
541                return True
542        elif receiver in self._widgets_to_delete and \
543                event.type() == QEvent.DeferredDelete:
544            if self._widget_backup.get(self._active_node, None) is receiver:
545                # The widget is still being updated. We need to keep it alive,
546                # it will be deleted in `send_to_node`.
[11470]547                log.info("Deferring a 'DeferredDelete' until widget exits "
548                         "the 'process_signals_for_widget'.")
[11465]549                event.setAccepted(False)
550                return True
551
552        return SignalManager.eventFilter(self, receiver, event)
553
554    def __on_scheme_destroyed(self, obj):
555        self.__scheme_deleted = True
556
[11269]557
558class SignalLink(object):
559    """
[11465]560    Back compatibility with old orngSignalManager, do not use.
[11269]561    """
562    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
563                 enabled=True, dynamic=False):
564        self.widgetFrom = widgetFrom
565        self.widgetTo = widgetTo
566
567        self.outputSignal = outputSignal
568        self.inputSignal = inputSignal
569
570        self.dynamic = dynamic
571
572        self.enabled = enabled
573
574        self.signalNameFrom = self.outputSignal.name
575        self.signalNameTo = self.inputSignal.name
576
577    def canEnableDynamic(self, obj):
578        """
579        Can dynamic signal link be enabled for `obj`?
580        """
581        return isinstance(obj, name_lookup(self.inputSignal.type))
582
583
584class SignalWrapper(object):
585    """
586    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
587    This disables (freezes) the widget's signal manager when slots are
[11297]588    invoked from GUI signals. Not sure if this is still needed, could instead
589    just set the blocking flag on the widget itself.
[11269]590
591    """
592    def __init__(self, widget, method):
593        self.widget = widget
594        self.method = method
595
[11297]596    def __call__(self, *args):
[11269]597        manager = self.widget.signalManager
[11297]598        if manager:
599            with manager.freeze(self.method):
600                self.method(*args)
601        else:
602            # Might be running stand alone without a manager.
603            self.method(*args)
Note: See TracBrowser for help on using the repository browser.