337 lines
12 KiB
Python
337 lines
12 KiB
Python
#!/usr/bin/python3
|
|
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
|
# Mugshot - Lightweight user configuration utility
|
|
# Copyright (C) 2013-2019 Sean Davis <sean@bluesabre.org>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
|
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
|
# PURPOSE. See the GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
'''Enhances builder connections, provides object to access glade objects'''
|
|
|
|
import inspect
|
|
import functools
|
|
import logging
|
|
|
|
from xml.etree.cElementTree import ElementTree
|
|
|
|
import gi
|
|
gi.require_version('Gtk', '3.0')
|
|
from gi.repository import GObject, Gtk
|
|
|
|
|
|
logger = logging.getLogger('mugshot_lib')
|
|
|
|
|
|
# this module is big so uses some conventional prefixes and postfixes
|
|
# *s list, except self.widgets is a dictionary
|
|
# *_dict dictionary
|
|
# *name string
|
|
# ele_* element in a ElementTree
|
|
|
|
|
|
# pylint: disable=R0904
|
|
# the many public methods is a feature of Gtk.Builder
|
|
class Builder(Gtk.Builder):
|
|
|
|
''' extra features
|
|
connects glade defined handler to default_handler if necessary
|
|
auto connects widget to handler with matching name or alias
|
|
auto connects several widgets to a handler via multiple aliases
|
|
allow handlers to lookup widget name
|
|
logs every connection made, and any on_* not made
|
|
'''
|
|
|
|
def __init__(self):
|
|
"""Initialize the builder."""
|
|
Gtk.Builder.__init__(self)
|
|
self.widgets = {}
|
|
self.glade_handler_dict = {}
|
|
self.connections = []
|
|
self._reverse_widget_dict = {}
|
|
|
|
# pylint: disable=R0201
|
|
# this is a method so that a subclass of Builder can redefine it
|
|
def default_handler(self, handler_name, filename, *args, **kwargs):
|
|
'''helps the apprentice guru
|
|
|
|
glade defined handlers that do not exist come here instead.
|
|
An apprentice guru might wonder which signal does what he wants,
|
|
now he can define any likely candidates in glade and notice which
|
|
ones get triggered when he plays with the project.
|
|
this method does not appear in Gtk.Builder'''
|
|
logger.debug('''tried to call non-existent function:%s()
|
|
expected in %s
|
|
args:%s
|
|
kwargs:%s''', handler_name, filename, args, kwargs)
|
|
# pylint: enable=R0201
|
|
|
|
def get_name(self, widget):
|
|
''' allows a handler to get the name (id) of a widget
|
|
|
|
this method does not appear in Gtk.Builder'''
|
|
return self._reverse_widget_dict.get(widget)
|
|
|
|
def add_from_file(self, filename):
|
|
'''parses xml file and stores wanted details'''
|
|
Gtk.Builder.add_from_file(self, filename)
|
|
|
|
# extract data for the extra interfaces
|
|
tree = ElementTree()
|
|
tree.parse(filename)
|
|
|
|
ele_widgets = tree.getiterator("object")
|
|
for ele_widget in ele_widgets:
|
|
name = ele_widget.attrib['id']
|
|
widget = self.get_object(name)
|
|
|
|
# populate indexes - a dictionary of widgets
|
|
self.widgets[name] = widget
|
|
|
|
# populate a reversed dictionary
|
|
self._reverse_widget_dict[widget] = name
|
|
|
|
# populate connections list
|
|
ele_signals = ele_widget.findall("signal")
|
|
|
|
connections = [
|
|
(name,
|
|
ele_signal.attrib['name'],
|
|
ele_signal.attrib['handler']) for ele_signal in ele_signals]
|
|
|
|
if connections:
|
|
self.connections.extend(connections)
|
|
|
|
ele_signals = tree.getiterator("signal")
|
|
for ele_signal in ele_signals:
|
|
self.glade_handler_dict.update(
|
|
{ele_signal.attrib["handler"]: None})
|
|
|
|
def connect_signals(self, callback_obj):
|
|
'''connect the handlers defined in glade
|
|
|
|
reports successful and failed connections
|
|
and logs call to missing handlers'''
|
|
filename = inspect.getfile(callback_obj.__class__)
|
|
callback_handler_dict = dict_from_callback_obj(callback_obj)
|
|
connection_dict = {}
|
|
connection_dict.update(self.glade_handler_dict)
|
|
connection_dict.update(callback_handler_dict)
|
|
for item in list(connection_dict.items()):
|
|
if item[1] is None:
|
|
# the handler is missing so reroute to default_handler
|
|
handler = functools.partial(
|
|
self.default_handler, item[0], filename)
|
|
|
|
connection_dict[item[0]] = handler
|
|
|
|
# replace the run time warning
|
|
logger.warning("expected handler '%s' in %s",
|
|
item[0], filename)
|
|
|
|
# connect glade define handlers
|
|
Gtk.Builder.connect_signals(self, connection_dict)
|
|
|
|
# let's tell the user how we applied the glade design
|
|
for connection in self.connections:
|
|
widget_name, signal_name, handler_name = connection
|
|
logger.debug("connect builder by design '%s', '%s', '%s'",
|
|
widget_name, signal_name, handler_name)
|
|
|
|
def get_ui(self, callback_obj=None, by_name=True):
|
|
'''Creates the ui object with widgets as attributes
|
|
|
|
connects signals by 2 methods
|
|
this method does not appear in Gtk.Builder'''
|
|
|
|
result = UiFactory(self.widgets)
|
|
|
|
# Hook up any signals the user defined in glade
|
|
if callback_obj is not None:
|
|
# connect glade define handlers
|
|
self.connect_signals(callback_obj)
|
|
|
|
if by_name:
|
|
auto_connect_by_name(callback_obj, self)
|
|
|
|
return result
|
|
|
|
|
|
# pylint: disable=R0903
|
|
# this class deliberately does not provide any public interfaces
|
|
# apart from the glade widgets
|
|
class UiFactory():
|
|
|
|
''' provides an object with attributes as glade widgets'''
|
|
|
|
def __init__(self, widget_dict):
|
|
"""Initialize the UiFactory."""
|
|
self._widget_dict = widget_dict
|
|
for (widget_name, widget) in list(widget_dict.items()):
|
|
setattr(self, widget_name, widget)
|
|
|
|
# Mangle any non-usable names (like with spaces or dashes)
|
|
# into pythonic ones
|
|
cannot_message = """cannot bind ui.%s, name already exists
|
|
consider using a pythonic name instead of design name '%s'"""
|
|
consider_message = """consider using a pythonic name instead of
|
|
design name '%s'"""
|
|
|
|
for (widget_name, widget) in list(widget_dict.items()):
|
|
pyname = make_pyname(widget_name)
|
|
if pyname != widget_name:
|
|
if hasattr(self, pyname):
|
|
logger.debug(cannot_message, pyname, widget_name)
|
|
else:
|
|
logger.debug(consider_message, widget_name)
|
|
setattr(self, pyname, widget)
|
|
|
|
def iterator():
|
|
'''Support 'for o in self' '''
|
|
return iter(list(widget_dict.values()))
|
|
setattr(self, '__iter__', iterator)
|
|
|
|
def __getitem__(self, name):
|
|
'access as dictionary where name might be non-pythonic'
|
|
return self._widget_dict[name]
|
|
# pylint: enable=R0903
|
|
|
|
|
|
def make_pyname(name):
|
|
''' mangles non-pythonic names into pythonic ones'''
|
|
pyname = ''
|
|
for character in name:
|
|
if (character.isalpha() or character == '_' or
|
|
(pyname and character.isdigit())):
|
|
pyname += character
|
|
else:
|
|
pyname += '_'
|
|
return pyname
|
|
|
|
|
|
# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
|
|
# need to reimplement inspect.getmembers. GObject introspection doesn't
|
|
# play nice with it.
|
|
def getmembers(obj, check):
|
|
"""Reimplementation of getmembers"""
|
|
members = []
|
|
for k in dir(obj):
|
|
try:
|
|
attr = getattr(obj, k)
|
|
except:
|
|
continue
|
|
if check(attr):
|
|
members.append((k, attr))
|
|
members.sort()
|
|
return members
|
|
|
|
|
|
def dict_from_callback_obj(callback_obj):
|
|
'''a dictionary interface to callback_obj'''
|
|
methods = getmembers(callback_obj, inspect.ismethod)
|
|
|
|
aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
|
|
|
|
# a method may have several aliases
|
|
# ~ @alias('on_btn_foo_clicked')
|
|
# ~ @alias('on_tool_foo_activate')
|
|
# ~ on_menu_foo_activate():
|
|
# ~ pass
|
|
alias_groups = [(x.aliases, x) for x in aliased_methods]
|
|
|
|
aliases = []
|
|
for item in alias_groups:
|
|
for alias in item[0]:
|
|
aliases.append((alias, item[1]))
|
|
|
|
dict_methods = dict(methods)
|
|
dict_aliases = dict(aliases)
|
|
|
|
results = {}
|
|
results.update(dict_methods)
|
|
results.update(dict_aliases)
|
|
|
|
return results
|
|
|
|
|
|
def auto_connect_by_name(callback_obj, builder):
|
|
'''finds handlers like on_<widget_name>_<signal> and connects them
|
|
|
|
i.e. find widget,signal pair in builder and call
|
|
widget.connect(signal, on_<widget_name>_<signal>)'''
|
|
|
|
callback_handler_dict = dict_from_callback_obj(callback_obj)
|
|
|
|
for item in list(builder.widgets.items()):
|
|
(widget_name, widget) = item
|
|
signal_ids = []
|
|
try:
|
|
widget_type = type(widget)
|
|
while widget_type:
|
|
signal_ids.extend(GObject.signal_list_ids(widget_type))
|
|
widget_type = GObject.type_parent(widget_type)
|
|
except RuntimeError: # pylint wants a specific error
|
|
pass
|
|
signal_names = [GObject.signal_name(sid) for sid in signal_ids]
|
|
|
|
# Now, automatically find any the user didn't specify in glade
|
|
for sig in signal_names:
|
|
# using convention suggested by glade
|
|
sig = sig.replace("-", "_")
|
|
handler_names = ["on_%s_%s" % (widget_name, sig)]
|
|
|
|
# Using the convention that the top level window is not
|
|
# specified in the handler name. That is use
|
|
# on_destroy() instead of on_windowname_destroy()
|
|
if widget is callback_obj:
|
|
handler_names.append("on_%s" % sig)
|
|
|
|
do_connect(item, sig, handler_names,
|
|
callback_handler_dict, builder.connections)
|
|
|
|
log_unconnected_functions(callback_handler_dict, builder.connections)
|
|
|
|
|
|
def do_connect(item, signal_name, handler_names,
|
|
callback_handler_dict, connections):
|
|
'''connect this signal to an unused handler'''
|
|
widget_name, widget = item
|
|
|
|
for handler_name in handler_names:
|
|
target = handler_name in list(callback_handler_dict.keys())
|
|
connection = (widget_name, signal_name, handler_name)
|
|
duplicate = connection in connections
|
|
if target and not duplicate:
|
|
widget.connect(signal_name, callback_handler_dict[handler_name])
|
|
connections.append(connection)
|
|
|
|
logger.debug("connect builder by name '%s','%s', '%s'",
|
|
widget_name, signal_name, handler_name)
|
|
|
|
|
|
def log_unconnected_functions(callback_handler_dict, connections):
|
|
'''log functions like on_* that we could not connect'''
|
|
|
|
connected_functions = [x[2] for x in connections]
|
|
|
|
handler_names = list(callback_handler_dict.keys())
|
|
unconnected = [x for x in handler_names if x.startswith('on_')]
|
|
|
|
for handler_name in connected_functions:
|
|
try:
|
|
unconnected.remove(handler_name)
|
|
except ValueError:
|
|
pass
|
|
|
|
for handler_name in unconnected:
|
|
logger.debug("Not connected to builder '%s'", handler_name)
|