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