signals.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. # @Base: Miro - an RSS based video player application
  2. # Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
  3. # Participatory Culture Foundation
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  18. #
  19. # In addition, as a special exception, the copyright holders give
  20. # permission to link the code of portions of this program with the OpenSSL
  21. # library.
  22. #
  23. # You must obey the GNU General Public License in all respects for all of
  24. # the code used other than OpenSSL. If you modify file(s) with this
  25. # exception, you may extend this exception to your version of the file(s),
  26. # but you are not obligated to do so. If you do not wish to do so, delete
  27. # this exception statement from your version. If you delete this exception
  28. # statement from all source files in the program, then also delete it here.
  29. """signals.py
  30. GObject-like signal handling for Miro.
  31. """
  32. import itertools
  33. import logging
  34. import sys
  35. import weakref
  36. class NestedSignalError(StandardError):
  37. pass
  38. class WeakMethodReference:
  39. """Used to handle weak references to a method.
  40. We can't simply keep a weak reference to method itself, because there
  41. almost certainly aren't any other references to it. Instead we keep a
  42. weak reference to the object, it's class and the unbound method. This
  43. gives us enough info to recreate the bound method when we need it.
  44. """
  45. def __init__(self, method):
  46. self.object = weakref.ref(method.im_self)
  47. self.func = weakref.ref(method.im_func)
  48. # don't create a weak reference to the class. That only works for
  49. # new-style classes. It's highly unlikely the class will ever need to
  50. # be garbage collected anyways.
  51. self.cls = method.im_class
  52. def __call__(self):
  53. func = self.func()
  54. if func is None:
  55. return None
  56. obj = self.object()
  57. if obj is None:
  58. return None
  59. return func.__get__(obj, self.cls)
  60. class Callback:
  61. def __init__(self, func, extra_args):
  62. self.func = func
  63. self.extra_args = extra_args
  64. def invoke(self, obj, args):
  65. return self.func(obj, *(args + self.extra_args))
  66. def compare_function(self, func):
  67. return self.func == func
  68. def is_dead(self):
  69. return False
  70. class WeakCallback:
  71. def __init__(self, method, extra_args):
  72. self.ref = WeakMethodReference(method)
  73. self.extra_args = extra_args
  74. def compare_function(self, func):
  75. return self.ref() == func
  76. def invoke(self, obj, args):
  77. callback = self.ref()
  78. if callback is not None:
  79. return callback(obj, *(args + self.extra_args))
  80. else:
  81. return None
  82. def is_dead(self):
  83. return self.ref() is None
  84. class SignalEmitter(object):
  85. def __init__(self, *signal_names):
  86. self.signal_callbacks = {}
  87. self.id_generator = itertools.count()
  88. self._currently_emitting = set()
  89. self._frozen = False
  90. for name in signal_names:
  91. self.create_signal(name)
  92. def freeze_signals(self):
  93. self._frozen = True
  94. def thaw_signals(self):
  95. self._frozen = False
  96. def create_signal(self, name):
  97. self.signal_callbacks[name] = {}
  98. def get_callbacks(self, signal_name):
  99. try:
  100. return self.signal_callbacks[signal_name]
  101. except KeyError:
  102. raise KeyError("Signal: %s doesn't exist" % signal_name)
  103. def _check_already_connected(self, name, func):
  104. for callback in self.get_callbacks(name).values():
  105. if callback.compare_function(func):
  106. raise ValueError("signal %s already connected to %s" %
  107. (name, func))
  108. def connect(self, name, func, *extra_args):
  109. """Connect a callback to a signal. Returns an callback handle that
  110. can be passed into disconnect().
  111. If func is already connected to the signal, then a ValueError will be
  112. raised.
  113. """
  114. self._check_already_connected(name, func)
  115. id_ = self.id_generator.next()
  116. callbacks = self.get_callbacks(name)
  117. callbacks[id_] = Callback(func, extra_args)
  118. return (name, id_)
  119. def connect_weak(self, name, method, *extra_args):
  120. """Connect a callback weakly. Callback must be a method of some
  121. object. We create a weak reference to the method, so that the
  122. connection doesn't keep the object from being garbage collected.
  123. If method is already connected to the signal, then a ValueError will be
  124. raised.
  125. """
  126. self._check_already_connected(name, method)
  127. if not hasattr(method, 'im_self'):
  128. raise TypeError("connect_weak must be called with object methods")
  129. id_ = self.id_generator.next()
  130. callbacks = self.get_callbacks(name)
  131. callbacks[id_] = WeakCallback(method, extra_args)
  132. return (name, id_)
  133. def disconnect(self, callback_handle):
  134. """Disconnect a signal. callback_handle must be the return value from
  135. connect() or connect_weak().
  136. """
  137. callbacks = self.get_callbacks(callback_handle[0])
  138. if callback_handle[1] in callbacks:
  139. del callbacks[callback_handle[1]]
  140. else:
  141. logging.warning(
  142. "disconnect called but callback_handle not in the callback")
  143. def disconnect_all(self):
  144. for signal in self.signal_callbacks:
  145. self.signal_callbacks[signal] = {}
  146. def emit(self, name, *args):
  147. if self._frozen:
  148. return
  149. if name in self._currently_emitting:
  150. raise NestedSignalError("Can't emit %s while handling %s" %
  151. (name, name))
  152. self._currently_emitting.add(name)
  153. try:
  154. callback_returned_true = self._run_signal(name, args)
  155. finally:
  156. self._currently_emitting.discard(name)
  157. self.clear_old_weak_references()
  158. return callback_returned_true
  159. def _run_signal(self, name, args):
  160. callback_returned_true = False
  161. try:
  162. self_callback = getattr(self, 'do_' + name.replace('-', '_'))
  163. except AttributeError:
  164. pass
  165. else:
  166. if self_callback(*args):
  167. callback_returned_true = True
  168. if not callback_returned_true:
  169. for callback in self.get_callbacks(name).values():
  170. if callback.invoke(self, args):
  171. callback_returned_true = True
  172. break
  173. return callback_returned_true
  174. def clear_old_weak_references(self):
  175. for callback_map in self.signal_callbacks.values():
  176. for id_ in callback_map.keys():
  177. if callback_map[id_].is_dead():
  178. del callback_map[id_]
  179. class SystemSignals(SignalEmitter):
  180. """System wide signals for Miro. These can be accessed from the singleton
  181. object signals.system. Signals include:
  182. "error" - A problem occurred in Miro. The frontend should let the user
  183. know this happened, hopefully with a nice dialog box or something that
  184. lets the user report the error to bugzilla.
  185. Arguments:
  186. - report -- string that can be submitted to the bug tracker
  187. - exception -- Exception object (can be None)
  188. "startup-success" - The startup process is complete. The frontend should
  189. wait for this signal to show the UI to the user.
  190. No arguments.
  191. "startup-failure" - The startup process fails. The frontend should inform
  192. the user that this happened and quit.
  193. Arguments:
  194. - summary -- Short, user-friendly, summary of the problem
  195. - description -- Longer explanation of the problem
  196. "shutdown" - The backend has shutdown. The event loop is stopped at this
  197. point.
  198. No arguments.
  199. "update-available" - A new version of LibreVideoConverter is available.
  200. Arguments:
  201. - rssItem -- The RSS item for the latest version (in sparkle
  202. appcast format).
  203. "new-dialog" - The backend wants to display a dialog to the user.
  204. Arguments:
  205. - dialog -- The dialog to be displayed.
  206. "theme-first-run" - A theme was used for the first time
  207. Arguments:
  208. - theme -- The name of the theme.
  209. "videos-added" -- Videos were added via the singleclick module.
  210. Arguments:
  211. - view -- A database view than contains the videos.
  212. "download-complete" -- A download was completed.
  213. Arguments:
  214. - item -- an Item of class Item.
  215. """
  216. def __init__(self):
  217. SignalEmitter.__init__(self, 'error', 'startup-success',
  218. 'startup-failure', 'shutdown',
  219. 'update-available', 'new-dialog',
  220. 'theme-first-run', 'videos-added',
  221. 'download-complete')
  222. def shutdown(self):
  223. self.emit('shutdown')
  224. def update_available(self, latest):
  225. self.emit('update-available', latest)
  226. def new_dialog(self, dialog):
  227. self.emit('new-dialog', dialog)
  228. def theme_first_run(self, theme):
  229. self.emit('theme-first-run', theme)
  230. def videos_added(self, view):
  231. self.emit('videos-added', view)
  232. def download_complete(self, item):
  233. self.emit('download-complete', item)
  234. def failed_exn(self, when, details=None):
  235. self.failed(when, with_exception=True, details=details)
  236. def failed(self, when, with_exception=False, details=None):
  237. """Used to emit the error signal. Formats a nice crash report."""
  238. if with_exception:
  239. exc_info = sys.exc_info()
  240. else:
  241. exc_info = None
  242. logging.error('%s: %s' % (when, details), exc_info=exc_info)
  243. system = SystemSignals()