office-gobmx/bin/gla11y
Samuel Thibault 4c483204ed gla11y: Fix checking orphan labels
Change-Id: Icd43014f40a655b512b6328f4800906325d9b164
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/89767
Tested-by: Jenkins
Reviewed-by: Caolán McNamara <caolanm@redhat.com>
2020-03-01 16:42:14 +01:00

1402 lines
44 KiB
Python
Executable file

#!/usr/bin/env python
# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This file incorporates work covered by the following license notice:
#
# Copyright (c) 2018 Martin Pieuchot
# Copyright (c) 2018-2020 Samuel Thibault <sthibault@hypra.fr>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# Take LibreOffice (glade) .ui files and check for non accessible widgets
from __future__ import print_function
import os
import sys
import getopt
try:
import lxml.etree as ET
lxml = True
except ImportError:
if sys.version_info < (2,7):
print("gla11y needs lxml or python >= 2.7")
exit()
import xml.etree.ElementTree as ET
lxml = False
# Toplevel widgets
widgets_toplevel = [
'GtkWindow',
'GtkOffscreenWindow',
'GtkApplicationWindow',
'GtkDialog',
'GtkAboutDialog',
'GtkFileChooserDialog',
'GtkColorChooserDialog',
'GtkFontChooserDialog',
'GtkMessageDialog',
'GtkRecentChooserDialog',
'GtkAssistant',
'GtkAppChooserDialog',
'GtkPrintUnixDialog',
'GtkShortcutsWindow',
]
widgets_ignored = widgets_toplevel + [
# Containers
'GtkBox',
'GtkGrid',
'GtkNotebook',
'GtkFrame',
'GtkAspectFrame',
'GtkListBox',
'GtkFlowBox',
'GtkOverlay',
'GtkMenuBar',
'GtkToolbar',
'GtkToolpalette',
'GtkPaned',
'GtkHPaned',
'GtkVPaned',
'GtkButtonBox',
'GtkHButtonBox',
'GtkVButtonBox',
'GtkLayout',
'GtkFixed',
'GtkEventBox',
'GtkExpander',
'GtkViewport',
'GtkScrolledWindow',
'GtkAlignment',
'GtkRevealer',
'GtkSearchBar',
'GtkHeaderBar',
'GtkStack',
'GtkStackSwticher',
'GtkPopover',
'GtkPopoverMenu',
'GtkActionBar',
'GtkHandleBox',
'GtkShortcutsSection',
'GtkShortcutsGroup',
'GtkTable',
'GtkVBox',
'GtkHBox',
'GtkToolItem',
'GtkMenu',
# Invisible actions
'GtkSeparator',
'GtkHSeparator',
'GtkVSeparator',
'GtkAction',
'GtkToggleAction',
'GtkActionGroup',
'GtkCellRendererGraph',
'GtkCellRendererPixbuf',
'GtkCellRendererProgress',
'GtkCellRendererSpin',
'GtkCellRendererText',
'GtkCellRendererToggle',
'GtkSeparatorMenuItem',
'GtkSeparatorToolItem',
# Storage objects
'GtkListStore',
'GtkTreeStore',
'GtkTreeModelFilter',
'GtkTreeModelSort',
'GtkEntryBuffer',
'GtkTextBuffer',
'GtkTextTag',
'GtkTextTagTable',
'GtkSizeGroup',
'GtkWindowGroup',
'GtkAccelGroup',
'GtkAdjustment',
'GtkEntryCompletion',
'GtkIconFactory',
'GtkStatusIcon',
'GtkFileFilter',
'GtkRecentFilter',
'GtkRecentManager',
'GThemedIcon',
'GtkTreeSelection',
'GtkListBoxRow',
'GtkTreeViewColumn',
# Useless to label
'GtkScrollbar',
'GtkHScrollbar',
'GtkStatusbar',
'GtkInfoBar',
# These are actually labels
'GtkLinkButton',
# This precisely give a11y information :)
'AtkObject',
]
widgets_suffixignored = [
]
# These widgets always need a label
widgets_needlabel = [
'GtkEntry',
'GtkSearchEntry',
'GtkScale',
'GtkHScale',
'GtkVScale',
'GtkSpinButton',
'GtkSwitch',
]
# These widgets normally have their own label
widgets_buttons = [
'GtkButton',
'GtkToolButton',
'GtkToggleButton',
'GtkToggleToolButton',
'GtkRadioButton',
'GtkRadioToolButton',
'GtkCheckButton',
'GtkModelButton',
'GtkLockButton',
'GtkColorButton',
'GtkMenuButton',
'GtkMenuItem',
'GtkImageMenuItem',
'GtkMenuToolButton',
'GtkRadioMenuItem',
'GtkCheckMenuItem',
]
# These widgets are labels that can label other widgets
widgets_labels = [
'GtkLabel',
'GtkAccelLabel',
]
# The rest should probably be labelled if there are orphan labels
# GtkSpinner
# GtkProgressBar
# GtkLevelBar
# GtkComboBox
# GtkComboBoxText
# GtkFileChooserButton
# GtkAppChooserButton
# GtkFontButton
# GtkCalendar
# GtkColorChooserWidget
# GtkCellView
# GtkTreeView
# GtkTextView
# GtkIconView
# GtkImage
# GtkArrow
# GtkDrawingArea
# GtkScaleButton
# GtkVolumeButton
# TODO:
# GtkColorPlane ?
# GtkColorScale ?
# GtkColorSwatch ?
# GtkFileChooserWidget ?
# GtkFishbowl ?
# GtkFontChooserWidget ?
# GtkIcon ?
# GtkInspector* ?
# GtkMagnifier ?
# GtkPathBar ?
# GtkPlacesSidebar ?
# GtkPlacesView ?
# GtkPrinterOptionWidget ?
# GtkStackCombo ?
# GtkStackSidebar ?
# GtkStackSwitcher ?
progname = os.path.basename(sys.argv[0])
suppressions = {}
suppressions_to_line = {}
false_positives = {}
ids = {}
ids_dup = {}
labelled_by_elm = {}
label_for_elm = {}
mnemonic_for_elm = {}
gen_suppr = None
gen_supprfile = None
suppr_prefix = ""
outfile = None
pflag = False
warn_orphan_labels = True
errors = 0
errexists = 0
warnings = 0
warnexists = 0
fatals = 0
fatalexists = 0
enables = [ ]
dofatals = [ ]
#
# XML browsing and printing functions
#
def elm_parent(root, elm):
"""
Return the parent of the element.
"""
if lxml:
return elm.getparent()
else:
def find_parent(cur, elm):
for o in cur:
if o == elm:
return cur
parent = find_parent(o, elm)
if parent is not None:
return parent
return None
return find_parent(root, elm)
def step_elm(elm):
"""
Return the XML class path step corresponding to elm.
This can be empty if the elm does not have any class or id.
"""
step = elm.attrib.get('class')
if step is None:
step = ""
oid = elm.attrib.get('id')
if oid is not None:
oid = oid.encode('ascii','ignore').decode('ascii')
step += "[@id='%s']" % oid
if len(step) > 0:
step += '/'
return step
def find_elm(root, elm):
"""
Return the XML class path of the element from the given root.
This is the slow version used when getparent is not available.
"""
if root == elm:
return ""
for o in root:
path = find_elm(o, elm)
if path is not None:
step = step_elm(o)
return step + path
return None
def errpath(filename, tree, elm):
"""
Return the XML class path of the element
"""
if elm is None:
return ""
path = ""
if 'class' in elm.attrib:
path += elm.attrib['class']
oid = elm.attrib.get('id')
if oid is not None:
oid = oid.encode('ascii','ignore').decode('ascii')
path = "//" + path + "[@id='%s']" % oid
else:
if lxml:
elm = elm.getparent()
while elm is not None:
step = step_elm(elm)
path = step + path
elm = elm.getparent()
else:
path = find_elm(tree.getroot(), elm)[:-1]
path = filename + ':' + path
return path
#
# Warning/Error printing functions
#
def elm_prefix(filename, elm):
"""
Return the display prefix of the element
"""
if elm == None or not lxml:
return "%s:" % filename
else:
return "%s:%u" % (filename, elm.sourceline)
def elm_name(elm):
"""
Return a display name of the element
"""
if elm is not None:
name = ""
if 'class' in elm.attrib:
name = "'%s' " % elm.attrib['class']
if 'id' in elm.attrib:
id = elm.attrib['id'].encode('ascii','ignore').decode('ascii')
name += "'%s' " % id
if not name:
name = "'" + elm.tag + "'"
if lxml:
name += " line " + str(elm.sourceline)
return name
return ""
def elm_name_line(elm):
"""
Return a display name of the element with line number
"""
if elm is not None:
name = elm_name(elm)
if lxml and " line " not in name:
name += "line " + str(elm.sourceline) + " "
return name
return ""
def elm_line(elm):
"""
Return the line for the given element.
"""
if lxml:
return " line " + str(elm.sourceline)
else:
return ""
def elms_lines(elms):
"""
Return the list of lines for the given elements.
"""
if lxml:
return " lines " + ', '.join([str(l.sourceline) for l in elms])
else:
return ""
def elms_names_lines(elms):
"""
Return the list of names and lines for the given elements.
"""
return ', '.join([elm_name_line(elm) for elm in elms])
def elm_suppr(filename, tree, elm, msgtype, dogen):
"""
Return the prefix to be displayed to the user and the suppression line for
the warning type "msgtype" for element "elm"
"""
global gen_suppr, gen_supprfile, suppr_prefix, pflag
if suppressions or false_positives or gen_suppr is not None or pflag:
prefix = errpath(filename, tree, elm)
if prefix[0:len(suppr_prefix)] == suppr_prefix:
prefix = prefix[len(suppr_prefix):]
if suppressions or false_positives or gen_suppr is not None:
suppr = '%s %s' % (prefix, msgtype)
if gen_suppr is not None and msgtype is not None and dogen:
if gen_supprfile is None:
gen_supprfile = open(gen_suppr, 'w')
print(suppr, file=gen_supprfile)
else:
suppr = None
if not pflag:
# Use user-friendly line numbers
prefix = elm_prefix(filename, elm)
if prefix[0:len(suppr_prefix)] == suppr_prefix:
prefix = prefix[len(suppr_prefix):]
return (prefix, suppr)
def is_enabled(elm, msgtype, l, default):
"""
Test whether warning type msgtype is enabled for elm in l
"""
enabled = default
for (enable, thetype, klass) in l:
# Match warning type
if thetype is not None:
if thetype != msgtype:
continue
# Match elm class
if klass is not None and elm is not None:
if klass != elm.attrib.get('class'):
continue
enabled = enable
return enabled
def err(filename, tree, elm, msgtype, msg, error = True):
"""
Emit a warning or error for an element
"""
global errors, errexists, warnings, warnexists, fatals, fatalexists
# Let user tune whether a warning or error
fatal = is_enabled(elm, msgtype, dofatals, error)
# By default warnings and errors are enabled, but let user tune it
if not is_enabled(elm, msgtype, enables, True):
return
(prefix, suppr) = elm_suppr(filename, tree, elm, msgtype, True)
if suppr in false_positives:
# That was actually expected
return
if suppr in suppressions:
# Suppressed
suppressions[suppr] = False
if fatal:
fatalexists += 1
if error:
errexists += 1
else:
warnexists += 1
return
if error:
errors += 1
else:
warnings += 1
if fatal:
fatals += 1
msg = "%s %s%s: %s%s" % (prefix,
"FATAL " if fatal else "",
"ERROR" if error else "WARNING",
elm_name(elm), msg)
print(msg)
if outfile is not None:
print(msg, file=outfile)
def warn(filename, tree, elm, msgtype, msg):
"""
Emit a warning for an element
"""
err(filename, tree, elm, msgtype, msg, False)
#
# Labelling testing functions
#
def find_button_parent(root, elm):
"""
Find a parent which is a button
"""
if lxml:
parent = elm.getparent()
if parent is not None:
if parent.attrib.get('class') in widgets_buttons:
return parent
return find_button_parent(root, parent)
else:
def find_parent(cur, elm):
for o in cur:
if o == elm:
if cur.attrib.get('class') in widgets_buttons:
# we are the button, immediately above the target
return cur
else:
# we aren't the button, but target is over there
return True
parent = find_parent(o, elm)
if parent == True:
# It is over there, but didn't find a button yet
if cur.attrib.get('class') in widgets_buttons:
# we are the button
return cur
else:
return True
if parent is not None:
# we have the button parent over there
return parent
return None
parent = find_parent(root, elm)
if parent == True:
parent = None
return parent
def is_labelled_parent(elm):
"""
Return whether this element is a labelled parent
"""
klass = elm.attrib.get('class')
if klass in widgets_toplevel:
return True
if klass == 'GtkShortcutsGroup':
children = elm.findall("property[@name='title']")
if len(children) >= 1:
return True
if klass == 'GtkFrame' or klass == 'GtkNotebook':
children = elm.findall("child[@type='tab']") + elm.findall("child[@type='label']")
if len(children) >= 1:
return True
return False
def elm_labelled_parent(root, elm):
"""
Return the first labelled parent of the element, which can thus be used as
the root of widgets with common labelled context
"""
if lxml:
def find_labelled_parent(elm):
if is_labelled_parent(elm):
return elm
parent = elm.getparent()
if parent is None:
return None
return find_labelled_parent(parent)
parent = elm.getparent()
if parent is None:
return None
return find_labelled_parent(elm.getparent())
else:
def find_labelled_parent(cur, elm):
if cur == elm:
# the target element is over there
return True
for o in cur:
parent = find_labelled_parent(o, elm)
if parent == True:
# target element is over there, check ourself
if is_labelled_parent(cur):
# yes, and we are the first ancestor of the target element
return cur
else:
# no, but target element is over there.
return True
if parent != None:
# the first ancestor of the target element was over there
return parent
return None
parent = find_labelled_parent(root, elm)
if parent == True:
parent = None
return parent
def is_orphan_label(filename, tree, root, obj, orphan_root, doprint = False):
"""
Check whether this label has no accessibility relation, or doubtful relation
because another label labels the same target
"""
global label_for_elm, labelled_by_elm, mnemonic_for_elm, warnexists
# label-for
label_for = obj.findall("accessibility/relation[@type='label-for']")
for rel in label_for:
target = rel.attrib['target']
l = label_for_elm[target]
if len(l) > 1:
return True
# mnemonic_widget
mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \
obj.findall("property[@name='mnemonic-widget']")
for rel in mnemonic_for:
target = rel.text
l = mnemonic_for_elm[target]
if len(l) > 1:
return True
if len(label_for) > 0:
# At least one label-for, we are not orphan.
return False
if len(mnemonic_for) > 0:
# At least one mnemonic_widget, we are not orphan.
return False
labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
if len(labelled_by) > 0:
# Oh, a labelled label, probably not to be labelling anything
return False
# explicit role?
roles = [x.text for x in obj.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
roles += [x.attrib.get("type") for x in obj.findall("accessibility/role")]
if len(roles) > 1 and doprint:
err(filename, tree, obj, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
"%s" % elms_lines(children))
for role in roles:
if role == 'static' or role == 'ATK_ROLE_STATIC':
# This is static text, not meant to label anything
return False
parent = elm_parent(root, obj)
if parent is not None:
childtype = parent.attrib.get('type')
if childtype is None:
childtype = parent.attrib.get('internal-child')
if parent.tag == 'child' and childtype == 'label' \
or childtype == 'tab':
# This is a frame or a notebook label, not orphan.
return False
if find_button_parent(root, obj) is not None:
# This label is part of a button
return False
oid = obj.attrib.get('id')
if oid is not None:
if oid in labelled_by_elm:
# Some widget is labelled by us, we are not orphan.
# We should have had a label-for, will warn about it later.
return False
# No label-for, no mnemonic-for, no labelled-by, we are orphan.
(_, suppr) = elm_suppr(filename, tree, obj, "orphan-label", False)
if suppr in false_positives:
# That was actually expected
return False
if suppr in suppressions:
# Warning suppressed for this label
if suppressions[suppr]:
warnexists += 1
suppressions[suppr] = False
return False
if doprint:
context = elm_name(orphan_root)
if context:
context = " within " + context
warn(filename, tree, obj, "orphan-label", "does not specify what it labels" + context)
return True
def is_orphan_widget(filename, tree, root, obj, orphan, orphan_root, doprint = False):
"""
Check whether this widget has no accessibility relation.
"""
global warnexists
if obj.tag != 'object':
return False
oid = obj.attrib.get('id')
klass = obj.attrib.get('class')
# "Don't care" special case
if klass in widgets_ignored:
return False
for suffix in widgets_suffixignored:
if klass[-len(suffix):] == suffix:
return False
# Widgets usual do not strictly require a label, i.e. a labelled parent
# is enough for context, but some do always need one.
requires_label = klass in widgets_needlabel
labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
# Labels special case
if klass in widgets_labels:
return False
# Case 1: has an explicit <child internal-child="accessible"> sub-element
children = obj.findall("child[@internal-child='accessible']")
if len(children) > 1 and doprint:
err(filename, tree, obj, "multiple-accessible", "has multiple <child internal-child='accessible'>"
"%s" % elms_lines(children))
if len(children) >= 1:
return False
# Case 2: has an <accessibility> sub-element with a "labelled-by"
# <relation> pointing to an existing element.
if len(labelled_by) > 0:
return False
# Case 3: has a label-for
if oid in label_for_elm:
return False
# Case 4: has a mnemonic
if oid in mnemonic_for_elm:
return False
# Case 5: Has a <property name="tooltip_text">
tooltips = obj.findall("property[@name='tooltip_text']") + \
obj.findall("property[@name='tooltip-text']")
if len(tooltips) > 1 and doprint:
err(filename, tree, obj, "multiple-tooltip", "has multiple tooltip_text properties")
if len(tooltips) >= 1 and klass != 'GtkCheckButton':
return False
# Case 6: Has a <property name="placeholder_text">
placeholders = obj.findall("property[@name='placeholder_text']") + \
obj.findall("property[@name='placeholder-text']")
if len(placeholders) > 1 and doprint:
err(filename, tree, obj, "multiple-placeholder", "has multiple placeholder_text properties")
if len(placeholders) >= 1:
return False
# Buttons usually don't need an external label, their own is enough, (but they do need one)
if klass in widgets_buttons:
labels = obj.findall("property[@name='label']")
if len(labels) > 1 and doprint:
err(filename, tree, obj, "multiple-label", "has multiple label properties")
if len(labels) >= 1:
# Has a <property name="label">
return False
actions = obj.findall("property[@name='action_name']")
if len(actions) > 1 and doprint:
err(filename, tree, obj, "multiple-action_name", "has multiple action_name properties")
if len(actions) >= 1:
# Has a <property name="action_name">
return False
# Uses id as an action_name
if 'id' in obj.attrib:
if obj.attrib['id'].startswith(".uno:"):
return False
gtklabels = obj.findall(".//object[@class='GtkLabel']") + obj.findall(".//object[@class='GtkAccelLabel']")
if len(gtklabels) >= 1:
# Has a custom label
return False
# no label for a button, warn
if doprint:
warn(filename, tree, obj, "button-no-label", "does not have its own label");
if not is_enabled(obj, "button-no-label", enables, True):
# Warnings disabled
return False
(_, suppr) = elm_suppr(filename, tree, obj, "button-no-label", False)
if suppr in false_positives:
# That was actually expected
return False
if suppr in suppressions:
# Warning suppressed for this widget
if suppressions[suppr]:
warnexists += 1
suppressions[suppr] = False
return False
return True
# GtkImages special case
if klass == "GtkImage":
uses = [u for u in tree.iterfind(".//object/property[@name='image']") if u.text == oid]
if len(uses) > 0:
# This image is just used by another element, don't warn
# about the image itself, we probably want the warning on
# the element instead.
return False
if find_button_parent(root, obj) is not None:
# This image is part of a button, we want the warning on the button
# instead, if any.
return False
# GtkEntry special case
if klass == 'GtkEntry' or klass == 'GtkSearchEntry':
parent = elm_parent(root, obj)
if parent is not None:
if parent.tag == 'child' and \
parent.attrib.get('internal-child') == "entry":
# This is an internal entry of another widget. Relations
# will be handled by that widget.
return False
# GtkShortcutsShortcut special case
if klass == 'GtkShortcutsShortcut':
children = obj.findall("property[@name='title']")
if len(children) >= 1:
return False
# Really no label, perhaps emit a warning
if not is_enabled(obj, "no-labelled-by", enables, True):
# Warnings disabled for this class of widgets
return False
(_, suppr) = elm_suppr(filename, tree, obj, "no-labelled-by", False)
if suppr in false_positives:
# That was actually expected
return False
if suppr in suppressions:
# Warning suppressed for this widget
if suppressions[suppr]:
warnexists += 1
suppressions[suppr] = False
return False
if not orphan:
# No orphan label, so probably the labelled parent provides enough
# context.
if requires_label:
# But these always need a label.
if doprint:
warn(filename, tree, obj, "no-labelled-by", "has no accessibility label")
return True
return False
if doprint:
context = elm_name(orphan_root)
if context:
context = " within " + context
warn(filename, tree, obj, "no-labelled-by", "has no accessibility label while there are orphan labels" + context)
return True
def orphan_items(filename, tree, root, elm):
"""
Check whether from some element there exists orphan labels and orphan widgets
"""
orphan_labels = False
orphan_widgets = False
if elm.attrib.get('class') in widgets_labels:
orphan_labels = is_orphan_label(filename, tree, root, elm, None)
else:
orphan_widgets = is_orphan_widget(filename, tree, root, elm, True, None)
for obj in elm:
# We are not interested in orphan labels under another labelled
# parent. This also allows to keep linear complexity.
if not is_labelled_parent(obj):
label, widget = orphan_items(filename, tree, root, obj)
if label:
orphan_labels = True
if widget:
orphan_widgets = True
if orphan_labels and orphan_widgets:
# No need to look up more
break
return orphan_labels, orphan_widgets
#
# UI accessibility checks
#
def check_props(filename, tree, root, elm, forward):
"""
Check the given list of relation properties
"""
props = elm.findall("property[@name='" + forward + "']")
for prop in props:
if prop.text not in ids:
err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % prop.text)
return props
def is_visible(obj):
visible = False
visible_prop = obj.findall("property[@name='visible']")
visible_len = len(visible_prop)
if visible_len:
visible_txt = visible_prop[visible_len - 1].text
if visible_txt.lower() == "true":
visible = True
elif visible_txt.lower() == "false":
visible = False
return visible
def check_rels(filename, tree, root, elm, forward, backward = None):
"""
Check the relations given by forward
"""
oid = elm.attrib.get('id')
rels = elm.findall("accessibility/relation[@type='" + forward + "']")
for rel in rels:
target = rel.attrib['target']
if target not in ids:
err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % target)
elif backward is not None:
widget = ids[target]
backrels = widget.findall("accessibility/relation[@type='" + backward + "']")
if len([x for x in backrels if x.attrib['target'] == oid]) == 0:
err(filename, tree, elm, "missing-" + backward, "has " + forward + \
", but is not " + backward + " by " + elm_name_line(widget))
return rels
def check_a11y_relation(filename, tree):
"""
Emit an error message if any of the 'object' elements of the XML
document represented by `root' doesn't comply with Accessibility
rules.
"""
global widgets_ignored, ids, label_for_elm, labelled_by_elm, mnemonic_for_elm
def check_elm(orphan_root, obj, orphan_labels, orphan_widgets):
"""
Check one element, knowing that orphan_labels/widgets tell whether
there are orphan labels and widgets within orphan_root
"""
oid = obj.attrib.get('id')
klass = obj.attrib.get('class')
# "Don't care" special case
if klass in widgets_ignored:
return
for suffix in widgets_suffixignored:
if klass[-len(suffix):] == suffix:
return
# Widgets usual do not strictly require a label, i.e. a labelled parent
# is enough for context, but some do always need one.
requires_label = klass in widgets_needlabel
if oid is not None:
# Check that ids are unique
if oid in ids_dup:
if ids[oid] == obj:
# We are the first, warn
duplicates = tree.findall(".//object[@id='" + oid + "']")
err(filename, tree, obj, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates))
# Check label-for and their dual labelled-by
label_for = check_rels(filename, tree, root, obj, "label-for", "labelled-by")
# Check labelled-by and its dual label-for
labelled_by = check_rels(filename, tree, root, obj, "labelled-by", "label-for")
visible = is_visible(obj)
# Should have only one label
if len(labelled_by) >= 1:
if oid in mnemonic_for_elm:
warn(filename, tree, obj, "labelled-by-and-mnemonic",
"has both a mnemonic " + elm_name_line(mnemonic_for_elm[oid][0]) + "and labelled-by relation")
if len(labelled_by) > 1:
warn(filename, tree, obj, "multiple-labelled-by", "has multiple labelled-by relations")
if oid in label_for_elm:
if len(label_for_elm[oid]) > 1:
warn(filename, tree, obj, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm[oid]))
elif len(label_for_elm[oid]) == 1:
paired = label_for_elm[oid][0]
if visible != is_visible(paired):
warn(filename, tree, obj, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired))
if oid in mnemonic_for_elm:
if len(mnemonic_for_elm[oid]) > 1:
warn(filename, tree, obj, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm[oid]))
# Check member-of
member_of = check_rels(filename, tree, root, obj, "member-of")
# Labels special case
if klass in widgets_labels:
properties = check_props(filename, tree, root, obj, "mnemonic_widget") + \
check_props(filename, tree, root, obj, "mnemonic-widget")
if len(properties) > 1:
err(filename, tree, obj, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
"%s" % elms_lines(properties))
# Emit orphaning warnings
if warn_orphan_labels or orphan_widgets:
is_orphan_label(filename, tree, root, obj, orphan_root, True)
# We are done with the label
return
# Not a label, will perhaps need one
# Emit orphaning warnings
is_orphan_widget(filename, tree, root, obj, orphan_labels, orphan_root, True)
root = tree.getroot()
# Flush ids and relations from previous files
ids = {}
ids_dup = {}
labelled_by_elm = {}
label_for_elm = {}
mnemonic_for_elm = {}
# First pass to get links into hash tables, no warning, just record duplicates
for obj in root.iter('object'):
oid = obj.attrib.get('id')
if oid is not None:
if oid not in ids:
ids[oid] = obj
else:
ids_dup[oid] = True
labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
for rel in labelled_by:
target = rel.attrib.get('target')
if target is not None:
if target not in labelled_by_elm:
labelled_by_elm[target] = [ obj ]
else:
labelled_by_elm[target].append(obj)
label_for = obj.findall("accessibility/relation[@type='label-for']")
for rel in label_for:
target = rel.attrib.get('target')
if target is not None:
if target not in label_for_elm:
label_for_elm[target] = [ obj ]
else:
label_for_elm[target].append(obj)
mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \
obj.findall("property[@name='mnemonic-widget']")
for rel in mnemonic_for:
target = rel.text
if target is not None:
if target not in mnemonic_for_elm:
mnemonic_for_elm[target] = [ obj ]
else:
mnemonic_for_elm[target].append(obj)
# Second pass, recursive depth-first, to be able to efficiently know whether
# there are orphan labels within a part of the tree.
def recurse(orphan_root, obj, orphan_labels, orphan_widgets):
if obj == root or is_labelled_parent(obj):
orphan_root = obj
orphan_labels, orphan_widgets = orphan_items(filename, tree, root, obj)
if obj.tag == 'object':
check_elm(orphan_root, obj, orphan_labels, orphan_widgets)
for o in obj:
recurse(orphan_root, o, orphan_labels, orphan_widgets)
recurse(root, root, False, False)
#
# Main
#
def usage(fatal = True):
print("`%s' checks accessibility of glade .ui files" % progname)
print("")
print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname)
print("")
print(" -p Print XML class path instead of line number")
print(" -g Generate suppression file SUPPR_FILE")
print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
print(" -f Suppress warnings given by file SUPPR_FILE completely")
print(" -P Remove PREFIX from file names in warnings")
print(" -o Also prints errors and warnings to given file")
print("")
print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
print(" Give or extend one of the lists of widget classes, where FOO can be:")
print(" - toplevel : widgets to be considered toplevel windows")
print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
print(" - suffixignored : suffixes of widget classes which do not need labelling")
print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
print(" - buttons : widgets which need their own label but not more")
print(" (e.g. GtkButton)")
print(" - labels : widgets which provide labels (e.g. GtkLabel)")
print(" --widgets-print print default widgets lists")
print("")
print(" --enable-all enable all warnings/dofatals (default)")
print(" --disable-all disable all warnings/dofatals")
print(" --fatal-all make all warnings dofatals")
print(" --not-fatal-all do not make all warnings dofatals (default)")
print("")
print(" --enable-type=TYPE enable warning/fatal type TYPE")
print(" --disable-type=TYPE disable warning/fatal type TYPE")
print(" --fatal-type=TYPE make warning type TYPE a fatal")
print(" --not-fatal-type=TYPE make warning type TYPE not a fatal")
print("")
print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
print(" --fatal-widgets=CLASS make warning type CLASS a fatal")
print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal")
print("")
print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
print(" class CLASS")
print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
print(" class CLASS")
print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget")
print(" class CLASS")
print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget")
print(" class CLASS")
print("")
print(" --disable-orphan-labels only warn about orphan labels when there are")
print(" orphan widgets in the same context")
print("")
print("Report bugs to <bugs@hypra.fr>")
sys.exit(2 if fatal else 0)
def widgets_opt(widgets_list, arg):
"""
Replace or extend `widgets_list' with the list of classes contained in `arg'
"""
append = arg and arg[0] == '+'
if append:
arg = arg[1:]
if arg:
widgets = arg.split(',')
else:
widgets = []
if not append:
del widgets_list[:]
widgets_list.extend(widgets)
def main():
global pflag, gen_suppr, gen_supprfile, suppressions, suppr_prefix, false_positives, dofatals, enables, dofatals, warn_orphan_labels
global widgets_toplevel, widgets_ignored, widgets_suffixignored, widgets_needlabel, widgets_buttons, widgets_labels
global outfile
try:
opts, args = getopt.getopt(sys.argv[1:], "hpiIg:s:f:P:o:L:", [
"help",
"version",
"widgets-toplevel=",
"widgets-ignored=",
"widgets-suffixignored=",
"widgets-needlabel=",
"widgets-buttons=",
"widgets-labels=",
"widgets-print",
"enable-all",
"disable-all",
"fatal-all",
"not-fatal-all",
"enable-type=",
"disable-type=",
"fatal-type=",
"not-fatal-type=",
"enable-widgets=",
"disable-widgets=",
"fatal-widgets=",
"not-fatal-widgets=",
"enable-specific=",
"disable-specific=",
"fatal-specific=",
"not-fatal-specific=",
"disable-orphan-labels",
] )
except getopt.GetoptError:
usage()
suppr = None
false = None
out = None
filelist = None
for o, a in opts:
if o == "--help" or o == "-h":
usage(False)
if o == "--version":
print("0.1")
sys.exit(0)
elif o == "-p":
pflag = True
elif o == "-g":
gen_suppr = a
elif o == "-s":
suppr = a
elif o == "-f":
false = a
elif o == "-P":
suppr_prefix = a
elif o == "-o":
out = a
elif o == "-L":
filelist = a
elif o == "--widgets-toplevel":
widgets_opt(widgets_toplevel, a)
elif o == "--widgets-ignored":
widgets_opt(widgets_ignored, a)
elif o == "--widgets-suffixignored":
widgets_opt(widgets_suffixignored, a)
elif o == "--widgets-needlabel":
widgets_opt(widgets_needlabel, a)
elif o == "--widgets-buttons":
widgets_opt(widgets_buttons, a)
elif o == "--widgets-labels":
widgets_opt(widgets_labels, a)
elif o == "--widgets-print":
print("--widgets-toplevel '" + ','.join(widgets_toplevel) + "'")
print("--widgets-ignored '" + ','.join(widgets_ignored) + "'")
print("--widgets-suffixignored '" + ','.join(widgets_suffixignored) + "'")
print("--widgets-needlabel '" + ','.join(widgets_needlabel) + "'")
print("--widgets-buttons '" + ','.join(widgets_buttons) + "'")
print("--widgets-labels '" + ','.join(widgets_labels) + "'")
sys.exit(0)
elif o == '--enable-all':
enables.append( (True, None, None) )
elif o == '--disable-all':
enables.append( (False, None, None) )
elif o == '--fatal-all':
dofatals.append( (True, None, None) )
elif o == '--not-fatal-all':
dofatals.append( (False, None, None) )
elif o == '--enable-type':
enables.append( (True, a, None) )
elif o == '--disable-type':
enables.append( (False, a, None) )
elif o == '--fatal-type':
dofatals.append( (True, a, None) )
elif o == '--not-fatal-type':
dofatals.append( (False, a, None) )
elif o == '--enable-widgets':
enables.append( (True, None, a) )
elif o == '--disable-widgets':
enables.append( (False, None, a) )
elif o == '--fatal-widgets':
dofatals.append( (True, None, a) )
elif o == '--not-fatal-widgets':
dofatals.append( (False, None, a) )
elif o == '--enable-specific':
(thetype, klass) = a.split('.', 1)
enables.append( (True, thetype, klass) )
elif o == '--disable-specific':
(thetype, klass) = a.split('.', 1)
enables.append( (False, thetype, klass) )
elif o == '--fatal-specific':
(thetype, klass) = a.split('.', 1)
dofatals.append( (True, thetype, klass) )
elif o == '--not-fatal-specific':
(thetype, klass) = a.split('.', 1)
dofatals.append( (False, thetype, klass) )
elif o == '--disable-orphan-labels':
warn_orphan_labels = False
# Read suppression file before overwriting it
if suppr is not None:
try:
supprfile = open(suppr, 'r')
line_no = 1;
for line in supprfile.readlines():
prefix = line.rstrip()
suppressions[prefix] = True
suppressions_to_line[prefix] = line_no
line_no = line_no + 1;
supprfile.close()
except IOError:
pass
# Read false positives file
if false is not None:
try:
falsefile = open(false, 'r')
for line in falsefile.readlines():
prefix = line.rstrip()
false_positives[prefix] = True
falsefile.close()
except IOError:
pass
if out is not None:
outfile = open(out, 'w')
if filelist is not None:
try:
filelistfile = open(filelist, 'r')
for line in filelistfile.readlines():
line = line.strip()
if line:
args += line.split(' ')
filelistfile.close()
except IOError:
err(filelist, None, None, "unable to read file list file")
for filename in args:
try:
tree = ET.parse(filename)
except ET.ParseError:
err(filename, None, None, "parse", "malformatted xml file")
continue
except IOError:
err(filename, None, None, None, "unable to read file")
continue
try:
check_a11y_relation(filename, tree)
except Exception as error:
import traceback
traceback.print_exc()
err(filename, None, None, "parse", "error parsing file")
if errors > 0 or errexists > 0:
estr = "%s new error%s" % (errors, 's' if errors > 1 else '')
if errexists > 0:
estr += " (%s suppressed by %s)" % (errexists, suppr)
print(estr)
if warnings > 0 or warnexists > 0:
wstr = "%s new warning%s" % (warnings, 's' if warnings > 1 else '')
if warnexists > 0:
wstr += " (%s suppressed by %s)" % (warnexists, suppr)
print(wstr)
if fatals > 0 or fatalexists > 0:
wstr = "%s new fatal%s" % (fatals, 's' if fatals > 1 else '')
if fatalexists > 0:
wstr += " (%s suppressed by %s)" % (fatalexists, suppr)
print(wstr)
n = 0
for (suppr,unused) in suppressions.items():
if unused:
n += 1
if n > 0:
print("%s suppression%s unused:" % (n, 's' if n > 1 else ''))
for (suppr,unused) in suppressions.items():
if unused:
print(" %s:%s" % (suppressions_to_line[suppr], suppr))
if gen_supprfile is not None:
gen_supprfile.close()
if outfile is not None:
outfile.close()
if fatals > 0 and gen_suppr is None:
print("Explanations are available on https://wiki.documentfoundation.org/Development/Accessibility")
sys.exit(1)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass
# vim: set shiftwidth=4 softtabstop=4 expandtab: