Ignore:
Timestamp:
01/22/13 15:30:37 (15 months ago)
Author:
Ales Erjavec <ales.erjavec@…>
Branch:
default
Message:

Replaced old orngSignalManager.SignalManager class.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • Orange/OrangeCanvas/scheme/widgetsscheme.py

    r11226 r11269  
     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""" 
    119import logging 
    2 from functools import partial 
    3  
    4 from PyQt4.QtCore import QTimer, SIGNAL 
    5  
    6 from .. import orngSignalManager 
    7 from .scheme import Scheme 
    8 from .utils import name_lookup 
     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 
    927from ..config import rc 
    10 from ..gui.utils import signals_disabled 
    1128 
    1229log = logging.getLogger(__name__) 
     
    1431 
    1532class WidgetsScheme(Scheme): 
    16     """A Scheme containing Orange Widgets managed with a SignalManager 
     33    """ 
     34    A Scheme containing Orange Widgets managed with a `WidgetsSignalManager` 
    1735    instance. 
     36    This class handles the lifetime (creation/deletion, etc.) of OWBaseWidget 
     37    instances corresponding to the nodes of the scheme. 
    1838 
    1939    """ 
     
    2343        self.widgets = [] 
    2444        self.widget_for_node = {} 
    25         self.signal_manager = orngSignalManager.SignalManager() 
     45        self.node_for_widget = {} 
     46        self.signal_manager = WidgetsSignalManager(self) 
    2647 
    2748    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. 
    2860        widget = self.create_widget_instance(node) 
    2961 
    30         # don't emit the node_added signal until the widget is successfully 
    31         # added to signal manager etc. 
    32         with signals_disabled(self): 
    33             Scheme.add_node(self, node) 
     62        Scheme.add_node(self, node) 
    3463 
    3564        self.widgets.append(widget) 
    36  
    37         self.widget_for_node[node] = widget 
    38  
    39         self.signal_manager.addWidget(widget) 
    40  
    41         self.node_added.emit(node) 
    4265 
    4366    def remove_node(self, node): 
    4467        Scheme.remove_node(self, node) 
    4568        widget = self.widget_for_node[node] 
    46         self.signal_manager.removeWidget(widget) 
     69 
     70        self.signal_manager.on_node_removed(node) 
     71 
    4772        del self.widget_for_node[node] 
     73        del self.node_for_widget[widget] 
    4874 
    4975        # Save settings to user global settings. 
     
    5783    def add_link(self, link): 
    5884        Scheme.add_link(self, link) 
    59         source_widget = self.widget_for_node[link.source_node] 
    60         sink_widget = self.widget_for_node[link.sink_node] 
    61         source_channel = link.source_channel.name 
    62         sink_channel = link.sink_channel.name 
    63         self.signal_manager.addLink(source_widget, sink_widget, source_channel, 
    64                                     sink_channel, enabled=link.enabled) 
    65  
    66         link.enabled_changed.connect( 
    67             partial(self.signal_manager.setLinkEnabled, 
    68                     source_widget, sink_widget) 
    69         ) 
    70  
    71         QTimer.singleShot(0, self.signal_manager.processNewSignals) 
     85        self.signal_manager.link_added(link) 
    7286 
    7387    def remove_link(self, link): 
    7488        Scheme.remove_link(self, link) 
    75  
    76         source_widget = self.widget_for_node[link.source_node] 
    77         sink_widget = self.widget_for_node[link.sink_node] 
    78         source_channel = link.source_channel.name 
    79         sink_channel = link.sink_channel.name 
    80  
    81         self.signal_manager.removeLink(source_widget, sink_widget, 
    82                                        source_channel, sink_channel) 
     89        self.signal_manager.link_removed(link) 
    8390 
    8491    def create_widget_instance(self, node): 
     92        """ 
     93        Create a OWBaseWidget instance for the node. 
     94        """ 
    8595        desc = node.description 
    8696        klass = name_lookup(desc.qualified_name) 
     
    98108        ) 
    99109 
     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 
    100116        widget.__init__(None, self.signal_manager) 
    101117        widget.setCaption(node.title) 
     
    109125        widget.processingStateChanged.connect(node.set_processing_state) 
    110126 
    111         # TODO: Change how the signal is emitted in signal manager (should 
    112         # notify the SchemeLink directly). 
    113         widget.connect( 
    114            widget, 
    115            SIGNAL("dynamicLinkEnabledChanged(PyQt_PyObject, bool)"), 
    116            self.__on_dynamic_link_enabled_changed 
    117         ) 
    118  
    119127        return widget 
    120  
    121     def __on_dynamic_link_enabled_changed(self, link, enabled): 
    122         rev = dict(map(reversed, self.widget_for_node.items())) 
    123  
    124         source_node = rev[link.widgetFrom] 
    125         sink_node = rev[link.widgetTo] 
    126         source_channel = source_node.output_channel(link.signalNameFrom) 
    127         sink_channel = sink_node.input_channel(link.signalNameTo) 
    128  
    129         links = self.find_links(source_node, source_channel, 
    130                                 sink_node, sink_channel) 
    131  
    132         if links: 
    133             link = links[0] 
    134             link.set_dynamic_enabled(enabled) 
    135128 
    136129    def close_all_open_widgets(self): 
     
    173166        self.sync_node_properties() 
    174167        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 TracChangeset for help on using the changeset viewer.