暂无描述

Builder.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
  2. ### BEGIN LICENSE
  3. # Copyright (C) 2013 Bryan M. Allred <bryan.allred@gmail.com>
  4. # This program is free software: you can redistribute it and/or modify it
  5. # under the terms of the GNU General Public License version 3, as published
  6. # by the Free Software Foundation.
  7. #
  8. # This program is distributed in the hope that it will be useful, but
  9. # WITHOUT ANY WARRANTY; without even the implied warranties of
  10. # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
  11. # PURPOSE. See the GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along
  14. # with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ### END LICENSE
  16. ### DO NOT EDIT THIS FILE ###
  17. '''Enhances builder connections, provides object to access glade objects'''
  18. import inspect
  19. import functools
  20. import logging
  21. from gi.repository import GObject, Gtk # pylint: disable=E0611
  22. from xml.etree.cElementTree import ElementTree
  23. logger = logging.getLogger('organiccode_lib')
  24. # this module is big so uses some conventional prefixes and postfixes
  25. # *s list, except self.widgets is a dictionary
  26. # *_dict dictionary
  27. # *name string
  28. # ele_* element in a ElementTree
  29. # pylint: disable=R0904
  30. # the many public methods is a feature of Gtk.Builder
  31. class Builder(Gtk.Builder):
  32. ''' extra features
  33. connects glade defined handler to default_handler if necessary
  34. auto connects widget to handler with matching name or alias
  35. auto connects several widgets to a handler via multiple aliases
  36. allow handlers to lookup widget name
  37. logs every connection made, and any on_* not made
  38. '''
  39. def __init__(self):
  40. Gtk.Builder.__init__(self)
  41. self.widgets = {}
  42. self.glade_handler_dict = {}
  43. self.connections = []
  44. self._reverse_widget_dict = {}
  45. # pylint: disable=R0201
  46. # this is a method so that a subclass of Builder can redefine it
  47. def default_handler(self,
  48. handler_name, filename, *args, **kwargs):
  49. '''helps the apprentice guru
  50. glade defined handlers that do not exist come here instead.
  51. An apprentice guru might wonder which signal does what he wants,
  52. now he can define any likely candidates in glade and notice which
  53. ones get triggered when he plays with the project.
  54. this method does not appear in Gtk.Builder'''
  55. logger.debug('''tried to call non-existent function:%s()
  56. expected in %s
  57. args:%s
  58. kwargs:%s''', handler_name, filename, args, kwargs)
  59. # pylint: enable=R0201
  60. def get_name(self, widget):
  61. ''' allows a handler to get the name (id) of a widget
  62. this method does not appear in Gtk.Builder'''
  63. return self._reverse_widget_dict.get(widget)
  64. def add_from_file(self, filename):
  65. '''parses xml file and stores wanted details'''
  66. Gtk.Builder.add_from_file(self, filename)
  67. # extract data for the extra interfaces
  68. tree = ElementTree()
  69. tree.parse(filename)
  70. ele_widgets = tree.getiterator("object")
  71. for ele_widget in ele_widgets:
  72. name = ele_widget.attrib['id']
  73. widget = self.get_object(name)
  74. # populate indexes - a dictionary of widgets
  75. self.widgets[name] = widget
  76. # populate a reversed dictionary
  77. self._reverse_widget_dict[widget] = name
  78. # populate connections list
  79. ele_signals = ele_widget.findall("signal")
  80. connections = [
  81. (name,
  82. ele_signal.attrib['name'],
  83. ele_signal.attrib['handler']) for ele_signal in ele_signals]
  84. if connections:
  85. self.connections.extend(connections)
  86. ele_signals = tree.getiterator("signal")
  87. for ele_signal in ele_signals:
  88. self.glade_handler_dict.update(
  89. {ele_signal.attrib["handler"]: None})
  90. def connect_signals(self, callback_obj):
  91. '''connect the handlers defined in glade
  92. reports successful and failed connections
  93. and logs call to missing handlers'''
  94. filename = inspect.getfile(callback_obj.__class__)
  95. callback_handler_dict = dict_from_callback_obj(callback_obj)
  96. connection_dict = {}
  97. connection_dict.update(self.glade_handler_dict)
  98. connection_dict.update(callback_handler_dict)
  99. for item in connection_dict.items():
  100. if item[1] is None:
  101. # the handler is missing so reroute to default_handler
  102. handler = functools.partial(
  103. self.default_handler, item[0], filename)
  104. connection_dict[item[0]] = handler
  105. # replace the run time warning
  106. logger.warn("expected handler '%s' in %s",
  107. item[0], filename)
  108. # connect glade define handlers
  109. Gtk.Builder.connect_signals(self, connection_dict)
  110. # let's tell the user how we applied the glade design
  111. for connection in self.connections:
  112. widget_name, signal_name, handler_name = connection
  113. logger.debug("connect builder by design '%s', '%s', '%s'",
  114. widget_name, signal_name, handler_name)
  115. def get_ui(self, callback_obj=None, by_name=True):
  116. '''Creates the ui object with widgets as attributes
  117. connects signals by 2 methods
  118. this method does not appear in Gtk.Builder'''
  119. result = UiFactory(self.widgets)
  120. # Hook up any signals the user defined in glade
  121. if callback_obj is not None:
  122. # connect glade define handlers
  123. self.connect_signals(callback_obj)
  124. if by_name:
  125. auto_connect_by_name(callback_obj, self)
  126. return result
  127. # pylint: disable=R0903
  128. # this class deliberately does not provide any public interfaces
  129. # apart from the glade widgets
  130. class UiFactory():
  131. ''' provides an object with attributes as glade widgets'''
  132. def __init__(self, widget_dict):
  133. self._widget_dict = widget_dict
  134. for (widget_name, widget) in widget_dict.items():
  135. setattr(self, widget_name, widget)
  136. # Mangle any non-usable names (like with spaces or dashes)
  137. # into pythonic ones
  138. cannot_message = """cannot bind ui.%s, name already exists
  139. consider using a pythonic name instead of design name '%s'"""
  140. consider_message = """consider using a pythonic name instead of design name '%s'"""
  141. for (widget_name, widget) in widget_dict.items():
  142. pyname = make_pyname(widget_name)
  143. if pyname != widget_name:
  144. if hasattr(self, pyname):
  145. logger.debug(cannot_message, pyname, widget_name)
  146. else:
  147. logger.debug(consider_message, widget_name)
  148. setattr(self, pyname, widget)
  149. def iterator():
  150. '''Support 'for o in self' '''
  151. return iter(widget_dict.values())
  152. setattr(self, '__iter__', iterator)
  153. def __getitem__(self, name):
  154. 'access as dictionary where name might be non-pythonic'
  155. return self._widget_dict[name]
  156. # pylint: enable=R0903
  157. def make_pyname(name):
  158. ''' mangles non-pythonic names into pythonic ones'''
  159. pyname = ''
  160. for character in name:
  161. if (character.isalpha() or character == '_' or
  162. (pyname and character.isdigit())):
  163. pyname += character
  164. else:
  165. pyname += '_'
  166. return pyname
  167. # Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
  168. # need to reimplement inspect.getmembers. GObject introspection doesn't
  169. # play nice with it.
  170. def getmembers(obj, check):
  171. members = []
  172. for k in dir(obj):
  173. try:
  174. attr = getattr(obj, k)
  175. except:
  176. continue
  177. if check(attr):
  178. members.append((k, attr))
  179. members.sort()
  180. return members
  181. def dict_from_callback_obj(callback_obj):
  182. '''a dictionary interface to callback_obj'''
  183. methods = getmembers(callback_obj, inspect.ismethod)
  184. aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
  185. # a method may have several aliases
  186. #~ @alias('on_btn_foo_clicked')
  187. #~ @alias('on_tool_foo_activate')
  188. #~ on_menu_foo_activate():
  189. #~ pass
  190. alias_groups = [(x.aliases, x) for x in aliased_methods]
  191. aliases = []
  192. for item in alias_groups:
  193. for alias in item[0]:
  194. aliases.append((alias, item[1]))
  195. dict_methods = dict(methods)
  196. dict_aliases = dict(aliases)
  197. results = {}
  198. results.update(dict_methods)
  199. results.update(dict_aliases)
  200. return results
  201. def auto_connect_by_name(callback_obj, builder):
  202. '''finds handlers like on_<widget_name>_<signal> and connects them
  203. i.e. find widget,signal pair in builder and call
  204. widget.connect(signal, on_<widget_name>_<signal>)'''
  205. callback_handler_dict = dict_from_callback_obj(callback_obj)
  206. for item in builder.widgets.items():
  207. (widget_name, widget) = item
  208. signal_ids = []
  209. try:
  210. widget_type = type(widget)
  211. while widget_type:
  212. signal_ids.extend(GObject.signal_list_ids(widget_type))
  213. widget_type = GObject.type_parent(widget_type)
  214. except RuntimeError: # pylint wants a specific error
  215. pass
  216. signal_names = [GObject.signal_name(sid) for sid in signal_ids]
  217. # Now, automatically find any the user didn't specify in glade
  218. for sig in signal_names:
  219. # using convention suggested by glade
  220. sig = sig.replace("-", "_")
  221. handler_names = ["on_%s_%s" % (widget_name, sig)]
  222. # Using the convention that the top level window is not
  223. # specified in the handler name. That is use
  224. # on_destroy() instead of on_windowname_destroy()
  225. if widget is callback_obj:
  226. handler_names.append("on_%s" % sig)
  227. do_connect(item, sig, handler_names,
  228. callback_handler_dict, builder.connections)
  229. log_unconnected_functions(callback_handler_dict, builder.connections)
  230. def do_connect(item, signal_name, handler_names,
  231. callback_handler_dict, connections):
  232. '''connect this signal to an unused handler'''
  233. widget_name, widget = item
  234. for handler_name in handler_names:
  235. target = handler_name in callback_handler_dict.keys()
  236. connection = (widget_name, signal_name, handler_name)
  237. duplicate = connection in connections
  238. if target and not duplicate:
  239. widget.connect(signal_name, callback_handler_dict[handler_name])
  240. connections.append(connection)
  241. logger.debug("connect builder by name '%s','%s', '%s'",
  242. widget_name, signal_name, handler_name)
  243. def log_unconnected_functions(callback_handler_dict, connections):
  244. '''log functions like on_* that we could not connect'''
  245. connected_functions = [x[2] for x in connections]
  246. handler_names = callback_handler_dict.keys()
  247. unconnected = [x for x in handler_names if x.startswith('on_')]
  248. for handler_name in connected_functions:
  249. try:
  250. unconnected.remove(handler_name)
  251. except ValueError:
  252. pass
  253. for handler_name in unconnected:
  254. logger.debug("Not connected to builder '%s'", handler_name)