~jaap.karssenberg/zim/pyzim-gtk3

« back to all changes in this revision

Viewing changes to zim/signals.py

  • Committer: Jaap Karssenberg
  • Date: 2014-03-08 11:47:43 UTC
  • mfrom: (668.1.49 pyzim-refactor)
  • Revision ID: jaap.karssenberg@gmail.com-20140308114743-fero6uvy9zirbb4o
Merge branch with refactoring

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
import weakref
7
7
import logging
8
8
import gobject
 
9
import os
9
10
 
10
11
 
11
12
logger = logging.getLogger('zim')
12
13
 
13
14
 
 
15
if os.environ.get('ZIM_TEST_RUNNING'):
 
16
        TEST_MODE = True
 
17
else:
 
18
        TEST_MODE = False
 
19
 
 
20
 
14
21
# Constants for signal order
15
22
SIGNAL_NORMAL = 1
16
23
SIGNAL_AFTER = 2
17
24
SIGNAL_OBJECT = 4
18
25
 
19
26
 
 
27
class SignalHandler(object):
 
28
        '''Wrapper for a signal handler method that allows blocking the
 
29
        handler for incoming signals. To be used as function decorator.
 
30
 
 
31
        The method will be replaced by a L{BoundSignalHandler} object that
 
32
        supports a C{blocked()} method which returns a context manager
 
33
        to temporarily block a callback.
 
34
 
 
35
        Intended to be used as::
 
36
 
 
37
                class Foo():
 
38
 
 
39
                        @SignalHandler
 
40
                        def on_changed(self):
 
41
                                ...
 
42
 
 
43
                        def update(self):
 
44
                                with self.on_changed.blocked():
 
45
                                        ... # do something that results in a "changed" signal
 
46
 
 
47
        '''
 
48
 
 
49
        def __init__(self, func):
 
50
                self._func = func
 
51
 
 
52
        def __get__(self, instance, klass):
 
53
                if instance is None:
 
54
                        # class access
 
55
                        return self
 
56
                else:
 
57
                        # instance acces, return bound version
 
58
                        name = '_bound_' + self._func.__name__
 
59
                        if not hasattr(instance, name) \
 
60
                        or getattr(instance, name) is None:
 
61
                                bound_obj = BoundSignalHandler(instance, self._func)
 
62
                                setattr(instance, name, bound_obj)
 
63
 
 
64
                        return getattr(instance, name)
 
65
 
 
66
 
 
67
class BoundSignalHandler(object):
 
68
 
 
69
        def __init__(self, instance, func):
 
70
                self._instance = instance
 
71
                self._func = func
 
72
                self._blocked = 0
 
73
 
 
74
        def __call__(self, *args, **kwargs):
 
75
                if self._blocked == 0:
 
76
                        return self._func(self._instance, *args, **kwargs)
 
77
 
 
78
        def _block(self):
 
79
                self._blocked += 1
 
80
 
 
81
        def _unblock(self):
 
82
                if self._blocked > 0:
 
83
                        self._blocked -= 1
 
84
 
 
85
        def blocked(self):
 
86
                '''Returns a context manager that can be used to temporarily
 
87
                block a callback.
 
88
                '''
 
89
                return SignalHandlerBlockContextManager(self)
 
90
 
 
91
 
 
92
class SignalHandlerBlockContextManager(object):
 
93
 
 
94
        def __init__(self, handler):
 
95
                self.handler = handler
 
96
 
 
97
        def __enter__(self):
 
98
                self.handler._block()
 
99
 
 
100
        def __exit__(self, exc_type, exc_val, exc_tb):
 
101
                self.handler._unblock()
 
102
 
 
103
 
 
104
 
 
105
 
20
106
class ConnectorMixin(object):
21
107
        '''Mixin class that has convenience methods for objects that
22
108
        want to connect to signals of other objects.
135
221
        name = 'do_' + signal.replace('-', '_')
136
222
        if hasattr(obj, name):
137
223
                method = getattr(obj, name)
138
 
                method(*args)
 
224
                return method(*args)
 
225
        else:
 
226
                return None
139
227
 
140
228
 
141
229
def call_handlers(obj, signal, handlers, args):
155
243
                        else:
156
244
                                r = callback(obj, *myargs)
157
245
                except:
158
 
                        # TODO in case of test mode, re-raise the error
159
246
                        logger.exception('Exception in signal handler for %s on %s', signal, obj)
 
247
                        if TEST_MODE:
 
248
                                raise
160
249
                else:
161
250
                        yield r
162
251
 
163
252
 
 
253
class BlockSignalsContextManager(object):
 
254
 
 
255
        def __init__(self, obj, signals):
 
256
                self.obj = obj
 
257
                self.signals = signals
 
258
 
 
259
        def __enter__(self):
 
260
                for signal in self.signals:
 
261
                        self.obj.block_signal(signal)
 
262
 
 
263
        def __exit__(self, exc_type, exc_val, exc_tb):
 
264
                for signal in self.signals:
 
265
                        self.obj.unblock_signal(signal)
 
266
 
 
267
 
164
268
class SignalEmitter(object):
165
269
        '''Replacement for C{GObject} to make objects emit signals.
166
270
        API should be backward compatible with API offered by GObject.
214
318
        def _connect(self, order, signal, callback, userdata):
215
319
                #if self._get_signal(signal) is None:
216
320
                #       raise ValueError, 'No such signal: %s' % signal
 
321
                assert not '_' in signal, 'Signal names use "-"'
217
322
 
218
323
                if not hasattr(self, '_signal_handlers'):
219
324
                        self._signal_handlers = {}
262
367
 
263
368
                return_first = signal in self.__hooks__ # XXX
264
369
 
 
370
                if hasattr(self, '_blocked_signals') \
 
371
                and self._blocked_signals.get(signal):
 
372
                        return # ignore emit
 
373
 
265
374
                if not hasattr(self, '_signal_handlers') \
266
375
                or not signal in self._signal_handlers:
267
 
                        call_default(self, signal, args)
 
376
                        return call_default(self, signal, args)
268
377
                else:
269
378
                        before = [h for h in self._signal_handlers[signal] if h[0] & SIGNAL_NORMAL]
270
379
                        for r in call_handlers(self, signal, before, args):
275
384
                        if return_first and r is not None:
276
385
                                        return r
277
386
 
 
387
                        if not signal in self._signal_handlers:
 
388
                                return None # Yes I have seen a case where default resulted in all handlers disconnected here ...
 
389
 
278
390
                        after = [h for h in self._signal_handlers[signal] if h[0] & SIGNAL_AFTER]
279
391
                        for r in call_handlers(self, signal, after, args):
280
392
                                if return_first and r is not None:
281
393
                                        return r
282
394
 
 
395
        def blocked_signals(self, *signals):
 
396
                '''Returns a context manager for blocking one or more signals'''
 
397
                return BlockSignalsContextManager(self, signals)
 
398
 
 
399
        def block_signal(self, signal):
 
400
                '''Block signal emition by signal name'''
 
401
                assert signal not in self.__hooks__, 'Cannot block a hook'
 
402
                #if self._get_signal(signal) is None:
 
403
                #       raise ValueError, 'No such signal: %s' % signal
 
404
 
 
405
                if not hasattr(self, '_blocked_signals'):
 
406
                        self._blocked_signals = {}
 
407
 
 
408
                self._blocked_signals.setdefault(signal, 0)
 
409
                self._blocked_signals[signal] += 1
 
410
 
 
411
        def unblock_signal(self, signal):
 
412
                '''Unblock signal emition by signal name'''
 
413
                #if self._get_signal(signal) is None:
 
414
                #       raise ValueError, 'No such signal: %s' % signal
 
415
 
 
416
                if hasattr(self, '_blocked_signals') \
 
417
                and signal in self._blocked_signals \
 
418
                and self._blocked_signals[signal] > 0:
 
419
                        self._blocked_signals[signal] -= 1
 
420
 
283
421
 
284
422
class DelayedCallback(object):
285
423
        '''Wrapper for callbacks that need to be delayed after a signal