diff options
author | Jesús Eduardo <heckyel@hyperbola.info> | 2017-09-11 17:47:17 -0500 |
---|---|---|
committer | Jesús Eduardo <heckyel@hyperbola.info> | 2017-09-11 17:47:17 -0500 |
commit | 14738704ede6dfa6ac79f362a9c1f7f40f470cdc (patch) | |
tree | 31c83bdd188ae7b64d7169974d6f066ccfe95367 /lvc/signals.py | |
parent | eb1896583afbbb622cadcde1a24e17173f61904f (diff) | |
download | librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.lz librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.xz librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.zip |
rename mvc at lvc
Diffstat (limited to 'lvc/signals.py')
-rw-r--r-- | lvc/signals.py | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/lvc/signals.py b/lvc/signals.py new file mode 100644 index 0000000..2f64dc9 --- /dev/null +++ b/lvc/signals.py @@ -0,0 +1,299 @@ +# @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() |