~apandada1/typhoon/quickly_trunk

« back to all changes in this revision

Viewing changes to typhoon_lib/Builder.py

  • Committer: Archisman Panigrahi
  • Date: 2014-05-17 16:43:45 UTC
  • Revision ID: apandada1@gmail.com-20140517164345-jren62tak20ng151
commit before release

Show diffs side-by-side

added added

removed removed

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