source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11297:8cb078ed56ec

Revision 11297:8cb078ed56ec, 17.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Small fixes to WidgetsScheme.

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