2016-04-10 04:00:00 +00:00
#!/usr/bin/python3
2014-04-07 12:38:09 +00:00
#
# 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>
2016-04-10 04:00:00 +00:00
# Ignore any interruptible calls
# pylint: disable=interruptible-system-call
2014-04-07 12:38:09 +00:00
import sys
import argparse
import re
import os . path
import copy
import locale
2015-03-23 11:36:12 +00:00
# Import translation methods if needed
if ( ' -t ' in sys . argv ) or ( ' --translate ' in sys . argv ) :
try :
2016-04-10 04:00:00 +00:00
from pocketlint . translatepo import translate_all
2015-03-23 11:36:12 +00:00
except ImportError :
print ( " Unable to load po translation module " )
sys . exit ( 99 )
2014-04-07 12:38:09 +00:00
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
2015-03-23 11:36:12 +00:00
def is_exception ( node , conflicting_node , language = None ) :
# Check for a comment of the form
# <!-- check_accelerators: <conflicting-node-id> -->
# The node passed in is the label property of the widget rather than the
# <object> node itself, so we actually want the id attribute of the parent node.
for comment in node . xpath ( " ./preceding-sibling::comment()[contains(., ' check_accelerators: ' )] " ) :
if comment . text . split ( " : " ) [ 1 ] . strip ( ) == conflicting_node . getparent ( ) . attrib [ ' id ' ] :
return True
2014-04-07 12:38:09 +00:00
2015-03-23 11:36:12 +00:00
return False
2014-04-07 12:38:09 +00:00
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
if po_map :
2015-03-23 11:36:12 +00:00
try :
label_texts = po_map . get ( label . text , label . get ( " context " ) )
except KeyError :
2014-04-07 12:38:09 +00:00
return
2015-03-23 11:36:12 +00:00
# If there is more than one translation (i.e., a plural string), check
# that the accelerator is the same for all translations
if len ( label_texts ) > 1 :
match = accel_re . search ( label_texts [ 0 ] )
if match :
accel0 = match . group ( ' accel ' ) . lower ( )
else :
accel0 = None
for label_text in label_texts [ 1 : ] :
match = accel_re . search ( label_text )
if match :
accel = match . group ( ' accel ' ) . lower ( )
else :
accel = None
if accel != accel0 :
print ( " Mismatched accelerator in translations for %s in language %s " % \
( label . text , po_map . metadata [ ' Language ' ] ) )
success = False
label . text = label_texts [ 0 ]
lang_str = " for language %s " % po_map . metadata [ ' Language ' ]
else :
lang_str = " "
2014-04-07 12:38:09 +00:00
match = accel_re . search ( label . text )
if match :
accel = match . group ( ' accel ' ) . lower ( )
if accel in accels :
# Check for an exception comment
2015-03-23 11:36:12 +00:00
if is_exception ( label , accels [ accel ] ) :
2014-04-07 12:38:09 +00:00
return
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
2015-03-23 11:36:12 +00:00
else :
print ( ( " No accelerator defined for %s in %s %s : line %d " % \
( label . text , os . path . normpath ( glade_filename ) , lang_str ,
label . sourceline ) ) . encode ( " utf-8 " ) )
success = False
2014-04-07 12:38:09 +00:00
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
2015-03-23 11:36:12 +00:00
podicts = translate_all ( args . podir )
2014-04-07 12:38:09 +00:00
2015-03-23 11:36:12 +00:00
for ( lang , po_map ) in ( ( key , podicts [ key ] ) for key in podicts . keys ( ) ) :
# 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 :
2014-04-07 12:38:09 +00:00
locale . setlocale ( locale . LC_ALL , ' C ' )
2015-03-23 11:36:12 +00:00
for glade_file in args . glade_files :
check_glade ( glade_file , po_map )
2014-04-07 12:38:09 +00:00
if __name__ == " __main__ " :
main ( sys . argv [ 1 : ] )
if success :
sys . exit ( 0 )
else :
sys . exit ( 1 )