Advertisement
Guest User

Untitled

a guest
Jun 17th, 2022
1,065
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.86 KB | None | 0 0
  1. # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
  2. ### BEGIN LICENSE
  3. # Copyright (C) 2014-2015 Perfect Privacy <support@perfect-privacy.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.  
  17. """Enhances builder connections, provides object to access glade objects"""
  18.  
  19. # noinspection PyUnresolvedReferences
  20. from gi.repository import GObject, Gtk # pylint: disable=E0611
  21. import inspect
  22. import functools
  23. import logging
  24. from xml.etree.cElementTree import ElementTree
  25.  
  26. # this module is big so uses some conventional prefixes and postfixes
  27. # *s list, except self.widgets is a dictionary
  28. # *_dict dictionary
  29. # *name string
  30. # ele_* element in a ElementTree
  31.  
  32.  
  33. # pylint: disable=R0904
  34. # the many public methods is a feature of Gtk.Builder
  35. class Builder(Gtk.Builder):
  36. """
  37. extra features
  38. connects glade defined handler to default_handler if necessary
  39. auto connects widget to handler with matching name or alias
  40. auto connects several widgets to a handler via multiple aliases
  41. allow handlers to lookup widget name
  42. logs every connection made, and any on_* not made
  43. """
  44.  
  45. def __init__(self):
  46. Gtk.Builder.__init__(self)
  47. self.widgets = {}
  48. self.glade_handler_dict = {}
  49. self.connections = []
  50. self._reverse_widget_dict = {}
  51. self._logger = logging.getLogger(__name__)
  52.  
  53. # pylint: disable=R0201
  54. # this is a method so that a subclass of Builder can redefine it
  55. def default_handler(self, handler_name, filename, *args, **kwargs):
  56. """
  57. helps the apprentice guru
  58.  
  59. glade defined handlers that do not exist come here instead.
  60. An apprentice guru might wonder which signal does what he wants,
  61. now he can define any likely candidates in glade and notice which
  62. ones get triggered when he plays with the project.
  63. this method does not appear in Gtk.Builder
  64. """
  65. self._logger.debug('''tried to call non-existent function:%s()
  66. expected in %s
  67. args:%s
  68. kwargs:%s''', handler_name, filename, args, kwargs)
  69. # pylint: enable=R0201
  70.  
  71. def get_name(self, widget):
  72. """
  73. allows a handler to get the name (id) of a widget
  74.  
  75. this method does not appear in Gtk.Builder
  76. """
  77. return self._reverse_widget_dict.get(widget)
  78.  
  79. def add_from_file(self, filename):
  80. """parses xml file and stores wanted details"""
  81. Gtk.Builder.add_from_file(self, filename)
  82.  
  83. # extract data for the extra interfaces
  84. tree = ElementTree()
  85. tree.parse(filename)
  86.  
  87. # TODO: dreprecated
  88. ele_widgets = tree.iter("object")
  89. for ele_widget in ele_widgets:
  90. name = ele_widget.attrib['id']
  91. widget = self.get_object(name)
  92.  
  93. # populate indexes - a dictionary of widgets
  94. self.widgets[name] = widget
  95.  
  96. # populate a reversed dictionary
  97. self._reverse_widget_dict[widget] = name
  98.  
  99. # populate connections list
  100. ele_signals = ele_widget.findall("signal")
  101.  
  102. connections = [
  103. (name,
  104. ele_signal.attrib['name'],
  105. ele_signal.attrib['handler']) for ele_signal in ele_signals]
  106.  
  107. if connections:
  108. self.connections.extend(connections)
  109.  
  110. #TODO: dreprecated
  111. ele_signals = tree.iter("signal")
  112. for ele_signal in ele_signals:
  113. self.glade_handler_dict.update(
  114. {ele_signal.attrib["handler"]: None})
  115.  
  116. def connect_signals(self, callback_obj):
  117. """
  118. connect the handlers defined in glade
  119.  
  120. reports successful and failed connections
  121. and logs call to missing handlers
  122. """
  123.  
  124. filename = inspect.getfile(callback_obj.__class__)
  125. callback_handler_dict = dict_from_callback_obj(callback_obj)
  126. connection_dict = {}
  127. connection_dict.update(self.glade_handler_dict)
  128. connection_dict.update(callback_handler_dict)
  129. for item in connection_dict.items():
  130. if item[1] is None:
  131. # the handler is missing so reroute to default_handler
  132. handler = functools.partial(
  133. self.default_handler, item[0], filename)
  134.  
  135. connection_dict[item[0]] = handler
  136.  
  137. # replace the run time warning
  138. self._logger.warn("expected handler '%s' in %s",
  139. item[0], filename)
  140.  
  141. # connect glade define handlers
  142. Gtk.Builder.connect_signals(self, connection_dict)
  143.  
  144. # let's tell the user how we applied the glade design
  145. for connection in self.connections:
  146. widget_name, signal_name, handler_name = connection
  147. self._logger.debug("connect builder by design '%s', '%s', '%s'",
  148. widget_name, signal_name, handler_name)
  149.  
  150. def get_ui(self, callback_obj=None, by_name=True):
  151. """
  152. Creates the ui object with widgets as attributes
  153.  
  154. connects signals by 2 methods
  155. this method does not appear in Gtk.Builder
  156. """
  157.  
  158. result = UiFactory(self.widgets)
  159.  
  160. # Hook up any signals the user defined in glade
  161. if callback_obj is not None:
  162. # connect glade define handlers
  163. self.connect_signals(callback_obj)
  164.  
  165. if by_name:
  166. auto_connect_by_name(callback_obj, self)
  167.  
  168. return result
  169.  
  170.  
  171. # pylint: disable=R0903
  172. # this class deliberately does not provide any public interfaces
  173. # apart from the glade widgets
  174. class UiFactory():
  175. """ provides an object with attributes as glade widgets"""
  176. def __init__(self, widget_dict):
  177. self._logger = logging.getLogger(__name__)
  178. self._widget_dict = widget_dict
  179. for (widget_name, widget) in widget_dict.items():
  180. setattr(self, widget_name, widget)
  181.  
  182. # Mangle any non-usable names (like with spaces or dashes)
  183. # into pythonic ones
  184. cannot_message = """cannot bind ui.%s, name already exists
  185. consider using a pythonic name instead of design name '%s'"""
  186. consider_message = """consider using a pythonic name instead of design name '%s'"""
  187.  
  188. for (widget_name, widget) in widget_dict.items():
  189. pyname = make_pyname(widget_name)
  190. if pyname != widget_name:
  191. if hasattr(self, pyname):
  192. self._logger.debug(cannot_message, pyname, widget_name)
  193. else:
  194. self._logger.debug(consider_message, widget_name)
  195. setattr(self, pyname, widget)
  196.  
  197. def iterator():
  198. """Support 'for o in self' """
  199. return iter(widget_dict.values())
  200. setattr(self, '__iter__', iterator)
  201.  
  202. def __getitem__(self, name):
  203. """access as dictionary where name might be non-pythonic"""
  204. return self._widget_dict[name]
  205. # pylint: enable=R0903
  206.  
  207.  
  208. def make_pyname(name):
  209. """ mangles non-pythonic names into pythonic ones"""
  210. pyname = ''
  211. for character in name:
  212. if (character.isalpha() or character == '_' or
  213. (pyname and character.isdigit())):
  214. pyname += character
  215. else:
  216. pyname += '_'
  217. return pyname
  218.  
  219.  
  220. # Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
  221. # need to reimplement inspect.getmembers. GObject introspection doesn't
  222. # play nice with it.
  223. def getmembers(obj, check):
  224. members = []
  225. for k in dir(obj):
  226. try:
  227. attr = getattr(obj, k)
  228. except:
  229. continue
  230. if check(attr):
  231. members.append((k, attr))
  232. members.sort()
  233. return members
  234.  
  235.  
  236. def dict_from_callback_obj(callback_obj):
  237. """a dictionary interface to callback_obj"""
  238. methods = getmembers(callback_obj, inspect.ismethod)
  239.  
  240. aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
  241.  
  242. # a method may have several aliases
  243. #~ @alias('on_btn_foo_clicked')
  244. #~ @alias('on_tool_foo_activate')
  245. #~ on_menu_foo_activate():
  246. #~ pass
  247. alias_groups = [(x.aliases, x) for x in aliased_methods]
  248.  
  249. aliases = []
  250. for item in alias_groups:
  251. for alias in item[0]:
  252. aliases.append((alias, item[1]))
  253.  
  254. dict_methods = dict(methods)
  255. dict_aliases = dict(aliases)
  256.  
  257. results = {}
  258. results.update(dict_methods)
  259. results.update(dict_aliases)
  260.  
  261. return results
  262.  
  263.  
  264. def auto_connect_by_name(callback_obj, builder):
  265. """
  266. finds handlers like on_<widget_name>_<signal> and connects them
  267.  
  268. i.e. find widget,signal pair in builder and call
  269. widget.connect(signal, on_<widget_name>_<signal>)"""
  270.  
  271. callback_handler_dict = dict_from_callback_obj(callback_obj)
  272.  
  273. for item in builder.widgets.items():
  274. (widget_name, widget) = item
  275. signal_ids = []
  276. try:
  277. widget_type = type(widget)
  278. while widget_type:
  279. signal_ids.extend(GObject.signal_list_ids(widget_type))
  280. widget_type = GObject.type_parent(widget_type)
  281. except RuntimeError: # pylint wants a specific error
  282. pass
  283. signal_names = [GObject.signal_name(sid) for sid in signal_ids]
  284.  
  285. # Now, automatically find any the user didn't specify in glade
  286. for sig in signal_names:
  287. # using convention suggested by glade
  288. sig = sig.replace("-", "_")
  289. handler_names = ["on_%s_%s" % (widget_name, sig)]
  290.  
  291. # Using the convention that the top level window is not
  292. # specified in the handler name. That is use
  293. # on_destroy() instead of on_windowname_destroy()
  294. if widget is callback_obj:
  295. handler_names.append("on_%s" % sig)
  296.  
  297. do_connect(item, sig, handler_names,
  298. callback_handler_dict, builder.connections)
  299.  
  300. log_unconnected_functions(callback_handler_dict, builder.connections)
  301.  
  302.  
  303. def do_connect(item, signal_name, handler_names,
  304. callback_handler_dict, connections):
  305. """connect this signal to an unused handler"""
  306. widget_name, widget = item
  307.  
  308. for handler_name in handler_names:
  309. target = handler_name in callback_handler_dict.keys()
  310. connection = (widget_name, signal_name, handler_name)
  311. duplicate = connection in connections
  312. if target and not duplicate:
  313. widget.connect(signal_name, callback_handler_dict[handler_name])
  314. connections.append(connection)
  315.  
  316. logging.getLogger(__name__).debug(
  317. "connect builder by name '%s','%s', '%s'",
  318. widget_name, signal_name, handler_name)
  319.  
  320.  
  321. def log_unconnected_functions(callback_handler_dict, connections):
  322. """log functions like on_* that we could not connect"""
  323.  
  324. connected_functions = [x[2] for x in connections]
  325.  
  326. handler_names = callback_handler_dict.keys()
  327. unconnected = [x for x in handler_names if x.startswith('on_')]
  328.  
  329. for handler_name in connected_functions:
  330. try:
  331. unconnected.remove(handler_name)
  332. except ValueError:
  333. pass
  334.  
  335. for handler_name in unconnected:
  336. logging.getLogger(__name__).debug(
  337. "Not connected to builder '%s'", handler_name)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement