1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3
# This file is in the public domain
6
'''Enhances builder connections, provides object to access glade objects'''
8
from gi.repository import GObject, Gtk # pylint: disable=E0611
13
logger = logging.getLogger('xkcd_browser_lib')
15
from xml.etree.cElementTree import ElementTree
17
# this module is big so uses some conventional prefixes and postfixes
18
# *s list, except self.widgets is a dictionary
21
# ele_* element in a ElementTree
24
# pylint: disable=R0904
25
# the many public methods is a feature of Gtk.Builder
26
class Builder(Gtk.Builder):
28
connects glade defined handler to default_handler if necessary
29
auto connects widget to handler with matching name or alias
30
auto connects several widgets to a handler via multiple aliases
31
allow handlers to lookup widget name
32
logs every connection made, and any on_* not made
36
Gtk.Builder.__init__(self)
38
self.glade_handler_dict = {}
40
self._reverse_widget_dict = {}
42
# pylint: disable=R0201
43
# this is a method so that a subclass of Builder can redefine it
44
def default_handler(self,
45
handler_name, filename, *args, **kwargs):
46
'''helps the apprentice guru
48
glade defined handlers that do not exist come here instead.
49
An apprentice guru might wonder which signal does what he wants,
50
now he can define any likely candidates in glade and notice which
51
ones get triggered when he plays with the project.
52
this method does not appear in Gtk.Builder'''
53
logger.debug('''tried to call non-existent function:%s()
56
kwargs:%s''', handler_name, filename, args, kwargs)
57
# pylint: enable=R0201
59
def get_name(self, widget):
60
''' 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)
65
def add_from_file(self, filename):
66
'''parses xml file and stores wanted details'''
67
Gtk.Builder.add_from_file(self, filename)
69
# extract data for the extra interfaces
73
ele_widgets = tree.getiterator("object")
74
for ele_widget in ele_widgets:
75
name = ele_widget.attrib['id']
76
widget = self.get_object(name)
78
# populate indexes - a dictionary of widgets
79
self.widgets[name] = widget
81
# populate a reversed dictionary
82
self._reverse_widget_dict[widget] = name
84
# populate connections list
85
ele_signals = ele_widget.findall("signal")
89
ele_signal.attrib['name'],
90
ele_signal.attrib['handler']) for ele_signal in ele_signals]
93
self.connections.extend(connections)
95
ele_signals = tree.getiterator("signal")
96
for ele_signal in ele_signals:
97
self.glade_handler_dict.update(
98
{ele_signal.attrib["handler"]: None})
100
def connect_signals(self, callback_obj):
101
'''connect the handlers defined in glade
103
reports successful and failed connections
104
and logs call to missing handlers'''
105
filename = inspect.getfile(callback_obj.__class__)
106
callback_handler_dict = dict_from_callback_obj(callback_obj)
108
connection_dict.update(self.glade_handler_dict)
109
connection_dict.update(callback_handler_dict)
110
for item in connection_dict.items():
112
# the handler is missing so reroute to default_handler
113
handler = functools.partial(
114
self.default_handler, item[0], filename)
116
connection_dict[item[0]] = handler
118
# replace the run time warning
119
logger.warn("expected handler '%s' in %s",
122
# connect glade define handlers
123
Gtk.Builder.connect_signals(self, connection_dict)
125
# let's tell the user how we applied the glade design
126
for connection in self.connections:
127
widget_name, signal_name, handler_name = connection
128
logger.debug("connect builder by design '%s', '%s', '%s'",
129
widget_name, signal_name, handler_name)
131
def get_ui(self, callback_obj=None, by_name=True):
132
'''Creates the ui object with widgets as attributes
134
connects signals by 2 methods
135
this method does not appear in Gtk.Builder'''
137
result = UiFactory(self.widgets)
139
# Hook up any signals the user defined in glade
140
if callback_obj is not None:
141
# connect glade define handlers
142
self.connect_signals(callback_obj)
145
auto_connect_by_name(callback_obj, self)
150
# pylint: disable=R0903
151
# this class deliberately does not provide any public interfaces
152
# apart from the glade widgets
154
''' provides an object with attributes as glade widgets'''
155
def __init__(self, widget_dict):
156
self._widget_dict = widget_dict
157
for (widget_name, widget) in widget_dict.items():
158
setattr(self, widget_name, widget)
160
# Mangle any non-usable names (like with spaces or dashes)
162
cannot_message = """cannot bind ui.%s, name already exists
163
consider using a pythonic name instead of design name '%s'"""
164
consider_message = """consider using a pythonic name instead of design name '%s'"""
166
for (widget_name, widget) in widget_dict.items():
167
pyname = make_pyname(widget_name)
168
if pyname != widget_name:
169
if hasattr(self, pyname):
170
logger.debug(cannot_message, pyname, widget_name)
172
logger.debug(consider_message, widget_name)
173
setattr(self, pyname, widget)
176
'''Support 'for o in self' '''
177
return iter(widget_dict.values())
178
setattr(self, '__iter__', iterator)
180
def __getitem__(self, name):
181
'access as dictionary where name might be non-pythonic'
182
return self._widget_dict[name]
183
# pylint: enable=R0903
186
def make_pyname(name):
187
''' mangles non-pythonic names into pythonic ones'''
189
for character in name:
190
if (character.isalpha() or character == '_' or
191
(pyname and character.isdigit())):
198
# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
199
# need to reimplement inspect.getmembers. GObject introspection doesn't
201
def getmembers(obj, check):
205
attr = getattr(obj, k)
209
members.append((k, attr))
214
def dict_from_callback_obj(callback_obj):
215
'''a dictionary interface to callback_obj'''
216
methods = getmembers(callback_obj, inspect.ismethod)
218
aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
220
# a method may have several aliases
221
#~ @alias('on_btn_foo_clicked')
222
#~ @alias('on_tool_foo_activate')
223
#~ on_menu_foo_activate():
225
alias_groups = [(x.aliases, x) for x in aliased_methods]
228
for item in alias_groups:
229
for alias in item[0]:
230
aliases.append((alias, item[1]))
232
dict_methods = dict(methods)
233
dict_aliases = dict(aliases)
236
results.update(dict_methods)
237
results.update(dict_aliases)
242
def auto_connect_by_name(callback_obj, builder):
243
'''finds handlers like on_<widget_name>_<signal> and connects them
245
i.e. find widget,signal pair in builder and call
246
widget.connect(signal, on_<widget_name>_<signal>)'''
248
callback_handler_dict = dict_from_callback_obj(callback_obj)
250
for item in builder.widgets.items():
251
(widget_name, widget) = item
254
widget_type = type(widget)
256
signal_ids.extend(GObject.signal_list_ids(widget_type))
257
widget_type = GObject.type_parent(widget_type)
258
except RuntimeError: # pylint wants a specific error
260
signal_names = [GObject.signal_name(sid) for sid in signal_ids]
262
# Now, automatically find any the user didn't specify in glade
263
for sig in signal_names:
264
# using convention suggested by glade
265
sig = sig.replace("-", "_")
266
handler_names = ["on_%s_%s" % (widget_name, sig)]
268
# Using the convention that the top level window is not
269
# specified in the handler name. That is use
270
# on_destroy() instead of on_windowname_destroy()
271
if widget is callback_obj:
272
handler_names.append("on_%s" % sig)
274
do_connect(item, sig, handler_names,
275
callback_handler_dict, builder.connections)
277
log_unconnected_functions(callback_handler_dict, builder.connections)
280
def do_connect(item, signal_name, handler_names,
281
callback_handler_dict, connections):
282
'''connect this signal to an unused handler'''
283
widget_name, widget = item
285
for handler_name in handler_names:
286
target = handler_name in callback_handler_dict.keys()
287
connection = (widget_name, signal_name, handler_name)
288
duplicate = connection in connections
289
if target and not duplicate:
290
widget.connect(signal_name, callback_handler_dict[handler_name])
291
connections.append(connection)
293
logger.debug("connect builder by name '%s','%s', '%s'",
294
widget_name, signal_name, handler_name)
297
def log_unconnected_functions(callback_handler_dict, connections):
298
'''log functions like on_* that we could not connect'''
300
connected_functions = [x[2] for x in connections]
302
handler_names = callback_handler_dict.keys()
303
unconnected = [x for x in handler_names if x.startswith('on_')]
305
for handler_name in connected_functions:
307
unconnected.remove(handler_name)
311
for handler_name in unconnected:
312
logger.debug("Not connected to builder '%s'", handler_name)