source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11269:b9a89af169b2

Revision 11269:b9a89af169b2, 15.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Replaced old orngSignalManager.SignalManager class.

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