254 lines
10 KiB
Python
254 lines
10 KiB
Python
|
#!/usr/bin/python
|
||
|
#
|
||
|
# Copyright (C) 2013 Red Hat, Inc.
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU Lesser General Public License as published
|
||
|
# by the Free Software Foundation; either version 2.1 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 warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU Lesser General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU Lesser General Public License
|
||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
#
|
||
|
# Author: David Shea <dshea@redhat.com>
|
||
|
|
||
|
import sys
|
||
|
import argparse
|
||
|
import re
|
||
|
import os.path
|
||
|
import copy
|
||
|
import collections
|
||
|
import locale
|
||
|
|
||
|
try:
|
||
|
from lxml import etree
|
||
|
except ImportError:
|
||
|
print("You need to install the python-lxml package to use check_accelerators.py")
|
||
|
sys.exit(1)
|
||
|
|
||
|
accel_re = re.compile(r'_(?P<accel>.)')
|
||
|
success = True
|
||
|
|
||
|
# Only used when --translate is requested.
|
||
|
class PODict(collections.Mapping):
|
||
|
def __init__(self, filename):
|
||
|
try:
|
||
|
import polib
|
||
|
except ImportError:
|
||
|
print("You need to install the python-polib package to check translations")
|
||
|
sys.exit(1)
|
||
|
|
||
|
self._dict = {}
|
||
|
|
||
|
pofile = polib.pofile(filename)
|
||
|
self.metadata = pofile.metadata
|
||
|
for entry in pofile.translated_entries():
|
||
|
self._dict[entry.msgid] = entry.msgstr
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self._dict[key]
|
||
|
|
||
|
def __iter__(self):
|
||
|
return self._dict.__iter__()
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self._dict)
|
||
|
|
||
|
def is_exception(node, language=None):
|
||
|
if language:
|
||
|
comment_str = "check_accelerators(%s)" % language
|
||
|
else:
|
||
|
comment_str = "check_accelerators"
|
||
|
|
||
|
return bool(node.xpath("./parent::*/comment()[contains(., '%s')]" % comment_str))
|
||
|
|
||
|
def add_check_accel(glade_filename, accels, label, po_map):
|
||
|
"""Check whether an accelerator conflicts with existing accelerators.
|
||
|
and add it to the current accelerator context.
|
||
|
"""
|
||
|
global success
|
||
|
|
||
|
language = None
|
||
|
if po_map:
|
||
|
if label.text not in po_map:
|
||
|
return
|
||
|
label.text = po_map[label.text]
|
||
|
language = po_map.metadata['Language']
|
||
|
|
||
|
match = accel_re.search(label.text)
|
||
|
if match:
|
||
|
accel = match.group('accel').lower()
|
||
|
if accel in accels:
|
||
|
# Check for an exception comment
|
||
|
if is_exception(label, language):
|
||
|
return
|
||
|
|
||
|
if language:
|
||
|
lang_str = " for language %s" % language
|
||
|
else:
|
||
|
lang_str = ""
|
||
|
|
||
|
print(("Accelerator collision for key %s in %s%s\n line %d: %s\n line %d: %s" %\
|
||
|
(accel, os.path.normpath(glade_filename), lang_str,
|
||
|
accels[accel].sourceline, accels[accel].text,
|
||
|
label.sourceline, label.text)).encode("utf-8"))
|
||
|
success = False
|
||
|
else:
|
||
|
accels[accel] = label
|
||
|
|
||
|
def combine_accels(glade_filename, list_a, list_b, po_map):
|
||
|
if not list_a:
|
||
|
return list_b
|
||
|
if not list_b:
|
||
|
return list_a
|
||
|
|
||
|
newlist = []
|
||
|
for accels_a in list_a:
|
||
|
for accels_b in list_b:
|
||
|
new_accels = copy.copy(accels_a)
|
||
|
for accel in accels_b.keys():
|
||
|
add_check_accel(glade_filename, new_accels, accels_b[accel], po_map)
|
||
|
newlist.append(new_accels)
|
||
|
return newlist
|
||
|
|
||
|
# GtkNotebook widgets define several child widgets, not all of which are active
|
||
|
# at the same time. To further complicate things, an object can have more than
|
||
|
# one GtkNotebook child, and a GtkNotebook can have GtkNotebook children.
|
||
|
#
|
||
|
# To handle this, GtkNotebook objects are processed separately.
|
||
|
# process_object returns a list of possible accelerator dictionaries, and each of
|
||
|
# these is compared against the list of accelerators returned for the object's
|
||
|
# other GtkNotebook children.
|
||
|
|
||
|
def process_object(glade_filename, interface_object, po_map):
|
||
|
"""Process keyboard shortcuts for a given glade object.
|
||
|
|
||
|
The return value from this function is a list of accelerator
|
||
|
dictionaries, with each consiting of accelerator shortcut characters
|
||
|
as keys and the corresponding <object> Element objects as values. Each
|
||
|
dictionary represents a set of accelerators that could be active at any
|
||
|
given time.
|
||
|
"""
|
||
|
# Start with an empty context for things that are always active
|
||
|
accels = [{}]
|
||
|
|
||
|
# Add everything that isn't a child of a GtkNotebook
|
||
|
for label in interface_object.xpath(".//property[@name='label' and ../property[@name='use_underline']/text() = 'True' and not(ancestor::object[@class='GtkNotebook'])]"):
|
||
|
add_check_accel(glade_filename, accels[0], label, po_map)
|
||
|
|
||
|
# For each GtkNotebook tab that is not a child of another notebook,
|
||
|
# add the tab to the top-level context
|
||
|
for notebook_label in interface_object.xpath(".//object[@class='GtkNotebook' and not(ancestor::object[@class='GtkNotebook'])]/child[@type='tab']//property[@name='label' and ../property[@name='use_underline']/text() = 'True']"):
|
||
|
add_check_accel(glade_filename, accels[0], notebook_label, po_map)
|
||
|
|
||
|
# Now process each non-tab object in each Gtknotebook that is not a child
|
||
|
# of another notebook. For each Gtk notebook, each non-tab child represents
|
||
|
# a separate potentially-active context. Since each list returned by
|
||
|
# process_object for a GtkNotebook child is independent of each other
|
||
|
# GtkNotebook child, we can just combine all of them into a single list.
|
||
|
# For example, say there's a notebook with two panes. The first pane
|
||
|
# contains another notebook with two panes. Let's call the main pane
|
||
|
# A, and the panes in the child GtkNotebook A_1 and A_2. A contains an
|
||
|
# accelerator for 'a', A_1 contains accelerators for 'b' and 'c', and A_2
|
||
|
# contains accelerators for 'b' and 'c'. The list returned would look like:
|
||
|
# [{'a': label1, 'b': label2, 'c': label3},
|
||
|
# {'a': label1, 'b': label4, 'c': label5}]
|
||
|
# Then when we process the other pane in the outermost Notebook (let's call
|
||
|
# it B), we find acclerators for 'a' and 'b':
|
||
|
# [{'a': label6, 'b': label7}].
|
||
|
# None of these dictionaries are active at the same time. Because
|
||
|
# process_object on A combined the accelerators that are in the top-level
|
||
|
# of A with each of the accelerators in the Notebook children of A, we can
|
||
|
# treat A as if it were actually two panes at the same-level of B and just
|
||
|
# create a list of three dictionaries for the whole notebook.
|
||
|
#
|
||
|
# A deepcopy of each object is taken so that the object can be treated as a
|
||
|
# separate XML document so that the ancestor axis stuff works.
|
||
|
|
||
|
for notebook in interface_object.xpath(".//object[@class='GtkNotebook' and not(ancestor::object[@class='GtkNotebook'])]"):
|
||
|
# Create the list of dictionaries for the notebook
|
||
|
notebook_list = []
|
||
|
for child in notebook.xpath("./child[not(@type='tab')]"):
|
||
|
notebook_list.extend(process_object(glade_filename, copy.deepcopy(child), po_map))
|
||
|
|
||
|
# Now combine this with our list of accelerators
|
||
|
accels = combine_accels(glade_filename, accels, notebook_list, po_map)
|
||
|
|
||
|
return accels
|
||
|
|
||
|
def check_glade(glade_filename, po_map=None):
|
||
|
with open(glade_filename) as glade_file:
|
||
|
# Parse the XML
|
||
|
glade_tree = etree.parse(glade_file)
|
||
|
|
||
|
# Treat each top-level object as a separate context
|
||
|
for interface_object in glade_tree.xpath("/interface/object"):
|
||
|
process_object(glade_filename, interface_object, po_map)
|
||
|
|
||
|
def main(argv=None):
|
||
|
if argv is None:
|
||
|
argv = sys.argv
|
||
|
|
||
|
parser = argparse.ArgumentParser("Check for duplicated accelerators")
|
||
|
parser.add_argument("-t", "--translate", action='store_true',
|
||
|
help="Check translated strings")
|
||
|
parser.add_argument("-p", "--podir", action='store', type=str,
|
||
|
metavar='PODIR', help='Directory containing po files', default='./po')
|
||
|
parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
|
||
|
help='The glade file to check')
|
||
|
args = parser.parse_args(args=argv)
|
||
|
|
||
|
# First check the untranslated strings in each file
|
||
|
for glade_file in args.glade_files:
|
||
|
check_glade(glade_file)
|
||
|
|
||
|
# Now loop over all of the translations
|
||
|
if args.translate:
|
||
|
import langtable
|
||
|
|
||
|
with open(os.path.join(args.podir, 'LINGUAS')) as linguas:
|
||
|
for line in linguas.readlines():
|
||
|
if re.match(r'^#', line):
|
||
|
continue
|
||
|
|
||
|
for lang in line.strip().split(" "):
|
||
|
# Reset the locale to C before parsing the po file because
|
||
|
# polib has erroneous uses of lower().
|
||
|
# See https://bitbucket.org/izi/polib/issue/54/pofile-parsing-crashes-in-turkish-locale
|
||
|
locale.setlocale(locale.LC_ALL, 'C')
|
||
|
po_map = PODict(os.path.join(args.podir, lang + ".po"))
|
||
|
|
||
|
# Set the locale so that we can use lower() on accelerator keys.
|
||
|
# If the language is of the form xx_XX, use that as the
|
||
|
# locale name. Otherwise use the first locale that
|
||
|
# langtable returns for the language. If that doesn't work,
|
||
|
# just use C and hope for the best.
|
||
|
if '_' in lang:
|
||
|
locale.setlocale(locale.LC_ALL, lang)
|
||
|
else:
|
||
|
locale_list = langtable.list_locales(languageId=lang)
|
||
|
if locale_list:
|
||
|
try:
|
||
|
locale.setlocale(locale.LC_ALL, locale_list[0])
|
||
|
except locale.Error:
|
||
|
print("No such locale %s, using C" % locale_list[0])
|
||
|
locale.setlocale(locale.LC_ALL, 'C')
|
||
|
else:
|
||
|
locale.setlocale(locale.LC_ALL, 'C')
|
||
|
|
||
|
for glade_file in args.glade_files:
|
||
|
check_glade(glade_file, po_map)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main(sys.argv[1:])
|
||
|
|
||
|
if success:
|
||
|
sys.exit(0)
|
||
|
else:
|
||
|
sys.exit(1)
|