# @Base: Miro - an RSS based video player application # Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 # Participatory Culture Foundation # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # In addition, as a special exception, the copyright holders give # permission to link the code of portions of this program with the OpenSSL # library. # # You must obey the GNU General Public License in all respects for all of # the code used other than OpenSSL. If you modify file(s) with this # exception, you may extend this exception to your version of the file(s), # but you are not obligated to do so. If you do not wish to do so, delete # this exception statement from your version. If you delete this exception # statement from all source files in the program, then also delete it here. """signals.py GObject-like signal handling for Miro. """ import itertools import logging import sys import weakref class NestedSignalError(StandardError): pass class WeakMethodReference: """Used to handle weak references to a method. We can't simply keep a weak reference to method itself, because there almost certainly aren't any other references to it. Instead we keep a weak reference to the object, it's class and the unbound method. This gives us enough info to recreate the bound method when we need it. """ def __init__(self, method): self.object = weakref.ref(method.im_self) self.func = weakref.ref(method.im_func) # don't create a weak reference to the class. That only works for # new-style classes. It's highly unlikely the class will ever need to # be garbage collected anyways. self.cls = method.im_class def __call__(self): func = self.func() if func is None: return None obj = self.object() if obj is None: return None return func.__get__(obj, self.cls) class Callback: def __init__(self, func, extra_args): self.func = func self.extra_args = extra_args def invoke(self, obj, args): return self.func(obj, *(args + self.extra_args)) def compare_function(self, func): return self.func == func def is_dead(self): return False class WeakCallback: def __init__(self, method, extra_args): self.ref = WeakMethodReference(method) self.extra_args = extra_args def compare_function(self, func): return self.ref() == func def invoke(self, obj, args): callback = self.ref() if callback is not None: return callback(obj, *(args + self.extra_args)) else: return None def is_dead(self): return self.ref() is None class SignalEmitter(object): def __init__(self, *signal_names): self.signal_callbacks = {} self.id_generator = itertools.count() self._currently_emitting = set() self._frozen = False for name in signal_names: self.create_signal(name) def freeze_signals(self): self._frozen = True def thaw_signals(self): self._frozen = False def create_signal(self, name): self.signal_callbacks[name] = {} def get_callbacks(self, signal_name): try: return self.signal_callbacks[signal_name] except KeyError: raise KeyError("Signal: %s doesn't exist" % signal_name) def _check_already_connected(self, name, func): for callback in self.get_callbacks(name).values(): if callback.compare_function(func): raise ValueError("signal %s already connected to %s" % (name, func)) def connect(self, name, func, *extra_args): """Connect a callback to a signal. Returns an callback handle that can be passed into disconnect(). If func is already connected to the signal, then a ValueError will be raised. """ self._check_already_connected(name, func) id_ = self.id_generator.next() callbacks = self.get_callbacks(name) callbacks[id_] = Callback(func, extra_args) return (name, id_) def connect_weak(self, name, method, *extra_args): """Connect a callback weakly. Callback must be a method of some object. We create a weak reference to the method, so that the connection doesn't keep the object from being garbage collected. If method is already connected to the signal, then a ValueError will be raised. """ self._check_already_connected(name, method) if not hasattr(method, 'im_self'): raise TypeError("connect_weak must be called with object methods") id_ = self.id_generator.next() callbacks = self.get_callbacks(name) callbacks[id_] = WeakCallback(method, extra_args) return (name, id_) def disconnect(self, callback_handle): """Disconnect a signal. callback_handle must be the return value from connect() or connect_weak(). """ callbacks = self.get_callbacks(callback_handle[0]) if callback_handle[1] in callbacks: del callbacks[callback_handle[1]] else: logging.warning( "disconnect called but callback_handle not in the callback") def disconnect_all(self): for signal in self.signal_callbacks: self.signal_callbacks[signal] = {} def emit(self, name, *args): if self._frozen: return if name in self._currently_emitting: raise NestedSignalError("Can't emit %s while handling %s" % (name, name)) self._currently_emitting.add(name) try: callback_returned_true = self._run_signal(name, args) finally: self._currently_emitting.discard(name) self.clear_old_weak_references() return callback_returned_true def _run_signal(self, name, args): callback_returned_true = False try: self_callback = getattr(self, 'do_' + name.replace('-', '_')) except AttributeError: pass else: if self_callback(*args): callback_returned_true = True if not callback_returned_true: for callback in self.get_callbacks(name).values(): if callback.invoke(self, args): callback_returned_true = True break return callback_returned_true def clear_old_weak_references(self): for callback_map in self.signal_callbacks.values(): for id_ in callback_map.keys(): if callback_map[id_].is_dead(): del callback_map[id_] class SystemSignals(SignalEmitter): """System wide signals for Miro. These can be accessed from the singleton object signals.system. Signals include: "error" - A problem occurred in Miro. The frontend should let the user know this happened, hopefully with a nice dialog box or something that lets the user report the error to bugzilla. Arguments: - report -- string that can be submitted to the bug tracker - exception -- Exception object (can be None) "startup-success" - The startup process is complete. The frontend should wait for this signal to show the UI to the user. No arguments. "startup-failure" - The startup process fails. The frontend should inform the user that this happened and quit. Arguments: - summary -- Short, user-friendly, summary of the problem - description -- Longer explanation of the problem "shutdown" - The backend has shutdown. The event loop is stopped at this point. No arguments. "update-available" - A new version of LibreVideoConverter is available. Arguments: - rssItem -- The RSS item for the latest version (in sparkle appcast format). "new-dialog" - The backend wants to display a dialog to the user. Arguments: - dialog -- The dialog to be displayed. "theme-first-run" - A theme was used for the first time Arguments: - theme -- The name of the theme. "videos-added" -- Videos were added via the singleclick module. Arguments: - view -- A database view than contains the videos. "download-complete" -- A download was completed. Arguments: - item -- an Item of class Item. """ def __init__(self): SignalEmitter.__init__(self, 'error', 'startup-success', 'startup-failure', 'shutdown', 'update-available', 'new-dialog', 'theme-first-run', 'videos-added', 'download-complete') def shutdown(self): self.emit('shutdown') def update_available(self, latest): self.emit('update-available', latest) def new_dialog(self, dialog): self.emit('new-dialog', dialog) def theme_first_run(self, theme): self.emit('theme-first-run', theme) def videos_added(self, view): self.emit('videos-added', view) def download_complete(self, item): self.emit('download-complete', item) def failed_exn(self, when, details=None): self.failed(when, with_exception=True, details=details) def failed(self, when, with_exception=False, details=None): """Used to emit the error signal. Formats a nice crash report.""" if with_exception: exc_info = sys.exc_info() else: exc_info = None logging.error('%s: %s' % (when, details), exc_info=exc_info) system = SystemSignals()