summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/gla11y216
-rw-r--r--config_host.mk.in1
-rw-r--r--configure.ac10
-rw-r--r--solenv/gbuild/TargetLocations.mk2
-rw-r--r--solenv/gbuild/UIConfig.mk26
5 files changed, 254 insertions, 1 deletions
diff --git a/bin/gla11y b/bin/gla11y
new file mode 100755
index 000000000000..d0619133ad0f
--- /dev/null
+++ b/bin/gla11y
@@ -0,0 +1,216 @@
+#!/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 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.
+#
+# vim: set shiftwidth=4 softtabstop=4 expandtab:
+
+# Take LibreOffice (glade) .ui files and check for non accessible widgets
+
+from __future__ import print_function
+
+import os
+import sys
+import getopt
+import lxml.etree as ET
+
+progname = os.path.basename(sys.argv[0])
+Werror = False
+Wnone = False
+errors = 0
+warnings = 0
+
+
+def errstr(elm):
+ """
+ Print the line number of the element
+ """
+
+ return str(elm.sourceline)
+
+def err(filename, elm, msg):
+ global errors
+
+ if elm == None:
+ prefix = "%s:" % filename
+ else:
+ prefix = "%s:%s" % (filename, errstr(elm))
+
+ errors += 1
+ msg = "%s ERROR: %s" % (prefix, msg)
+ print(msg.encode('ascii', 'ignore'))
+
+
+def warn(filename, elm, msg):
+ global Werror, Wnone, errors, warnings
+
+ if Wnone:
+ return
+
+ prefix = "%s:%s" % (filename, errstr(elm))
+
+ if Werror:
+ errors += 1
+ else:
+ warnings += 1
+
+ msg = "%s WARNING: %s" % (prefix, msg)
+ print(msg.encode('ascii', 'ignore'))
+
+
+def check_objects(filename, elm, objects, target):
+ """
+ Check that objects contains exactly one object
+ """
+ length = len(list(objects))
+ if length == 0:
+ err(filename, elm, "use of undeclared target '%s'" % target)
+ elif length > 1:
+ err(filename, elm, "sevral targets are named '%s'" % target)
+
+def check_props(filename, root, props):
+ """
+ Check the given list of relation properties
+ """
+ for prop in props:
+ objects = root.iterfind(".//object[@id='%s']" % prop.text)
+ check_objects(filename, prop, objects, prop.text)
+
+def check_rels(filename, root, rels):
+ """
+ Check the given list of relations
+ """
+ for rel in rels:
+ target = rel.attrib['target']
+ targets = root.iterfind(".//object[@id='%s']" % target)
+ check_objects(filename, rel, targets, target)
+
+def check_a11y_relation(filename, root):
+ """
+ Emit an error message if any of the 'object' elements of the XML
+ document represented by `root' doesn't comply with Accessibility
+ rules.
+ """
+
+ for obj in root.iter('object'):
+
+ label_for = obj.findall("accessibility/relation[@type='label-for']")
+ check_rels(filename, root, label_for)
+
+ labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
+ check_rels(filename, root, labelled_by)
+
+ member_of = obj.findall("accessibility/relation[@type='member-of']")
+ check_rels(filename, root, member_of)
+
+ if obj.attrib['class'] == 'GtkLabel':
+ # Case 0: A 'GtkLabel' must contain one or more "label-for"
+ # pointing to existing elements or...
+ if len(label_for) > 0:
+ continue
+
+ # ...a single "mnemonic_widget"
+ properties = obj.findall("property[@name='mnemonic_widget']")
+ check_props(filename, root, properties)
+ if len(properties) > 1:
+ # It does not make sense for a label to be a mnemonic for
+ # several actions.
+ lines = ', '.join([str(p.sourceline) for p in properties])
+ err(filename, obj, "too many sub-elements"
+ ", expected single <property name='mnemonic_widgets'>"
+ ": lines %s" % lines)
+ continue
+ if len(properties) == 1:
+ continue
+ # TODO: warn that it is a label for nothing
+ continue
+
+ # Case 1: has a <child internal-child="accessible"> sub-element
+ children = obj.findall("child[@internal-child='accessible']")
+ if children:
+ if len(children) > 1:
+ lines = ', '.join([str(c.sourceline) for c in children])
+ err(filename, obj, "too many sub-elements"
+ ", expected single <child internal-child='accessible'>"
+ ": lines %s" % lines)
+ continue
+
+ # Case 2: has an <accessibility> sub-element with a "labelled-by"
+ # <relation> pointing to an existing element.
+ if len(labelled_by) > 0:
+ continue
+
+ # TODO: after a few more checks and false-positives filtering, warn
+ # that this does not have a label
+
+
+def usage():
+ print("%s [-W error|none] [file ...]" % progname,
+ file=sys.stderr)
+ sys.exit(2)
+
+
+def main():
+ global Werror, Wnone, errors
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "pW:")
+ except getopt.GetoptError:
+ usage()
+
+ for o, a in opts:
+ if o == "-W":
+ if a == "error":
+ Werror = True
+ elif a == "none":
+ Wnone = True
+
+ if not args:
+ sys.exit("%s: no input files" % progname)
+
+ for filename in args:
+ try:
+ tree = ET.parse(filename)
+ except ET.ParseError:
+ err(filename, None, "malformatted xml file")
+ except IOError:
+ err(filename, None, "unable to read file")
+
+ try:
+ check_a11y_relation(filename, tree.getroot())
+ except Exception as error:
+ import traceback
+ traceback.print_exc()
+ err(filename, None, "error parsing file")
+
+
+ if errors > 0:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ pass
diff --git a/config_host.mk.in b/config_host.mk.in
index 8ce71275ca5c..f4d7d67a6601 100644
--- a/config_host.mk.in
+++ b/config_host.mk.in
@@ -460,6 +460,7 @@ export PTHREAD_LIBS=@PTHREAD_LIBS@
export PYTHON_CFLAGS=$(gb_SPACE)@PYTHON_CFLAGS@
export PYTHON_FOR_BUILD=@PYTHON_FOR_BUILD@
export PYTHON_LIBS=$(gb_SPACE)@PYTHON_LIBS@
+export PYTHON_LXML=@PYTHON_LXML@
export PYTHON_VERSION=@PYTHON_VERSION@
export PYTHON_VERSION_MAJOR=@PYTHON_VERSION_MAJOR@
export PYTHON_VERSION_MINOR=@PYTHON_VERSION_MINOR@
diff --git a/configure.ac b/configure.ac
index e20e91e7fa42..479968be94b9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8148,10 +8148,19 @@ if test $enable_python = system; then
fi
dnl By now enable_python should be "system", "internal" or "no"
+PYTHON_LXML=
case $enable_python in
system)
SYSTEM_PYTHON=TRUE
+ AC_MSG_CHECKING([for python lxml])
+ if $PYTHON -c "import lxml.etree as ET" ; then
+ PYTHON_LXML=TRUE
+ AC_MSG_RESULT([yes])
+ else
+ AC_MSG_RESULT([no, will not be able to check UI accessibility])
+ fi
+
dnl Check if the headers really work
save_CPPFLAGS="$CPPFLAGS"
CPPFLAGS="$CPPFLAGS $PYTHON_CFLAGS"
@@ -8213,6 +8222,7 @@ AC_SUBST(DISABLE_PYTHON)
AC_SUBST(SYSTEM_PYTHON)
AC_SUBST(PYTHON_CFLAGS)
AC_SUBST(PYTHON_LIBS)
+AC_SUBST(PYTHON_LXML)
AC_SUBST(PYTHON_VERSION)
AC_SUBST(PYTHON_VERSION_MAJOR)
AC_SUBST(PYTHON_VERSION_MINOR)
diff --git a/solenv/gbuild/TargetLocations.mk b/solenv/gbuild/TargetLocations.mk
index a8310655039a..06ec8bea0f43 100644
--- a/solenv/gbuild/TargetLocations.mk
+++ b/solenv/gbuild/TargetLocations.mk
@@ -156,6 +156,7 @@ gb_ScpTemplateTarget_get_target = $(abspath $(WORKDIR)/ScpTemplateTarget/$(dir $
gb_SdiTarget_get_target = $(WORKDIR)/SdiTarget/$(1)
gb_ThesaurusIndexTarget_get_target = $(WORKDIR)/ThesaurusIndexTarget/$(basename $(1)).idx
gb_UIConfig_get_imagelist_target = $(WORKDIR)/UIConfig/$(1).ilst
+gb_UIConfig_get_a11yerrors_target = $(WORKDIR)/UIConfig/$(1).a11yerrors
gb_UIConfig_get_target = $(WORKDIR)/UIConfig/$(1).done
gb_UIImageListTarget_get_target = $(WORKDIR)/UIImageListTarget/$(1).ilst
gb_UIMenubarTarget_get_target = $(WORKDIR)/UIMenubarTarget/$(1).xml
@@ -280,6 +281,7 @@ $(eval $(call gb_Helper_make_clean_targets,\
CppunitTestFakeExecutable \
CustomTarget \
ExternalProject \
+ UIA11YErrorsTarget \
UIConfig \
UIImageListTarget \
UIMenubarTarget \
diff --git a/solenv/gbuild/UIConfig.mk b/solenv/gbuild/UIConfig.mk
index 01bfdbaea5b6..fb8762e8a09c 100644
--- a/solenv/gbuild/UIConfig.mk
+++ b/solenv/gbuild/UIConfig.mk
@@ -94,6 +94,7 @@ endef
# * UIConfig/<name> containing all nontranslatable files
gb_UIConfig_INSTDIR := $(LIBO_SHARE_FOLDER)/config/soffice.cfg
+gb_UIConfig_a11yerrors_COMMAND = $(SRCDIR)/bin/gla11y
$(dir $(call gb_UIConfig_get_target,%)).dir :
$(if $(wildcard $(dir $@)),,mkdir -p $(dir $@))
@@ -101,7 +102,7 @@ $(dir $(call gb_UIConfig_get_target,%)).dir :
$(dir $(call gb_UIConfig_get_target,%))%/.dir :
$(if $(wildcard $(dir $@)),,mkdir -p $(dir $@))
-$(call gb_UIConfig_get_target,%) : $(call gb_UIConfig_get_imagelist_target,%)
+$(call gb_UIConfig_get_target,%) : $(call gb_UIConfig_get_imagelist_target,%) $(call gb_UIConfig_get_a11yerrors_target,%)
$(call gb_Output_announce,$*,$(true),UIC,2)
$(call gb_Helper_abbreviate_dirs,\
touch $@ \
@@ -117,6 +118,25 @@ $(call gb_UIConfig_get_clean_target,%) :
rm -f $(call gb_UIConfig_get_target,$*) \
)
+define gb_UIConfig_a11yerrors__command
+$(call gb_Output_announce,$(2),$(true),UIA,1)
+$(call gb_Helper_abbreviate_dirs,\
+ $(gb_UIConfig_a11yerrors_COMMAND) -W none $(UIFILE) > $@
+)
+endef
+
+$(call gb_UIConfig_get_a11yerrors_target,%) : $(gb_UIConfig_a11yerrors_COMMAND)
+ifeq ($(PYTHON_LXML),TRUE)
+ $(call gb_UIConfig_a11yerrors__command,$@,$*)
+else
+ touch $@
+endif
+
+.PHONY : $(call gb_UIA11YErrorsTarget_get_clean_target,%)
+$(call gb_UIA11YErrorsTarget_get_clean_target,%) :
+ $(call gb_Output_announce,$*,$(false),UIA,2)
+ rm -f $(call gb_UIConfig_get_a11yerrors_target,$*)
+
gb_UIConfig_get_packagename = UIConfig/$(1)
gb_UIConfig_get_packagesetname = UIConfig/$(1)
@@ -138,6 +158,7 @@ $(call gb_PackageSet_add_package,$(call gb_UIConfig_get_packagesetname,$(1)),$(c
$(call gb_UIConfig_get_target,$(1)) :| $(dir $(call gb_UIConfig_get_target,$(1))).dir
$(call gb_UIConfig_get_imagelist_target,$(1)) :| $(dir $(call gb_UIConfig_get_imagelist_target,$(1))).dir
+$(call gb_UIConfig_get_a11yerrors_target,$(1)) :| $(dir $(call gb_UIConfig_get_a11yerrors_target,$(1))).dir
$(call gb_UIConfig_get_target,$(1)) : $(call gb_PackageSet_get_target,$(call gb_UIConfig_get_packagesetname,$(1)))
$(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_PackageSet_get_clean_target,$(call gb_UIConfig_get_packagesetname,$(1)))
@@ -168,6 +189,9 @@ $(call gb_UIConfig_get_imagelist_target,$(1)) : UI_IMAGELISTS += $(call gb_UIIma
$(call gb_UIConfig_get_imagelist_target,$(1)) : $(call gb_UIImageListTarget_get_target,$(2))
$(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_UIImageListTarget_get_clean_target,$(2))
+$(call gb_UIConfig_get_a11yerrors_target,$(1)) : UIFILE := $(SRCDIR)/$(2).ui
+$(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_UIA11YErrorsTarget_get_clean_target,$(2))
+
endef
gb_UIConfig_ALLFILES:=