source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11411:f1d5470c8031

Revision 11411:f1d5470c8031, 17.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Added ping and shadow animations for node items.

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