aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/signals.py
diff options
context:
space:
mode:
authorJesús Eduardo <heckyel@hyperbola.info>2017-09-11 17:47:17 -0500
committerJesús Eduardo <heckyel@hyperbola.info>2017-09-11 17:47:17 -0500
commit14738704ede6dfa6ac79f362a9c1f7f40f470cdc (patch)
tree31c83bdd188ae7b64d7169974d6f066ccfe95367 /lvc/signals.py
parenteb1896583afbbb622cadcde1a24e17173f61904f (diff)
downloadlibrevideoconverter-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.py299
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()