2 # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
4 # This file is part of the LibreOffice project.
6 # This Source Code Form is subject to the terms of the Mozilla Public
7 # License, v. 2.0. If a copy of the MPL was not distributed with this
8 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 # This file incorporates work covered by the following license notice:
12 # Copyright (c) 2018 Martin Pieuchot
13 # Copyright (c) 2018-2020 Samuel Thibault <sthibault@hypra.fr>
15 # Permission to use, copy, modify, and distribute this software for any
16 # purpose with or without fee is hereby granted, provided that the above
17 # copyright notice and this permission notice appear in all copies.
19 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
20 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
21 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
22 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
23 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
24 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
25 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
27 # Take LibreOffice (glade) .ui files and check for non accessible widgets
29 # A white paper documents the rationale of the implementation:
31 # https://inria.hal.science/hal-02957129
33 from __future__
import print_function
39 import lxml
.etree
as ET
42 if sys
.version_info
< (2,7):
43 print("gla11y needs lxml or python >= 2.7")
45 import xml
.etree
.ElementTree
as ET
48 howto_url
= "https://wiki.documentfoundation.org/Development/Accessibility"
54 'GtkApplicationWindow',
56 'GtkFileChooserDialog',
57 'GtkColorChooserDialog',
58 'GtkFontChooserDialog',
60 'GtkRecentChooserDialog',
62 'GtkAppChooserDialog',
67 widgets_ignored
= widgets_toplevel
+ [
100 'GtkShortcutsSection',
116 'GtkCellRendererGraph',
117 'GtkCellRendererPixbuf',
118 'GtkCellRendererProgress',
119 'GtkCellRendererSpin',
120 'GtkCellRendererText',
121 'GtkCellRendererToggle',
122 'GtkSeparatorMenuItem',
123 'GtkSeparatorToolItem',
128 'GtkTreeModelFilter',
140 'GtkEntryCompletion',
159 # These are actually labels
162 # This precisely give a11y information :)
166 widgets_suffixignored
= [
169 # These widgets always need a label
170 widgets_needlabel
= [
180 # These widgets normally have their own label
185 'GtkToggleToolButton',
187 'GtkRadioToolButton',
201 # These widgets are labels that can label other widgets
207 # The rest should probably be labelled if there are orphan labels
215 # GtkFileChooserButton
216 # GtkAppChooserButton
219 # GtkColorChooserWidget
238 # GtkFileChooserWidget ?
240 # GtkFontChooserWidget ?
247 # GtkPrinterOptionWidget ?
252 progname
= os
.path
.basename(sys
.argv
[0])
254 # This dictionary contains the set of suppression lines as read from the
255 # suppression file(s). It is merely indexed by the text of the suppression line
256 # and contains whether the suppressions was unused.
259 # This dictionary is indexed like suppressions and returns a "file:line" string
260 # to report where in the suppression file the suppression was read
261 suppressions_to_line
= {}
263 # This dictionary is similar to the suppressions dictionary, but for false
264 # positives rather than suppressions
267 # This dictionary is indexed by the xml id and returns the element object.
269 # This dictionary is indexed by the xml id and returns whether several objects
273 # This dictionary is indexed by the xml id of an element A and returns the list
274 # of objects which are labelled-by A.
277 # This dictionary is indexed by the xml id of an element A and returns the list
278 # of objects which are label-for A.
281 # This dictionary is indexed by the xml id of an element A and returns the list
282 # of objects which have a mnemonic-for A.
283 mnemonic_for_elm
= {}
285 # Possibly a file name to put generated suppression lines in
287 # The corresponding opened file
289 # A prefix to remove from file names in the generated suppression lines
292 # Possibly an opened file in which our output should also be written to.
295 # Whether -p option was set, i.e. print XML class path instead of line number in
299 # Whether we should warn about labels which are orphan
300 warn_orphan_labels
= True
304 # Number of suppressed errors
308 # Number of suppressed warnings
310 # Number of fatal errors
312 # Number of suppressed fatal errors
315 # List of warnings and errors which are fatal
317 # Format of each element: (enabled, type, class)
318 # See the is_enabled function: the list is traversed completely, each element
319 # can specify whether it enables or disables the warning, possibly the type of
320 # warning to be enabled/disabled, possibly the class of XML element for which it
323 # This mechanism matches the semantic of the parameters on the command line,
324 # each of which refining the semantic set by the previous parameters
327 # List of warnings and errors which are enabled
328 # Same format as dofatals
331 # buffers all printed output, so it isn't split in parallel builds
335 # XML browsing and printing functions
338 def elm_parent(root
, elm
):
340 Return the parent of the element.
343 return elm
.getparent()
345 def find_parent(cur
, elm
):
349 parent
= find_parent(o
, elm
)
350 if parent
is not None:
353 return find_parent(root
, elm
)
357 Return the XML class path step corresponding to elm.
358 This can be empty if the elm does not have any class or id.
360 step
= elm
.attrib
.get('class')
363 oid
= elm
.attrib
.get('id')
365 oid
= oid
.encode('ascii','ignore').decode('ascii')
366 step
+= "[@id='%s']" % oid
371 def find_elm(root
, elm
):
373 Return the XML class path of the element from the given root.
374 This is the slow version used when getparent is not available.
379 path
= find_elm(o
, elm
)
385 def errpath(filename
, tree
, elm
):
387 Return the XML class path of the element
392 if 'class' in elm
.attrib
:
393 path
+= elm
.attrib
['class']
394 oid
= elm
.attrib
.get('id')
396 oid
= oid
.encode('ascii','ignore').decode('ascii')
397 path
= "//" + path
+ "[@id='%s']" % oid
400 elm
= elm
.getparent()
401 while elm
is not None:
404 elm
= elm
.getparent()
406 path
= find_elm(tree
.getroot(), elm
)[:-1]
407 path
= filename
+ ':' + path
411 # Warning/Error printing functions
414 def elm_prefix(filename
, elm
):
416 Return the display prefix of the element
418 if elm
== None or not lxml
:
419 return "%s:" % filename
421 return "%s:%u" % (filename
, elm
.sourceline
)
425 Return a display name of the element
429 if 'class' in elm
.attrib
:
430 name
= "'%s' " % elm
.attrib
['class']
431 if 'id' in elm
.attrib
:
432 id = elm
.attrib
['id'].encode('ascii','ignore').decode('ascii')
435 name
= "'" + elm
.tag
+ "'"
437 name
+= " line " + str(elm
.sourceline
)
441 def elm_name_line(elm
):
443 Return a display name of the element with line number
447 if lxml
and " line " not in name
:
448 name
+= "line " + str(elm
.sourceline
) + " "
454 Return the line for the given element.
457 return " line " + str(elm
.sourceline
)
461 def elms_lines(elms
):
463 Return the list of lines for the given elements.
466 return " lines " + ', '.join([str(l
.sourceline
) for l
in elms
])
470 def elms_names_lines(elms
):
472 Return the list of names and lines for the given elements.
474 return ', '.join([elm_name_line(elm
) for elm
in elms
])
476 def elm_suppr(filename
, tree
, elm
, msgtype
, dogen
):
478 Return the prefix to be displayed to the user and the suppression line for
479 the warning type "msgtype" for element "elm"
481 global gen_suppr
, gen_supprfile
, suppr_prefix
, pflag
483 if suppressions
or false_positives
or gen_suppr
is not None or pflag
:
484 prefix
= errpath(filename
, tree
, elm
)
485 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
486 prefix
= prefix
[len(suppr_prefix
):]
488 if suppressions
or false_positives
or gen_suppr
is not None:
489 suppr
= '%s %s' % (prefix
, msgtype
)
491 if gen_suppr
is not None and msgtype
is not None and dogen
:
492 if gen_supprfile
is None:
493 gen_supprfile
= open(gen_suppr
, 'w')
494 print(suppr
, file=gen_supprfile
)
499 # Use user-friendly line numbers
500 prefix
= elm_prefix(filename
, elm
)
501 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
502 prefix
= prefix
[len(suppr_prefix
):]
504 return (prefix
, suppr
)
506 def is_enabled(elm
, msgtype
, l
, default
):
508 Test whether warning type msgtype is enabled for elm in l
511 for (enable
, thetype
, klass
) in l
:
513 if thetype
is not None:
514 if thetype
!= msgtype
:
517 if klass
is not None and elm
is not None:
518 if klass
!= elm
.attrib
.get('class'):
523 def err(filename
, tree
, elm
, msgtype
, msg
, error
= True):
525 Emit a warning or error for an element
527 global errors
, errexists
, warnings
, warnexists
, fatals
, fatalexists
, output_buffer
529 # Let user tune whether a warning or error
530 fatal
= is_enabled(elm
, msgtype
, dofatals
, error
)
532 # By default warnings and errors are enabled, but let user tune it
533 if not is_enabled(elm
, msgtype
, enables
, True):
536 (prefix
, suppr
) = elm_suppr(filename
, tree
, elm
, msgtype
, True)
537 if suppr
in false_positives
:
538 # That was actually expected
540 if suppr
in suppressions
:
542 suppressions
[suppr
] = False
558 msg
= "%s %s%s: %s%s" % (prefix
,
559 "FATAL " if fatal
else "",
560 "ERROR" if error
else "WARNING",
562 output_buffer
+= msg
+ "\n"
563 if outfile
is not None:
564 print(msg
, file=outfile
)
566 def warn(filename
, tree
, elm
, msgtype
, msg
):
568 Emit a warning for an element
570 err(filename
, tree
, elm
, msgtype
, msg
, False)
573 # Labelling testing functions
576 def find_button_parent(root
, elm
):
578 Find a parent which is a button
581 parent
= elm
.getparent()
582 if parent
is not None:
583 if parent
.attrib
.get('class') in widgets_buttons
:
585 return find_button_parent(root
, parent
)
587 def find_parent(cur
, elm
):
590 if cur
.attrib
.get('class') in widgets_buttons
:
591 # we are the button, immediately above the target
594 # we aren't the button, but target is over there
596 parent
= find_parent(o
, elm
)
598 # It is over there, but didn't find a button yet
599 if cur
.attrib
.get('class') in widgets_buttons
:
604 if parent
is not None:
605 # we have the button parent over there
608 parent
= find_parent(root
, elm
)
614 def is_labelled_parent(elm
):
616 Return whether this element is a labelled parent
618 klass
= elm
.attrib
.get('class')
619 if klass
in widgets_toplevel
:
621 if klass
== 'GtkShortcutsGroup':
622 children
= elm
.findall("property[@name='title']")
623 if len(children
) >= 1:
625 if klass
== 'GtkFrame' or klass
== 'GtkNotebook':
626 children
= elm
.findall("child[@type='tab']") + elm
.findall("child[@type='label']")
627 if len(children
) >= 1:
631 def elm_labelled_parent(root
, elm
):
633 Return the first labelled parent of the element, which can thus be used as
634 the root of widgets with common labelled context
638 def find_labelled_parent(elm
):
639 if is_labelled_parent(elm
):
641 parent
= elm
.getparent()
644 return find_labelled_parent(parent
)
645 parent
= elm
.getparent()
648 return find_labelled_parent(elm
.getparent())
650 def find_labelled_parent(cur
, elm
):
652 # the target element is over there
655 parent
= find_labelled_parent(o
, elm
)
657 # target element is over there, check ourself
658 if is_labelled_parent(cur
):
659 # yes, and we are the first ancestor of the target element
662 # no, but target element is over there.
665 # the first ancestor of the target element was over there
668 parent
= find_labelled_parent(root
, elm
)
673 def is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, doprint
= False):
675 Check whether this label has no accessibility relation, or doubtful relation
676 because another label labels the same target
678 global label_for_elm
, labelled_by_elm
, mnemonic_for_elm
, warnexists
681 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
682 for rel
in label_for
:
683 target
= rel
.attrib
['target']
684 l
= label_for_elm
[target
]
689 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
690 obj
.findall("property[@name='mnemonic-widget']")
691 for rel
in mnemonic_for
:
693 l
= mnemonic_for_elm
[target
]
697 if len(label_for
) > 0:
698 # At least one label-for, we are not orphan.
701 if len(mnemonic_for
) > 0:
702 # At least one mnemonic_widget, we are not orphan.
705 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
706 if len(labelled_by
) > 0:
707 # Oh, a labelled label, probably not to be labelling anything
711 roles
= [x
.text
for x
in obj
.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
712 roles
+= [x
.attrib
.get("type") for x
in obj
.findall("accessibility/role")]
713 if len(roles
) > 1 and doprint
:
714 err(filename
, tree
, obj
, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
715 "%s" % elms_lines(children
))
717 if role
== 'static' or role
== 'ATK_ROLE_STATIC':
718 # This is static text, not meant to label anything
721 parent
= elm_parent(root
, obj
)
722 if parent
is not None:
723 childtype
= parent
.attrib
.get('type')
724 if childtype
is None:
725 childtype
= parent
.attrib
.get('internal-child')
726 if parent
.tag
== 'child' and childtype
== 'label' \
727 or childtype
== 'tab':
728 # This is a frame or a notebook label, not orphan.
731 if find_button_parent(root
, obj
) is not None:
732 # This label is part of a button
735 oid
= obj
.attrib
.get('id')
737 if oid
in labelled_by_elm
:
738 # Some widget is labelled by us, we are not orphan.
739 # We should have had a label-for, will warn about it later.
742 # No label-for, no mnemonic-for, no labelled-by, we are orphan.
743 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "orphan-label", False)
744 if suppr
in false_positives
:
745 # That was actually expected
747 if suppr
in suppressions
:
748 # Warning suppressed for this label
749 if suppressions
[suppr
]:
751 suppressions
[suppr
] = False
755 context
= elm_name(orphan_root
)
757 context
= " within " + context
758 warn(filename
, tree
, obj
, "orphan-label", "does not specify what it labels" + context
)
761 def is_orphan_widget(filename
, tree
, root
, obj
, orphan
, orphan_root
, doprint
= False):
763 Check whether this widget has no accessibility relation.
766 if obj
.tag
!= 'object':
769 oid
= obj
.attrib
.get('id')
770 klass
= obj
.attrib
.get('class')
772 # "Don't care" special case
773 if klass
in widgets_ignored
:
775 for suffix
in widgets_suffixignored
:
776 if klass
[-len(suffix
):] == suffix
:
779 # Widgets usual do not strictly require a label, i.e. a labelled parent
780 # is enough for context, but some do always need one.
781 requires_label
= klass
in widgets_needlabel
783 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
785 # Labels special case
786 if klass
in widgets_labels
:
789 # Case 1: has an explicit <child internal-child="accessible"> sub-element
790 children
= obj
.findall("child[@internal-child='accessible']")
791 if len(children
) > 1 and doprint
:
792 err(filename
, tree
, obj
, "multiple-accessible", "has multiple <child internal-child='accessible'>"
793 "%s" % elms_lines(children
))
794 if len(children
) >= 1:
797 # Case 2: has an <accessibility> sub-element with a "labelled-by"
798 # <relation> pointing to an existing element.
799 if len(labelled_by
) > 0:
802 # Case 3: has a label-for
803 if oid
in label_for_elm
:
806 # Case 4: has a mnemonic
807 if oid
in mnemonic_for_elm
:
810 # Case 5: Has a <property name="tooltip_text">
811 tooltips
= obj
.findall("property[@name='tooltip_text']") + \
812 obj
.findall("property[@name='tooltip-text']")
813 if len(tooltips
) > 1 and doprint
:
814 err(filename
, tree
, obj
, "multiple-tooltip", "has multiple tooltip_text properties")
815 if len(tooltips
) >= 1 and klass
!= 'GtkCheckButton':
818 # Case 6: Has a <property name="placeholder_text">
819 placeholders
= obj
.findall("property[@name='placeholder_text']") + \
820 obj
.findall("property[@name='placeholder-text']")
821 if len(placeholders
) > 1 and doprint
:
822 err(filename
, tree
, obj
, "multiple-placeholder", "has multiple placeholder_text properties")
823 if len(placeholders
) >= 1:
826 # Buttons usually don't need an external label, their own is enough, (but they do need one)
827 if klass
in widgets_buttons
:
829 labels
= obj
.findall("property[@name='label']")
830 if len(labels
) > 1 and doprint
:
831 err(filename
, tree
, obj
, "multiple-label", "has multiple label properties")
833 # Has a <property name="label">
836 actions
= obj
.findall("property[@name='action_name']")
837 if len(actions
) > 1 and doprint
:
838 err(filename
, tree
, obj
, "multiple-action_name", "has multiple action_name properties")
839 if len(actions
) >= 1:
840 # Has a <property name="action_name">
843 # Uses id as an action_name
844 if 'id' in obj
.attrib
:
845 if obj
.attrib
['id'].startswith(".uno:"):
848 gtklabels
= obj
.findall(".//object[@class='GtkLabel']") + obj
.findall(".//object[@class='GtkAccelLabel']")
849 if len(gtklabels
) >= 1:
853 # no label for a button, warn
855 warn(filename
, tree
, obj
, "button-no-label", "does not have its own label")
856 if not is_enabled(obj
, "button-no-label", enables
, True):
859 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "button-no-label", False)
860 if suppr
in false_positives
:
861 # That was actually expected
863 if suppr
in suppressions
:
864 # Warning suppressed for this widget
865 if suppressions
[suppr
]:
867 suppressions
[suppr
] = False
871 # GtkImages special case
872 if klass
== "GtkImage":
873 uses
= [u
for u
in tree
.iterfind(".//object/property[@name='image']") if u
.text
== oid
]
875 # This image is just used by another element, don't warn
876 # about the image itself, we probably want the warning on
877 # the element instead.
880 if find_button_parent(root
, obj
) is not None:
881 # This image is part of a button, we want the warning on the button
885 # GtkEntry special case
886 if klass
== 'GtkEntry' or klass
== 'GtkSearchEntry':
887 parent
= elm_parent(root
, obj
)
888 if parent
is not None:
889 if parent
.tag
== 'child' and \
890 parent
.attrib
.get('internal-child') == "entry":
891 # This is an internal entry of another widget. Relations
892 # will be handled by that widget.
895 # GtkShortcutsShortcut special case
896 if klass
== 'GtkShortcutsShortcut':
897 children
= obj
.findall("property[@name='title']")
898 if len(children
) >= 1:
901 # Really no label, perhaps emit a warning
902 if not is_enabled(obj
, "no-labelled-by", enables
, True):
903 # Warnings disabled for this class of widgets
905 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "no-labelled-by", False)
906 if suppr
in false_positives
:
907 # That was actually expected
909 if suppr
in suppressions
:
910 # Warning suppressed for this widget
911 if suppressions
[suppr
]:
913 suppressions
[suppr
] = False
917 # No orphan label, so probably the labelled parent provides enough
920 # But these always need a label.
922 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label")
927 context
= elm_name(orphan_root
)
929 context
= " within " + context
930 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label while there are orphan labels" + context
)
933 def orphan_items(filename
, tree
, root
, elm
):
935 Check whether from some element there exists orphan labels and orphan widgets
937 orphan_labels
= False
938 orphan_widgets
= False
939 if elm
.attrib
.get('class') in widgets_labels
:
940 orphan_labels
= is_orphan_label(filename
, tree
, root
, elm
, None)
942 orphan_widgets
= is_orphan_widget(filename
, tree
, root
, elm
, True, None)
944 # We are not interested in orphan labels under another labelled
945 # parent. This also allows to keep linear complexity.
946 if not is_labelled_parent(obj
):
947 label
, widget
= orphan_items(filename
, tree
, root
, obj
)
951 orphan_widgets
= True
952 if orphan_labels
and orphan_widgets
:
953 # No need to look up more
955 return orphan_labels
, orphan_widgets
958 # UI accessibility checks
961 def check_props(filename
, tree
, root
, elm
, forward
):
963 Check the given list of relation properties
965 props
= elm
.findall("property[@name='" + forward
+ "']")
967 if prop
.text
not in ids
:
968 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % prop
.text
)
973 visible_prop
= obj
.findall("property[@name='visible']")
974 visible_len
= len(visible_prop
)
976 visible_txt
= visible_prop
[visible_len
- 1].text
977 if visible_txt
.lower() == "true":
979 elif visible_txt
.lower() == "false":
983 def check_rels(filename
, tree
, root
, elm
, forward
, backward
= None):
985 Check the relations given by forward
987 oid
= elm
.attrib
.get('id')
988 rels
= elm
.findall("accessibility/relation[@type='" + forward
+ "']")
990 target
= rel
.attrib
['target']
991 if target
not in ids
:
992 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % target
)
993 elif backward
is not None:
995 backrels
= widget
.findall("accessibility/relation[@type='" + backward
+ "']")
996 if len([x
for x
in backrels
if x
.attrib
['target'] == oid
]) == 0:
997 err(filename
, tree
, elm
, "missing-" + backward
, "has " + forward
+ \
998 ", but is not " + backward
+ " by " + elm_name_line(widget
))
1001 def check_a11y_relation(filename
, tree
):
1003 Emit an error message if any of the 'object' elements of the XML
1004 document represented by `root' doesn't comply with Accessibility
1007 global widgets_ignored
, ids
, label_for_elm
, labelled_by_elm
, mnemonic_for_elm
1009 def check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1011 Check one element, knowing that orphan_labels/widgets tell whether
1012 there are orphan labels and widgets within orphan_root
1015 oid
= obj
.attrib
.get('id')
1016 klass
= obj
.attrib
.get('class')
1018 # "Don't care" special case
1019 if klass
in widgets_ignored
:
1021 for suffix
in widgets_suffixignored
:
1022 if klass
[-len(suffix
):] == suffix
:
1025 # Widgets usual do not strictly require a label, i.e. a labelled parent
1026 # is enough for context, but some do always need one.
1027 requires_label
= klass
in widgets_needlabel
1030 # Check that ids are unique
1033 # We are the first, warn
1034 duplicates
= tree
.findall(".//object[@id='" + oid
+ "']")
1035 err(filename
, tree
, obj
, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates
))
1037 # Check label-for and their dual labelled-by
1038 label_for
= check_rels(filename
, tree
, root
, obj
, "label-for", "labelled-by")
1040 # Check labelled-by and its dual label-for
1041 labelled_by
= check_rels(filename
, tree
, root
, obj
, "labelled-by", "label-for")
1043 visible
= is_visible(obj
)
1045 # warning message type "syntax" used:
1047 # multiple-* => 2+ XML tags of the inspected element itself
1048 # duplicate-* => 2+ XML tags of other elements referencing this element
1050 # Should have only one label
1051 if len(labelled_by
) >= 1:
1052 if oid
in mnemonic_for_elm
:
1053 warn(filename
, tree
, obj
, "labelled-by-and-mnemonic",
1054 "has both a mnemonic " + elm_name_line(mnemonic_for_elm
[oid
][0]) + "and labelled-by relation")
1055 if len(labelled_by
) > 1:
1056 warn(filename
, tree
, obj
, "multiple-labelled-by", "has multiple labelled-by relations")
1058 if oid
in labelled_by_elm
:
1059 if len(labelled_by_elm
[oid
]) == 1:
1060 paired
= labelled_by_elm
[oid
][0]
1061 if paired
!= None and visible
!= is_visible(paired
):
1062 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1064 if oid
in label_for_elm
:
1065 if len(label_for_elm
[oid
]) > 1:
1066 warn(filename
, tree
, obj
, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm
[oid
]))
1067 elif len(label_for_elm
[oid
]) == 1:
1068 paired
= label_for_elm
[oid
][0]
1069 if visible
!= is_visible(paired
):
1070 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1072 if oid
in mnemonic_for_elm
:
1073 if len(mnemonic_for_elm
[oid
]) > 1:
1074 warn(filename
, tree
, obj
, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm
[oid
]))
1076 # Check controlled-by/controller-for
1077 controlled_by
= check_rels(filename
, tree
, root
, obj
, "controlled-by", "controller-for")
1078 controller_for
= check_rels(filename
, tree
, root
, obj
, "controlled-for", "controlled-by")
1080 # Labels special case
1081 if klass
in widgets_labels
:
1082 properties
= check_props(filename
, tree
, root
, obj
, "mnemonic_widget") + \
1083 check_props(filename
, tree
, root
, obj
, "mnemonic-widget")
1084 if len(properties
) > 1:
1085 err(filename
, tree
, obj
, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
1086 "%s" % elms_lines(properties
))
1088 # Emit orphaning warnings
1089 if warn_orphan_labels
or orphan_widgets
:
1090 is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, True)
1092 # We are done with the label
1095 # Not a label, will perhaps need one
1097 # Emit orphaning warnings
1098 is_orphan_widget(filename
, tree
, root
, obj
, orphan_labels
, orphan_root
, True)
1100 root
= tree
.getroot()
1102 # Flush ids and relations from previous files
1105 labelled_by_elm
= {}
1107 mnemonic_for_elm
= {}
1109 # First pass to get links into hash tables, no warning, just record duplicates
1110 for obj
in root
.iter('object'):
1111 oid
= obj
.attrib
.get('id')
1118 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
1119 for rel
in labelled_by
:
1120 target
= rel
.attrib
.get('target')
1121 if target
is not None:
1122 if target
not in labelled_by_elm
:
1123 labelled_by_elm
[target
] = [ obj
]
1125 labelled_by_elm
[target
].append(obj
)
1127 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
1128 for rel
in label_for
:
1129 target
= rel
.attrib
.get('target')
1130 if target
is not None:
1131 if target
not in label_for_elm
:
1132 label_for_elm
[target
] = [ obj
]
1134 label_for_elm
[target
].append(obj
)
1136 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
1137 obj
.findall("property[@name='mnemonic-widget']")
1138 for rel
in mnemonic_for
:
1140 if target
is not None:
1141 if target
not in mnemonic_for_elm
:
1142 mnemonic_for_elm
[target
] = [ obj
]
1144 mnemonic_for_elm
[target
].append(obj
)
1146 # Second pass, recursive depth-first, to be able to efficiently know whether
1147 # there are orphan labels within a part of the tree.
1148 def recurse(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1149 if obj
== root
or is_labelled_parent(obj
):
1151 orphan_labels
, orphan_widgets
= orphan_items(filename
, tree
, root
, obj
)
1153 if obj
.tag
== 'object':
1154 check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
)
1157 recurse(orphan_root
, o
, orphan_labels
, orphan_widgets
)
1159 recurse(root
, root
, False, False)
1165 def usage(fatal
= True):
1166 print("`%s' checks accessibility of glade .ui files" % progname
)
1168 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname
)
1170 print(" -p Print XML class path instead of line number")
1171 print(" -g Generate suppression file SUPPR_FILE")
1172 print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
1173 print(" -f Suppress warnings given by file SUPPR_FILE completely")
1174 print(" -P Remove PREFIX from file names in warnings")
1175 print(" -o Also prints errors and warnings to given file")
1177 print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
1178 print(" Give or extend one of the lists of widget classes, where FOO can be:")
1179 print(" - toplevel : widgets to be considered toplevel windows")
1180 print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
1181 print(" - suffixignored : suffixes of widget classes which do not need labelling")
1182 print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
1183 print(" - buttons : widgets which need their own label but not more")
1184 print(" (e.g. GtkButton)")
1185 print(" - labels : widgets which provide labels (e.g. GtkLabel)")
1186 print(" --widgets-print print default widgets lists")
1188 print(" --enable-all enable all warnings/dofatals (default)")
1189 print(" --disable-all disable all warnings/dofatals")
1190 print(" --fatal-all make all warnings dofatals")
1191 print(" --not-fatal-all do not make all warnings dofatals (default)")
1193 print(" --enable-type=TYPE enable warning/fatal type TYPE")
1194 print(" --disable-type=TYPE disable warning/fatal type TYPE")
1195 print(" --fatal-type=TYPE make warning type TYPE a fatal")
1196 print(" --not-fatal-type=TYPE make warning type TYPE not a fatal")
1198 print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
1199 print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
1200 print(" --fatal-widgets=CLASS make warning type CLASS a fatal")
1201 print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal")
1203 print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
1204 print(" class CLASS")
1205 print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
1206 print(" class CLASS")
1207 print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget")
1208 print(" class CLASS")
1209 print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget")
1210 print(" class CLASS")
1212 print(" --disable-orphan-labels only warn about orphan labels when there are")
1213 print(" orphan widgets in the same context")
1215 print("Report bugs to <bugs@hypra.fr>")
1216 sys
.exit(2 if fatal
else 0)
1218 def widgets_opt(widgets_list
, arg
):
1220 Replace or extend `widgets_list' with the list of classes contained in `arg'
1222 append
= arg
and arg
[0] == '+'
1227 widgets
= arg
.split(',')
1234 widgets_list
.extend(widgets
)
1238 global pflag
, gen_suppr
, gen_supprfile
, suppressions
, suppr_prefix
, false_positives
, dofatals
, enables
, dofatals
, warn_orphan_labels
1239 global widgets_toplevel
, widgets_ignored
, widgets_suffixignored
, widgets_needlabel
, widgets_buttons
, widgets_labels
1240 global outfile
, output_buffer
1243 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hpiIg:s:f:P:o:L:", [
1247 "widgets-toplevel=",
1249 "widgets-suffixignored=",
1250 "widgets-needlabel=",
1268 "not-fatal-widgets=",
1271 "disable-specific=",
1273 "not-fatal-specific=",
1275 "disable-orphan-labels",
1277 except getopt
.GetoptError
:
1286 if o
== "--help" or o
== "-h":
1288 if o
== "--version":
1306 elif o
== "--widgets-toplevel":
1307 widgets_opt(widgets_toplevel
, a
)
1308 elif o
== "--widgets-ignored":
1309 widgets_opt(widgets_ignored
, a
)
1310 elif o
== "--widgets-suffixignored":
1311 widgets_opt(widgets_suffixignored
, a
)
1312 elif o
== "--widgets-needlabel":
1313 widgets_opt(widgets_needlabel
, a
)
1314 elif o
== "--widgets-buttons":
1315 widgets_opt(widgets_buttons
, a
)
1316 elif o
== "--widgets-labels":
1317 widgets_opt(widgets_labels
, a
)
1318 elif o
== "--widgets-print":
1319 print("--widgets-toplevel '" + ','.join(widgets_toplevel
) + "'")
1320 print("--widgets-ignored '" + ','.join(widgets_ignored
) + "'")
1321 print("--widgets-suffixignored '" + ','.join(widgets_suffixignored
) + "'")
1322 print("--widgets-needlabel '" + ','.join(widgets_needlabel
) + "'")
1323 print("--widgets-buttons '" + ','.join(widgets_buttons
) + "'")
1324 print("--widgets-labels '" + ','.join(widgets_labels
) + "'")
1327 elif o
== '--enable-all':
1328 enables
.append( (True, None, None) )
1329 elif o
== '--disable-all':
1330 enables
.append( (False, None, None) )
1331 elif o
== '--fatal-all':
1332 dofatals
.append( (True, None, None) )
1333 elif o
== '--not-fatal-all':
1334 dofatals
.append( (False, None, None) )
1336 elif o
== '--enable-type':
1337 enables
.append( (True, a
, None) )
1338 elif o
== '--disable-type':
1339 enables
.append( (False, a
, None) )
1340 elif o
== '--fatal-type':
1341 dofatals
.append( (True, a
, None) )
1342 elif o
== '--not-fatal-type':
1343 dofatals
.append( (False, a
, None) )
1345 elif o
== '--enable-widgets':
1346 enables
.append( (True, None, a
) )
1347 elif o
== '--disable-widgets':
1348 enables
.append( (False, None, a
) )
1349 elif o
== '--fatal-widgets':
1350 dofatals
.append( (True, None, a
) )
1351 elif o
== '--not-fatal-widgets':
1352 dofatals
.append( (False, None, a
) )
1354 elif o
== '--enable-specific':
1355 (thetype
, klass
) = a
.split('.', 1)
1356 enables
.append( (True, thetype
, klass
) )
1357 elif o
== '--disable-specific':
1358 (thetype
, klass
) = a
.split('.', 1)
1359 enables
.append( (False, thetype
, klass
) )
1360 elif o
== '--fatal-specific':
1361 (thetype
, klass
) = a
.split('.', 1)
1362 dofatals
.append( (True, thetype
, klass
) )
1363 elif o
== '--not-fatal-specific':
1364 (thetype
, klass
) = a
.split('.', 1)
1365 dofatals
.append( (False, thetype
, klass
) )
1367 elif o
== '--disable-orphan-labels':
1368 warn_orphan_labels
= False
1372 # Read suppression file before overwriting it
1373 if suppr
is not None:
1375 output_header
+= "Suppression file: " + suppr
+ "\n"
1376 supprfile
= open(suppr
, 'r')
1378 for line
in supprfile
.readlines():
1379 line_no
= line_no
+ 1
1380 if line
.startswith('#'):
1382 prefix
= line
.rstrip()
1383 suppressions
[prefix
] = True
1384 suppressions_to_line
[prefix
] = "%s:%u" % (suppr
, line_no
)
1389 # Read false positives file
1390 if false
is not None:
1392 output_header
+= "False positive file: " + false
+ "\n"
1393 falsefile
= open(false
, 'r')
1394 for line
in falsefile
.readlines():
1395 if line
.startswith('#'):
1397 prefix
= line
.rstrip()
1398 false_positives
[prefix
] = True
1404 outfile
= open(out
, 'w')
1406 if filelist
is not None:
1408 filelistfile
= open(filelist
, 'r')
1409 for line
in filelistfile
.readlines():
1412 args
+= line
.split(' ')
1413 filelistfile
.close()
1415 err(filelist
, None, None, "unable to read file list file")
1417 for filename
in args
:
1419 tree
= ET
.parse(filename
)
1420 except ET
.ParseError
:
1421 err(filename
, None, None, "parse", "malformatted xml file")
1424 err(filename
, None, None, None, "unable to read file")
1428 check_a11y_relation(filename
, tree
)
1429 except Exception as error
:
1431 output_buffer
+= traceback
.format_exc()
1432 err(filename
, None, None, "parse", "error parsing file")
1434 if errors
> 0 or errexists
> 0:
1435 output_buffer
+= "%s new error%s" % (errors
, 's' if errors
!= 1 else '')
1437 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (errexists
, suppr
, 'them' if errexists
> 1 else 'it')
1438 output_buffer
+= "\n"
1440 if warnings
> 0 or warnexists
> 0:
1441 output_buffer
+= "%s new warning%s" % (warnings
, 's' if warnings
!= 1 else '')
1443 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (warnexists
, suppr
, 'them' if warnexists
> 1 else 'it')
1444 output_buffer
+= "\n"
1446 if fatals
> 0 or fatalexists
> 0:
1447 output_buffer
+= "%s new fatal%s" % (fatals
, 's' if fatals
!= 1 else '')
1449 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (fatalexists
, suppr
, 'them' if fatalexists
> 1 else 'it')
1450 output_buffer
+= "\n"
1453 for (suppr
,unused
) in suppressions
.items():
1458 output_buffer
+= "%s suppression%s unused:\n" % (n
, 's' if n
!= 1 else '')
1459 for (suppr
,unused
) in suppressions
.items():
1461 output_buffer
+= " %s:%s\n" % (suppressions_to_line
[suppr
], suppr
)
1463 if gen_supprfile
is not None:
1464 gen_supprfile
.close()
1465 if outfile
is not None:
1468 if gen_suppr
is None:
1469 if output_buffer
!= "":
1470 output_buffer
+= "Explanations are available on " + howto_url
+ "\n"
1473 print(output_header
.rstrip() + "\n" + output_buffer
)
1476 if len(output_buffer
) > 0:
1477 print(output_header
.rstrip() + "\n" + output_buffer
)
1479 if __name__
== "__main__":
1482 except KeyboardInterrupt:
1485 # vim: set shiftwidth=4 softtabstop=4 expandtab: