1# Copyright (C) 2008-2010 Adam Olsen 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2, or (at your option) 6# any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# 18# The developers of the Exaile media player hereby grant permission 19# for non-GPL compatible GStreamer and Exaile plugins to be used and 20# distributed together with GStreamer and Exaile. This permission is 21# above and beyond the permissions granted by the GPL license by which 22# Exaile is covered. If you modify this code, you may extend this 23# exception to your version of the code, but you are not obligated to 24# do so. If you do not wish to do so, delete this exception statement 25# from your version. 26 27""" 28Provides a signals-like system for sending and listening for 'events' 29 30 31Events are kind of like signals, except they may be listened for on a 32global scale, rather than connected on a per-object basis like signals 33are. This means that ANY object can emit ANY event, and these events may 34be listened for by ANY object. 35 36Events should be emitted AFTER the given event has taken place. Often the 37most appropriate spot is immediately before a return statement. 38""" 39 40from inspect import ismethod 41import logging 42import re 43import threading 44import time 45import types 46import weakref 47from gi.repository import GLib 48 49# define this here so the interpreter doesn't complain 50EVENT_MANAGER = None 51 52logger = logging.getLogger(__name__) 53 54 55class Nothing: 56 pass 57 58 59_NONE = Nothing() # used by event for a safe None replacement 60 61# Assumes that this module was imported on main thread 62_UiThread = threading.current_thread() 63 64 65def log_event(evty, obj, data): 66 """ 67 Sends an event. 68 69 :param evty: the *type* or *name* of the event. 70 :type evty: string 71 :param obj: the object sending the event. 72 :type obj: object 73 :param data: some data about the event, None if not required 74 :type data: object 75 """ 76 global EVENT_MANAGER 77 e = Event(evty, obj, data) 78 EVENT_MANAGER.emit(e) 79 80 81def add_callback(function, evty=None, obj=None, *args, **kwargs): 82 """ 83 Adds a callback to an event 84 85 You should ALWAYS specify one of the two options on what to listen 86 for. While not forbidden to listen to all events, doing so will 87 cause your callback to be called very frequently, and possibly may 88 cause slowness within the player itself. 89 90 :param function: the function to call when the event happens 91 :type function: callable 92 :param evty: the *type* or *name* of the event to listen for, eg 93 `tracks_added`, `cover_changed`. Defaults to any event if 94 not specified. 95 :type evty: string 96 :param obj: the object to listen to events from, e.g. `exaile.collection` 97 or `xl.covers.MANAGER`. Defaults to any object if not 98 specified. 99 :type obj: object 100 :param destroy_with: (keyword arg only) If specified, this event will be 101 detached when the specified Gtk widget is destroyed 102 103 Any additional parameters will be passed to the callback. 104 105 :returns: a convenience function that you can call to remove the callback. 106 """ 107 global EVENT_MANAGER 108 return EVENT_MANAGER.add_callback(function, evty, obj, args, kwargs) 109 110 111def add_ui_callback(function, evty=None, obj=None, *args, **kwargs): 112 """ 113 Adds a callback to an event. The callback is guaranteed to 114 always be called on the UI thread. 115 116 You should ALWAYS specify one of the two options on what to listen 117 for. While not forbidden to listen to all events, doing so will 118 cause your callback to be called very frequently, and possibly may 119 cause slowness within the player itself. 120 121 :param function: the function to call when the event happens 122 :type function: callable 123 :param evty: the *type* or *name* of the event to listen for, eg 124 `tracks_added`, `cover_changed`. Defaults to any event if 125 not specified. 126 :type evty: string 127 :param obj: the object to listen to events from, e.g. `exaile.collection` 128 or `xl.covers.MANAGER`. Defaults to any object if not 129 specified. 130 :type obj: object 131 :param destroy_with: (keyword arg only) If specified, this event will be 132 detached when the specified Gtk widget is destroyed 133 134 Any additional parameters will be passed to the callback. 135 136 :returns: a convenience function that you can call to remove the callback. 137 """ 138 global EVENT_MANAGER 139 return EVENT_MANAGER.add_callback(function, evty, obj, args, kwargs, ui=True) 140 141 142def remove_callback(function, evty=None, obj=None): 143 """ 144 Removes a callback. Can remove both ui and non-ui callbacks. 145 146 The parameters passed should match those that were passed when adding 147 the callback 148 """ 149 global EVENT_MANAGER 150 EVENT_MANAGER.remove_callback(function, evty, obj) 151 152 153class Event: 154 """ 155 Represents an Event 156 """ 157 158 __slots__ = ['type', 'object', 'data'] 159 160 def __init__(self, evty, obj, data): 161 """ 162 evty: the 'type' or 'name' for this Event [string] 163 obj: the object emitting the Event [object] 164 data: some piece of data relevant to the Event [object] 165 """ 166 self.type = evty 167 self.object = obj 168 self.data = data 169 170 171class Callback: 172 """ 173 Represents a callback 174 """ 175 176 __slots__ = ['wfunction', 'time', 'args', 'kwargs'] 177 178 def __init__(self, function, time, args, kwargs): 179 """ 180 @param function: the function to call 181 @param time: the time this callback was added 182 """ 183 self.wfunction = _getWeakRef(function) 184 self.time = time 185 self.args = args 186 self.kwargs = kwargs 187 188 def __repr__(self): 189 return '<Callback %s>' % self.wfunction() 190 191 192class _WeakMethod: 193 """Represent a weak bound method, i.e. a method doesn't keep alive the 194 object that it is bound to. It uses WeakRef which, used on its own, 195 produces weak methods that are dead on creation, not very useful. 196 Typically, you will use the getRef() function instead of using 197 this class directly.""" 198 199 def __init__(self, method, notifyDead=None): 200 """ 201 The method must be bound. notifyDead will be called when 202 object that method is bound to dies. 203 """ 204 assert ismethod(method) 205 if method.__self__ is None: 206 raise ValueError("We need a bound method!") 207 if notifyDead is None: 208 self.objRef = weakref.ref(method.__self__) 209 else: 210 self.objRef = weakref.ref(method.__self__, notifyDead) 211 self.fun = method.__func__ 212 213 def __call__(self): 214 objref = self.objRef() 215 if objref is not None: 216 return types.MethodType(self.fun, objref) 217 218 def __eq__(self, method2): 219 if not isinstance(method2, _WeakMethod): 220 return False 221 return ( 222 self.fun is method2.fun 223 and self.objRef() is method2.objRef() 224 and self.objRef() is not None 225 ) 226 227 def __hash__(self): 228 return hash(self.fun) 229 230 def __repr__(self): 231 dead = '' 232 if self.objRef() is None: 233 dead = '; DEAD' 234 obj = '<%s at %s%s>' % (self.__class__, id(self), dead) 235 return obj 236 237 def refs(self, weakRef): 238 """Return true if we are storing same object referred to by weakRef.""" 239 return self.objRef == weakRef 240 241 242def _getWeakRef(obj, notifyDead=None): 243 """ 244 Get a weak reference to obj. If obj is a bound method, a _WeakMethod 245 object, that behaves like a WeakRef, is returned, if it is 246 anything else a WeakRef is returned. If obj is an unbound method, 247 a ValueError will be raised. 248 """ 249 if ismethod(obj): 250 createRef = _WeakMethod 251 else: 252 createRef = weakref.ref 253 254 if notifyDead is None: 255 return createRef(obj) 256 else: 257 return createRef(obj, notifyDead) 258 259 260class EventManager: 261 """ 262 Manages all Events 263 """ 264 265 def __init__(self, use_logger=False, logger_filter=None, verbose=False): 266 # sacrifice space for speed in emit 267 self.all_callbacks = {} 268 self.callbacks = {} 269 self.ui_callbacks = {} 270 self.use_logger = use_logger 271 self.use_verbose_logger = verbose 272 self.logger_filter = logger_filter 273 274 # RLock is needed so that event callbacks can themselves send 275 # synchronous events and add or remove callbacks 276 self.lock = threading.RLock() 277 278 self.pending_ui = [] 279 self.pending_ui_lock = threading.Lock() 280 281 def emit(self, event): 282 """ 283 Emits an Event, calling any registered callbacks. 284 285 event: the Event to emit [Event] 286 """ 287 288 emit_logmsg = self.use_logger and ( 289 not self.logger_filter or re.search(self.logger_filter, event.type) 290 ) 291 emit_verbose = emit_logmsg and self.use_verbose_logger 292 293 global _UiThread 294 is_ui_thread = threading.current_thread() == _UiThread 295 296 # note: a majority of the calls to emit are made on the 297 # UI thread 298 299 if is_ui_thread: 300 self._emit(event, self.all_callbacks, emit_logmsg, emit_verbose) 301 else: 302 # Don't issue the log message twice 303 with self.pending_ui_lock: 304 do_emit = not self.pending_ui 305 self.pending_ui.append( 306 (event, self.ui_callbacks, emit_logmsg, emit_verbose) 307 ) 308 309 if do_emit: 310 GLib.idle_add(self._emit_pending) 311 self._emit(event, self.callbacks, False, emit_verbose) 312 313 def _emit_pending(self): 314 315 with self.pending_ui_lock: 316 events = self.pending_ui 317 self.pending_ui = [] 318 319 for event in events: 320 self._emit(*event) 321 322 def _emit(self, event, exc_callbacks, emit_logmsg, emit_verbose): 323 324 # Accumulate in this set to ensure callbacks only get called once 325 callbacks = set() 326 327 with self.lock: 328 for tcall in [_NONE, event.type]: 329 tcb = exc_callbacks.get(tcall) 330 if tcb is not None: 331 for ocall in [_NONE, event.object]: 332 ocb = tcb.get(ocall) 333 if ocb is not None: 334 callbacks.update(ocb) 335 336 # However, do not actually call the callbacks from within the lock 337 # -> Otherwise non-ui threads could accidentally block the UI if 338 # they decide to run for too long 339 340 for cb in callbacks: 341 try: 342 fn = cb.wfunction() 343 if fn is None: 344 # Remove callbacks that have been garbage collected.. but 345 # really, should be using remove_callback to clean up after 346 # your event handler 347 with self.lock: 348 try: 349 exc_callbacks[event.type][event.object].remove(cb) 350 except (KeyError, ValueError): 351 pass 352 else: 353 if emit_verbose: 354 logger.debug( 355 "Attempting to call " 356 "%(function)s in response " 357 "to %(event)s." % {'function': fn, 'event': event.type} 358 ) 359 fn.__call__( 360 event.type, event.object, event.data, *cb.args, **cb.kwargs 361 ) 362 fn = None 363 except Exception: 364 # something went wrong inside the function we're calling 365 logger.exception("Event callback exception caught!") 366 367 if emit_logmsg: 368 logger.debug( 369 "Sent '%s' event from %r with data %r", 370 event.type, 371 event.object, 372 event.data, 373 ) 374 375 def emit_async(self, event): 376 """ 377 Same as emit(), but does not block. 378 """ 379 GLib.idle_add(self.emit, event) 380 381 def add_callback(self, function, evty, obj, args, kwargs, ui=False): 382 """ 383 Registers a callback. 384 You should always specify at least one of event type or object. 385 386 @param function: The function to call [function] 387 @param evty: The 'type' or 'name' of event to listen for. Defaults 388 to any. [string] 389 @param obj: The object to listen to events from. Defaults 390 to any. [string] 391 392 Returns a convenience function that you can call to 393 remove the callback. 394 """ 395 396 if ui: 397 all_cbs = [self.ui_callbacks, self.all_callbacks] 398 else: 399 all_cbs = [self.callbacks, self.all_callbacks] 400 401 destroy_with = kwargs.pop('destroy_with', None) 402 403 if evty is None: 404 evty = _NONE 405 if obj is None: 406 obj = _NONE 407 408 with self.lock: 409 cb = Callback(function, time.time(), args, kwargs) 410 411 # add the specified categories if needed. 412 for cbs in all_cbs: 413 if evty not in cbs: 414 cbs[evty] = weakref.WeakKeyDictionary() 415 try: 416 callbacks = cbs[evty][obj] 417 except KeyError: 418 callbacks = cbs[evty][obj] = [] 419 420 # add the actual callback 421 callbacks.append(cb) 422 423 if self.use_logger: 424 if ( 425 not self.logger_filter 426 or evty is _NONE 427 or re.search(self.logger_filter, evty) 428 ): 429 logger.debug("Added callback %s for [%s, %s]" % (function, evty, obj)) 430 431 if destroy_with is not None: 432 destroy_with.connect( 433 'destroy', lambda w: self.remove_callback(function, evty, obj) 434 ) 435 436 return lambda: self.remove_callback(function, evty, obj) 437 438 def remove_callback(self, function, evty=None, obj=None): 439 """ 440 Unsets a callback 441 442 The parameters must match those given when the callback was 443 registered. (minus any additional args) 444 """ 445 if evty is None: 446 evty = _NONE 447 if obj is None: 448 obj = _NONE 449 450 with self.lock: 451 for cbs in [self.callbacks, self.all_callbacks, self.ui_callbacks]: 452 remove = [] 453 try: 454 callbacks = cbs[evty][obj] 455 for cb in callbacks: 456 if cb.wfunction() == function: 457 remove.append(cb) 458 except KeyError: 459 continue 460 except TypeError: 461 continue 462 463 for cb in remove: 464 callbacks.remove(cb) 465 466 if len(callbacks) == 0: 467 del cbs[evty][obj] 468 if len(cbs[evty]) == 0: 469 del cbs[evty] 470 471 if self.use_logger: 472 if ( 473 not self.logger_filter 474 or evty is _NONE 475 or re.search(self.logger_filter, evty) 476 ): 477 logger.debug("Removed callback %s for [%s, %s]" % (function, evty, obj)) 478 479 480EVENT_MANAGER = EventManager() 481 482# vim: et sts=4 sw=4 483