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 from __future__
import print_function
35 import lxml
.etree
as ET
38 if sys
.version_info
< (2,7):
39 print("gla11y needs lxml or python >= 2.7")
41 import xml
.etree
.ElementTree
as ET
44 howto_url
= "https://wiki.documentfoundation.org/Development/Accessibility"
50 'GtkApplicationWindow',
52 'GtkFileChooserDialog',
53 'GtkColorChooserDialog',
54 'GtkFontChooserDialog',
56 'GtkRecentChooserDialog',
58 'GtkAppChooserDialog',
63 widgets_ignored
= widgets_toplevel
+ [
97 'GtkShortcutsSection',
113 'GtkCellRendererGraph',
114 'GtkCellRendererPixbuf',
115 'GtkCellRendererProgress',
116 'GtkCellRendererSpin',
117 'GtkCellRendererText',
118 'GtkCellRendererToggle',
119 'GtkSeparatorMenuItem',
120 'GtkSeparatorToolItem',
125 'GtkTreeModelFilter',
137 'GtkEntryCompletion',
156 # These are actually labels
159 # This precisely give a11y information :)
163 widgets_suffixignored
= [
166 # These widgets always need a label
167 widgets_needlabel
= [
177 # These widgets normally have their own label
182 'GtkToggleToolButton',
184 'GtkRadioToolButton',
198 # These widgets are labels that can label other widgets
204 # The rest should probably be labelled if there are orphan labels
212 # GtkFileChooserButton
213 # GtkAppChooserButton
216 # GtkColorChooserWidget
235 # GtkFileChooserWidget ?
237 # GtkFontChooserWidget ?
244 # GtkPrinterOptionWidget ?
249 progname
= os
.path
.basename(sys
.argv
[0])
251 # This dictionary contains the set of suppression lines as read from the
252 # suppression file(s). It is merely indexed by the text of the suppression line
253 # and contains whether the suppressions was unused.
256 # This dictionary is indexed like suppressions and returns a "file:line" string
257 # to report where in the suppression file the suppression was read
258 suppressions_to_line
= {}
260 # This dictionary is similar to the suppressions dictionary, but for false
261 # positives rather than suppressions
264 # This dictionary is indexed by the xml id and returns the element object.
266 # This dictionary is indexed by the xml id and returns whether several objects
270 # This dictionary is indexed by the xml id of an element A and returns the list
271 # of objects which are labelled-by A.
274 # This dictionary is indexed by the xml id of an element A and returns the list
275 # of objects which are label-for A.
278 # This dictionary is indexed by the xml id of an element A and returns the list
279 # of objects which have a mnemonic-for A.
280 mnemonic_for_elm
= {}
282 # Possibly a file name to put generated suppression lines in
284 # The corresponding opened file
286 # A prefix to remove from file names in the generated suppression lines
289 # Possibly an opened file in which our output should also be written to.
292 # Whether -p option was set, i.e. print XML class path instead of line number in
296 # Whether we should warn about labels which are orphan
297 warn_orphan_labels
= True
301 # Number of suppressed errors
305 # Number of suppressed warnings
307 # Number of fatal errors
309 # Number of suppressed fatal errors
312 # List of warnings and errors which are fatal
314 # Format of each element: (enabled, type, class)
315 # See the is_enabled function: the list is traversed completely, each element
316 # can specify whether it enables or disables the warning, possibly the type of
317 # warning to be enabled/disabled, possibly the class of XML element for which it
320 # This mechanism matches the semantic of the parameters on the command line,
321 # each of which refining the semantic set by the previous parameters
324 # List of warnings and errors which are enabled
325 # Same format as dofatals
328 # buffers all printed output, so it isn't split in parallel builds
332 # XML browsing and printing functions
335 def elm_parent(root
, elm
):
337 Return the parent of the element.
340 return elm
.getparent()
342 def find_parent(cur
, elm
):
346 parent
= find_parent(o
, elm
)
347 if parent
is not None:
350 return find_parent(root
, elm
)
354 Return the XML class path step corresponding to elm.
355 This can be empty if the elm does not have any class or id.
357 step
= elm
.attrib
.get('class')
360 oid
= elm
.attrib
.get('id')
362 oid
= oid
.encode('ascii','ignore').decode('ascii')
363 step
+= "[@id='%s']" % oid
368 def find_elm(root
, elm
):
370 Return the XML class path of the element from the given root.
371 This is the slow version used when getparent is not available.
376 path
= find_elm(o
, elm
)
382 def errpath(filename
, tree
, elm
):
384 Return the XML class path of the element
389 if 'class' in elm
.attrib
:
390 path
+= elm
.attrib
['class']
391 oid
= elm
.attrib
.get('id')
393 oid
= oid
.encode('ascii','ignore').decode('ascii')
394 path
= "//" + path
+ "[@id='%s']" % oid
397 elm
= elm
.getparent()
398 while elm
is not None:
401 elm
= elm
.getparent()
403 path
= find_elm(tree
.getroot(), elm
)[:-1]
404 path
= filename
+ ':' + path
408 # Warning/Error printing functions
411 def elm_prefix(filename
, elm
):
413 Return the display prefix of the element
415 if elm
== None or not lxml
:
416 return "%s:" % filename
418 return "%s:%u" % (filename
, elm
.sourceline
)
422 Return a display name of the element
426 if 'class' in elm
.attrib
:
427 name
= "'%s' " % elm
.attrib
['class']
428 if 'id' in elm
.attrib
:
429 id = elm
.attrib
['id'].encode('ascii','ignore').decode('ascii')
432 name
= "'" + elm
.tag
+ "'"
434 name
+= " line " + str(elm
.sourceline
)
438 def elm_name_line(elm
):
440 Return a display name of the element with line number
444 if lxml
and " line " not in name
:
445 name
+= "line " + str(elm
.sourceline
) + " "
451 Return the line for the given element.
454 return " line " + str(elm
.sourceline
)
458 def elms_lines(elms
):
460 Return the list of lines for the given elements.
463 return " lines " + ', '.join([str(l
.sourceline
) for l
in elms
])
467 def elms_names_lines(elms
):
469 Return the list of names and lines for the given elements.
471 return ', '.join([elm_name_line(elm
) for elm
in elms
])
473 def elm_suppr(filename
, tree
, elm
, msgtype
, dogen
):
475 Return the prefix to be displayed to the user and the suppression line for
476 the warning type "msgtype" for element "elm"
478 global gen_suppr
, gen_supprfile
, suppr_prefix
, pflag
480 if suppressions
or false_positives
or gen_suppr
is not None or pflag
:
481 prefix
= errpath(filename
, tree
, elm
)
482 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
483 prefix
= prefix
[len(suppr_prefix
):]
485 if suppressions
or false_positives
or gen_suppr
is not None:
486 suppr
= '%s %s' % (prefix
, msgtype
)
488 if gen_suppr
is not None and msgtype
is not None and dogen
:
489 if gen_supprfile
is None:
490 gen_supprfile
= open(gen_suppr
, 'w')
491 print(suppr
, file=gen_supprfile
)
496 # Use user-friendly line numbers
497 prefix
= elm_prefix(filename
, elm
)
498 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
499 prefix
= prefix
[len(suppr_prefix
):]
501 return (prefix
, suppr
)
503 def is_enabled(elm
, msgtype
, l
, default
):
505 Test whether warning type msgtype is enabled for elm in l
508 for (enable
, thetype
, klass
) in l
:
510 if thetype
is not None:
511 if thetype
!= msgtype
:
514 if klass
is not None and elm
is not None:
515 if klass
!= elm
.attrib
.get('class'):
520 def err(filename
, tree
, elm
, msgtype
, msg
, error
= True):
522 Emit a warning or error for an element
524 global errors
, errexists
, warnings
, warnexists
, fatals
, fatalexists
, output_buffer
526 # Let user tune whether a warning or error
527 fatal
= is_enabled(elm
, msgtype
, dofatals
, error
)
529 # By default warnings and errors are enabled, but let user tune it
530 if not is_enabled(elm
, msgtype
, enables
, True):
533 (prefix
, suppr
) = elm_suppr(filename
, tree
, elm
, msgtype
, True)
534 if suppr
in false_positives
:
535 # That was actually expected
537 if suppr
in suppressions
:
539 suppressions
[suppr
] = False
555 msg
= "%s %s%s: %s%s" % (prefix
,
556 "FATAL " if fatal
else "",
557 "ERROR" if error
else "WARNING",
559 output_buffer
+= msg
+ "\n"
560 if outfile
is not None:
561 print(msg
, file=outfile
)
563 def warn(filename
, tree
, elm
, msgtype
, msg
):
565 Emit a warning for an element
567 err(filename
, tree
, elm
, msgtype
, msg
, False)
570 # Labelling testing functions
573 def find_button_parent(root
, elm
):
575 Find a parent which is a button
578 parent
= elm
.getparent()
579 if parent
is not None:
580 if parent
.attrib
.get('class') in widgets_buttons
:
582 return find_button_parent(root
, parent
)
584 def find_parent(cur
, elm
):
587 if cur
.attrib
.get('class') in widgets_buttons
:
588 # we are the button, immediately above the target
591 # we aren't the button, but target is over there
593 parent
= find_parent(o
, elm
)
595 # It is over there, but didn't find a button yet
596 if cur
.attrib
.get('class') in widgets_buttons
:
601 if parent
is not None:
602 # we have the button parent over there
605 parent
= find_parent(root
, elm
)
611 def is_labelled_parent(elm
):
613 Return whether this element is a labelled parent
615 klass
= elm
.attrib
.get('class')
616 if klass
in widgets_toplevel
:
618 if klass
== 'GtkShortcutsGroup':
619 children
= elm
.findall("property[@name='title']")
620 if len(children
) >= 1:
622 if klass
== 'GtkFrame' or klass
== 'GtkNotebook':
623 children
= elm
.findall("child[@type='tab']") + elm
.findall("child[@type='label']")
624 if len(children
) >= 1:
628 def elm_labelled_parent(root
, elm
):
630 Return the first labelled parent of the element, which can thus be used as
631 the root of widgets with common labelled context
635 def find_labelled_parent(elm
):
636 if is_labelled_parent(elm
):
638 parent
= elm
.getparent()
641 return find_labelled_parent(parent
)
642 parent
= elm
.getparent()
645 return find_labelled_parent(elm
.getparent())
647 def find_labelled_parent(cur
, elm
):
649 # the target element is over there
652 parent
= find_labelled_parent(o
, elm
)
654 # target element is over there, check ourself
655 if is_labelled_parent(cur
):
656 # yes, and we are the first ancestor of the target element
659 # no, but target element is over there.
662 # the first ancestor of the target element was over there
665 parent
= find_labelled_parent(root
, elm
)
670 def is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, doprint
= False):
672 Check whether this label has no accessibility relation, or doubtful relation
673 because another label labels the same target
675 global label_for_elm
, labelled_by_elm
, mnemonic_for_elm
, warnexists
678 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
679 for rel
in label_for
:
680 target
= rel
.attrib
['target']
681 l
= label_for_elm
[target
]
686 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
687 obj
.findall("property[@name='mnemonic-widget']")
688 for rel
in mnemonic_for
:
690 l
= mnemonic_for_elm
[target
]
694 if len(label_for
) > 0:
695 # At least one label-for, we are not orphan.
698 if len(mnemonic_for
) > 0:
699 # At least one mnemonic_widget, we are not orphan.
702 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
703 if len(labelled_by
) > 0:
704 # Oh, a labelled label, probably not to be labelling anything
708 roles
= [x
.text
for x
in obj
.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
709 roles
+= [x
.attrib
.get("type") for x
in obj
.findall("accessibility/role")]
710 if len(roles
) > 1 and doprint
:
711 err(filename
, tree
, obj
, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
712 "%s" % elms_lines(children
))
714 if role
== 'static' or role
== 'ATK_ROLE_STATIC':
715 # This is static text, not meant to label anything
718 parent
= elm_parent(root
, obj
)
719 if parent
is not None:
720 childtype
= parent
.attrib
.get('type')
721 if childtype
is None:
722 childtype
= parent
.attrib
.get('internal-child')
723 if parent
.tag
== 'child' and childtype
== 'label' \
724 or childtype
== 'tab':
725 # This is a frame or a notebook label, not orphan.
728 if find_button_parent(root
, obj
) is not None:
729 # This label is part of a button
732 oid
= obj
.attrib
.get('id')
734 if oid
in labelled_by_elm
:
735 # Some widget is labelled by us, we are not orphan.
736 # We should have had a label-for, will warn about it later.
739 # No label-for, no mnemonic-for, no labelled-by, we are orphan.
740 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "orphan-label", False)
741 if suppr
in false_positives
:
742 # That was actually expected
744 if suppr
in suppressions
:
745 # Warning suppressed for this label
746 if suppressions
[suppr
]:
748 suppressions
[suppr
] = False
752 context
= elm_name(orphan_root
)
754 context
= " within " + context
755 warn(filename
, tree
, obj
, "orphan-label", "does not specify what it labels" + context
)
758 def is_orphan_widget(filename
, tree
, root
, obj
, orphan
, orphan_root
, doprint
= False):
760 Check whether this widget has no accessibility relation.
763 if obj
.tag
!= 'object':
766 oid
= obj
.attrib
.get('id')
767 klass
= obj
.attrib
.get('class')
769 # "Don't care" special case
770 if klass
in widgets_ignored
:
772 for suffix
in widgets_suffixignored
:
773 if klass
[-len(suffix
):] == suffix
:
776 # Widgets usual do not strictly require a label, i.e. a labelled parent
777 # is enough for context, but some do always need one.
778 requires_label
= klass
in widgets_needlabel
780 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
782 # Labels special case
783 if klass
in widgets_labels
:
786 # Case 1: has an explicit <child internal-child="accessible"> sub-element
787 children
= obj
.findall("child[@internal-child='accessible']")
788 if len(children
) > 1 and doprint
:
789 err(filename
, tree
, obj
, "multiple-accessible", "has multiple <child internal-child='accessible'>"
790 "%s" % elms_lines(children
))
791 if len(children
) >= 1:
794 # Case 2: has an <accessibility> sub-element with a "labelled-by"
795 # <relation> pointing to an existing element.
796 if len(labelled_by
) > 0:
799 # Case 3: has a label-for
800 if oid
in label_for_elm
:
803 # Case 4: has a mnemonic
804 if oid
in mnemonic_for_elm
:
807 # Case 5: Has a <property name="tooltip_text">
808 tooltips
= obj
.findall("property[@name='tooltip_text']") + \
809 obj
.findall("property[@name='tooltip-text']")
810 if len(tooltips
) > 1 and doprint
:
811 err(filename
, tree
, obj
, "multiple-tooltip", "has multiple tooltip_text properties")
812 if len(tooltips
) >= 1 and klass
!= 'GtkCheckButton':
815 # Case 6: Has a <property name="placeholder_text">
816 placeholders
= obj
.findall("property[@name='placeholder_text']") + \
817 obj
.findall("property[@name='placeholder-text']")
818 if len(placeholders
) > 1 and doprint
:
819 err(filename
, tree
, obj
, "multiple-placeholder", "has multiple placeholder_text properties")
820 if len(placeholders
) >= 1:
823 # Buttons usually don't need an external label, their own is enough, (but they do need one)
824 if klass
in widgets_buttons
:
826 labels
= obj
.findall("property[@name='label']")
827 if len(labels
) > 1 and doprint
:
828 err(filename
, tree
, obj
, "multiple-label", "has multiple label properties")
830 # Has a <property name="label">
833 actions
= obj
.findall("property[@name='action_name']")
834 if len(actions
) > 1 and doprint
:
835 err(filename
, tree
, obj
, "multiple-action_name", "has multiple action_name properties")
836 if len(actions
) >= 1:
837 # Has a <property name="action_name">
840 # Uses id as an action_name
841 if 'id' in obj
.attrib
:
842 if obj
.attrib
['id'].startswith(".uno:"):
845 gtklabels
= obj
.findall(".//object[@class='GtkLabel']") + obj
.findall(".//object[@class='GtkAccelLabel']")
846 if len(gtklabels
) >= 1:
850 # no label for a button, warn
852 warn(filename
, tree
, obj
, "button-no-label", "does not have its own label")
853 if not is_enabled(obj
, "button-no-label", enables
, True):
856 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "button-no-label", False)
857 if suppr
in false_positives
:
858 # That was actually expected
860 if suppr
in suppressions
:
861 # Warning suppressed for this widget
862 if suppressions
[suppr
]:
864 suppressions
[suppr
] = False
868 # GtkImages special case
869 if klass
== "GtkImage":
870 uses
= [u
for u
in tree
.iterfind(".//object/property[@name='image']") if u
.text
== oid
]
872 # This image is just used by another element, don't warn
873 # about the image itself, we probably want the warning on
874 # the element instead.
877 if find_button_parent(root
, obj
) is not None:
878 # This image is part of a button, we want the warning on the button
882 # GtkEntry special case
883 if klass
== 'GtkEntry' or klass
== 'GtkSearchEntry':
884 parent
= elm_parent(root
, obj
)
885 if parent
is not None:
886 if parent
.tag
== 'child' and \
887 parent
.attrib
.get('internal-child') == "entry":
888 # This is an internal entry of another widget. Relations
889 # will be handled by that widget.
892 # GtkShortcutsShortcut special case
893 if klass
== 'GtkShortcutsShortcut':
894 children
= obj
.findall("property[@name='title']")
895 if len(children
) >= 1:
898 # Really no label, perhaps emit a warning
899 if not is_enabled(obj
, "no-labelled-by", enables
, True):
900 # Warnings disabled for this class of widgets
902 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "no-labelled-by", False)
903 if suppr
in false_positives
:
904 # That was actually expected
906 if suppr
in suppressions
:
907 # Warning suppressed for this widget
908 if suppressions
[suppr
]:
910 suppressions
[suppr
] = False
914 # No orphan label, so probably the labelled parent provides enough
917 # But these always need a label.
919 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label")
924 context
= elm_name(orphan_root
)
926 context
= " within " + context
927 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label while there are orphan labels" + context
)
930 def orphan_items(filename
, tree
, root
, elm
):
932 Check whether from some element there exists orphan labels and orphan widgets
934 orphan_labels
= False
935 orphan_widgets
= False
936 if elm
.attrib
.get('class') in widgets_labels
:
937 orphan_labels
= is_orphan_label(filename
, tree
, root
, elm
, None)
939 orphan_widgets
= is_orphan_widget(filename
, tree
, root
, elm
, True, None)
941 # We are not interested in orphan labels under another labelled
942 # parent. This also allows to keep linear complexity.
943 if not is_labelled_parent(obj
):
944 label
, widget
= orphan_items(filename
, tree
, root
, obj
)
948 orphan_widgets
= True
949 if orphan_labels
and orphan_widgets
:
950 # No need to look up more
952 return orphan_labels
, orphan_widgets
955 # UI accessibility checks
958 def check_props(filename
, tree
, root
, elm
, forward
):
960 Check the given list of relation properties
962 props
= elm
.findall("property[@name='" + forward
+ "']")
964 if prop
.text
not in ids
:
965 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % prop
.text
)
970 visible_prop
= obj
.findall("property[@name='visible']")
971 visible_len
= len(visible_prop
)
973 visible_txt
= visible_prop
[visible_len
- 1].text
974 if visible_txt
.lower() == "true":
976 elif visible_txt
.lower() == "false":
980 def check_rels(filename
, tree
, root
, elm
, forward
, backward
= None):
982 Check the relations given by forward
984 oid
= elm
.attrib
.get('id')
985 rels
= elm
.findall("accessibility/relation[@type='" + forward
+ "']")
987 target
= rel
.attrib
['target']
988 if target
not in ids
:
989 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % target
)
990 elif backward
is not None:
992 backrels
= widget
.findall("accessibility/relation[@type='" + backward
+ "']")
993 if len([x
for x
in backrels
if x
.attrib
['target'] == oid
]) == 0:
994 err(filename
, tree
, elm
, "missing-" + backward
, "has " + forward
+ \
995 ", but is not " + backward
+ " by " + elm_name_line(widget
))
998 def check_a11y_relation(filename
, tree
):
1000 Emit an error message if any of the 'object' elements of the XML
1001 document represented by `root' doesn't comply with Accessibility
1004 global widgets_ignored
, ids
, label_for_elm
, labelled_by_elm
, mnemonic_for_elm
1006 def check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1008 Check one element, knowing that orphan_labels/widgets tell whether
1009 there are orphan labels and widgets within orphan_root
1012 oid
= obj
.attrib
.get('id')
1013 klass
= obj
.attrib
.get('class')
1015 # "Don't care" special case
1016 if klass
in widgets_ignored
:
1018 for suffix
in widgets_suffixignored
:
1019 if klass
[-len(suffix
):] == suffix
:
1022 # Widgets usual do not strictly require a label, i.e. a labelled parent
1023 # is enough for context, but some do always need one.
1024 requires_label
= klass
in widgets_needlabel
1027 # Check that ids are unique
1030 # We are the first, warn
1031 duplicates
= tree
.findall(".//object[@id='" + oid
+ "']")
1032 err(filename
, tree
, obj
, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates
))
1034 # Check label-for and their dual labelled-by
1035 label_for
= check_rels(filename
, tree
, root
, obj
, "label-for", "labelled-by")
1037 # Check labelled-by and its dual label-for
1038 labelled_by
= check_rels(filename
, tree
, root
, obj
, "labelled-by", "label-for")
1040 visible
= is_visible(obj
)
1042 # warning message type "syntax" used:
1044 # multiple-* => 2+ XML tags of the inspected element itself
1045 # duplicate-* => 2+ XML tags of other elements referencing this element
1047 # Should have only one label
1048 if len(labelled_by
) >= 1:
1049 if oid
in mnemonic_for_elm
:
1050 warn(filename
, tree
, obj
, "labelled-by-and-mnemonic",
1051 "has both a mnemonic " + elm_name_line(mnemonic_for_elm
[oid
][0]) + "and labelled-by relation")
1052 if len(labelled_by
) > 1:
1053 warn(filename
, tree
, obj
, "multiple-labelled-by", "has multiple labelled-by relations")
1055 if oid
in labelled_by_elm
:
1056 if len(labelled_by_elm
[oid
]) == 1:
1057 paired
= labelled_by_elm
[oid
][0]
1058 if paired
!= None and visible
!= is_visible(paired
):
1059 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1061 if oid
in label_for_elm
:
1062 if len(label_for_elm
[oid
]) > 1:
1063 warn(filename
, tree
, obj
, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm
[oid
]))
1064 elif len(label_for_elm
[oid
]) == 1:
1065 paired
= label_for_elm
[oid
][0]
1066 if visible
!= is_visible(paired
):
1067 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1069 if oid
in mnemonic_for_elm
:
1070 if len(mnemonic_for_elm
[oid
]) > 1:
1071 warn(filename
, tree
, obj
, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm
[oid
]))
1074 member_of
= check_rels(filename
, tree
, root
, obj
, "member-of")
1076 # Labels special case
1077 if klass
in widgets_labels
:
1078 properties
= check_props(filename
, tree
, root
, obj
, "mnemonic_widget") + \
1079 check_props(filename
, tree
, root
, obj
, "mnemonic-widget")
1080 if len(properties
) > 1:
1081 err(filename
, tree
, obj
, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
1082 "%s" % elms_lines(properties
))
1084 # Emit orphaning warnings
1085 if warn_orphan_labels
or orphan_widgets
:
1086 is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, True)
1088 # We are done with the label
1091 # Not a label, will perhaps need one
1093 # Emit orphaning warnings
1094 is_orphan_widget(filename
, tree
, root
, obj
, orphan_labels
, orphan_root
, True)
1096 root
= tree
.getroot()
1098 # Flush ids and relations from previous files
1101 labelled_by_elm
= {}
1103 mnemonic_for_elm
= {}
1105 # First pass to get links into hash tables, no warning, just record duplicates
1106 for obj
in root
.iter('object'):
1107 oid
= obj
.attrib
.get('id')
1114 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
1115 for rel
in labelled_by
:
1116 target
= rel
.attrib
.get('target')
1117 if target
is not None:
1118 if target
not in labelled_by_elm
:
1119 labelled_by_elm
[target
] = [ obj
]
1121 labelled_by_elm
[target
].append(obj
)
1123 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
1124 for rel
in label_for
:
1125 target
= rel
.attrib
.get('target')
1126 if target
is not None:
1127 if target
not in label_for_elm
:
1128 label_for_elm
[target
] = [ obj
]
1130 label_for_elm
[target
].append(obj
)
1132 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
1133 obj
.findall("property[@name='mnemonic-widget']")
1134 for rel
in mnemonic_for
:
1136 if target
is not None:
1137 if target
not in mnemonic_for_elm
:
1138 mnemonic_for_elm
[target
] = [ obj
]
1140 mnemonic_for_elm
[target
].append(obj
)
1142 # Second pass, recursive depth-first, to be able to efficiently know whether
1143 # there are orphan labels within a part of the tree.
1144 def recurse(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1145 if obj
== root
or is_labelled_parent(obj
):
1147 orphan_labels
, orphan_widgets
= orphan_items(filename
, tree
, root
, obj
)
1149 if obj
.tag
== 'object':
1150 check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
)
1153 recurse(orphan_root
, o
, orphan_labels
, orphan_widgets
)
1155 recurse(root
, root
, False, False)
1161 def usage(fatal
= True):
1162 print("`%s' checks accessibility of glade .ui files" % progname
)
1164 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname
)
1166 print(" -p Print XML class path instead of line number")
1167 print(" -g Generate suppression file SUPPR_FILE")
1168 print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
1169 print(" -f Suppress warnings given by file SUPPR_FILE completely")
1170 print(" -P Remove PREFIX from file names in warnings")
1171 print(" -o Also prints errors and warnings to given file")
1173 print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
1174 print(" Give or extend one of the lists of widget classes, where FOO can be:")
1175 print(" - toplevel : widgets to be considered toplevel windows")
1176 print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
1177 print(" - suffixignored : suffixes of widget classes which do not need labelling")
1178 print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
1179 print(" - buttons : widgets which need their own label but not more")
1180 print(" (e.g. GtkButton)")
1181 print(" - labels : widgets which provide labels (e.g. GtkLabel)")
1182 print(" --widgets-print print default widgets lists")
1184 print(" --enable-all enable all warnings/dofatals (default)")
1185 print(" --disable-all disable all warnings/dofatals")
1186 print(" --fatal-all make all warnings dofatals")
1187 print(" --not-fatal-all do not make all warnings dofatals (default)")
1189 print(" --enable-type=TYPE enable warning/fatal type TYPE")
1190 print(" --disable-type=TYPE disable warning/fatal type TYPE")
1191 print(" --fatal-type=TYPE make warning type TYPE a fatal")
1192 print(" --not-fatal-type=TYPE make warning type TYPE not a fatal")
1194 print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
1195 print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
1196 print(" --fatal-widgets=CLASS make warning type CLASS a fatal")
1197 print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal")
1199 print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
1200 print(" class CLASS")
1201 print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
1202 print(" class CLASS")
1203 print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget")
1204 print(" class CLASS")
1205 print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget")
1206 print(" class CLASS")
1208 print(" --disable-orphan-labels only warn about orphan labels when there are")
1209 print(" orphan widgets in the same context")
1211 print("Report bugs to <bugs@hypra.fr>")
1212 sys
.exit(2 if fatal
else 0)
1214 def widgets_opt(widgets_list
, arg
):
1216 Replace or extend `widgets_list' with the list of classes contained in `arg'
1218 append
= arg
and arg
[0] == '+'
1223 widgets
= arg
.split(',')
1230 widgets_list
.extend(widgets
)
1234 global pflag
, gen_suppr
, gen_supprfile
, suppressions
, suppr_prefix
, false_positives
, dofatals
, enables
, dofatals
, warn_orphan_labels
1235 global widgets_toplevel
, widgets_ignored
, widgets_suffixignored
, widgets_needlabel
, widgets_buttons
, widgets_labels
1236 global outfile
, output_buffer
1239 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hpiIg:s:f:P:o:L:", [
1243 "widgets-toplevel=",
1245 "widgets-suffixignored=",
1246 "widgets-needlabel=",
1264 "not-fatal-widgets=",
1267 "disable-specific=",
1269 "not-fatal-specific=",
1271 "disable-orphan-labels",
1273 except getopt
.GetoptError
:
1282 if o
== "--help" or o
== "-h":
1284 if o
== "--version":
1302 elif o
== "--widgets-toplevel":
1303 widgets_opt(widgets_toplevel
, a
)
1304 elif o
== "--widgets-ignored":
1305 widgets_opt(widgets_ignored
, a
)
1306 elif o
== "--widgets-suffixignored":
1307 widgets_opt(widgets_suffixignored
, a
)
1308 elif o
== "--widgets-needlabel":
1309 widgets_opt(widgets_needlabel
, a
)
1310 elif o
== "--widgets-buttons":
1311 widgets_opt(widgets_buttons
, a
)
1312 elif o
== "--widgets-labels":
1313 widgets_opt(widgets_labels
, a
)
1314 elif o
== "--widgets-print":
1315 print("--widgets-toplevel '" + ','.join(widgets_toplevel
) + "'")
1316 print("--widgets-ignored '" + ','.join(widgets_ignored
) + "'")
1317 print("--widgets-suffixignored '" + ','.join(widgets_suffixignored
) + "'")
1318 print("--widgets-needlabel '" + ','.join(widgets_needlabel
) + "'")
1319 print("--widgets-buttons '" + ','.join(widgets_buttons
) + "'")
1320 print("--widgets-labels '" + ','.join(widgets_labels
) + "'")
1323 elif o
== '--enable-all':
1324 enables
.append( (True, None, None) )
1325 elif o
== '--disable-all':
1326 enables
.append( (False, None, None) )
1327 elif o
== '--fatal-all':
1328 dofatals
.append( (True, None, None) )
1329 elif o
== '--not-fatal-all':
1330 dofatals
.append( (False, None, None) )
1332 elif o
== '--enable-type':
1333 enables
.append( (True, a
, None) )
1334 elif o
== '--disable-type':
1335 enables
.append( (False, a
, None) )
1336 elif o
== '--fatal-type':
1337 dofatals
.append( (True, a
, None) )
1338 elif o
== '--not-fatal-type':
1339 dofatals
.append( (False, a
, None) )
1341 elif o
== '--enable-widgets':
1342 enables
.append( (True, None, a
) )
1343 elif o
== '--disable-widgets':
1344 enables
.append( (False, None, a
) )
1345 elif o
== '--fatal-widgets':
1346 dofatals
.append( (True, None, a
) )
1347 elif o
== '--not-fatal-widgets':
1348 dofatals
.append( (False, None, a
) )
1350 elif o
== '--enable-specific':
1351 (thetype
, klass
) = a
.split('.', 1)
1352 enables
.append( (True, thetype
, klass
) )
1353 elif o
== '--disable-specific':
1354 (thetype
, klass
) = a
.split('.', 1)
1355 enables
.append( (False, thetype
, klass
) )
1356 elif o
== '--fatal-specific':
1357 (thetype
, klass
) = a
.split('.', 1)
1358 dofatals
.append( (True, thetype
, klass
) )
1359 elif o
== '--not-fatal-specific':
1360 (thetype
, klass
) = a
.split('.', 1)
1361 dofatals
.append( (False, thetype
, klass
) )
1363 elif o
== '--disable-orphan-labels':
1364 warn_orphan_labels
= False
1368 # Read suppression file before overwriting it
1369 if suppr
is not None:
1371 output_header
+= "Suppression file: " + suppr
+ "\n"
1372 supprfile
= open(suppr
, 'r')
1374 for line
in supprfile
.readlines():
1375 line_no
= line_no
+ 1
1376 if line
.startswith('#'):
1378 prefix
= line
.rstrip()
1379 suppressions
[prefix
] = True
1380 suppressions_to_line
[prefix
] = "%s:%u" % (suppr
, line_no
)
1385 # Read false positives file
1386 if false
is not None:
1388 output_header
+= "False positive file: " + false
+ "\n"
1389 falsefile
= open(false
, 'r')
1390 for line
in falsefile
.readlines():
1391 if line
.startswith('#'):
1393 prefix
= line
.rstrip()
1394 false_positives
[prefix
] = True
1400 outfile
= open(out
, 'w')
1402 if filelist
is not None:
1404 filelistfile
= open(filelist
, 'r')
1405 for line
in filelistfile
.readlines():
1408 args
+= line
.split(' ')
1409 filelistfile
.close()
1411 err(filelist
, None, None, "unable to read file list file")
1413 for filename
in args
:
1415 tree
= ET
.parse(filename
)
1416 except ET
.ParseError
:
1417 err(filename
, None, None, "parse", "malformatted xml file")
1420 err(filename
, None, None, None, "unable to read file")
1424 check_a11y_relation(filename
, tree
)
1425 except Exception as error
:
1427 output_buffer
+= traceback
.format_exc()
1428 err(filename
, None, None, "parse", "error parsing file")
1430 if errors
> 0 or errexists
> 0:
1431 output_buffer
+= "%s new error%s" % (errors
, 's' if errors
!= 1 else '')
1433 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (errexists
, suppr
, 'them' if errexists
> 1 else 'it')
1434 output_buffer
+= "\n"
1436 if warnings
> 0 or warnexists
> 0:
1437 output_buffer
+= "%s new warning%s" % (warnings
, 's' if warnings
!= 1 else '')
1439 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (warnexists
, suppr
, 'them' if warnexists
> 1 else 'it')
1440 output_buffer
+= "\n"
1442 if fatals
> 0 or fatalexists
> 0:
1443 output_buffer
+= "%s new fatal%s" % (fatals
, 's' if fatals
!= 1 else '')
1445 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (fatalexists
, suppr
, 'them' if fatalexists
> 1 else 'it')
1446 output_buffer
+= "\n"
1449 for (suppr
,unused
) in suppressions
.items():
1454 output_buffer
+= "%s suppression%s unused:\n" % (n
, 's' if n
!= 1 else '')
1455 for (suppr
,unused
) in suppressions
.items():
1457 output_buffer
+= " %s:%s\n" % (suppressions_to_line
[suppr
], suppr
)
1459 if gen_supprfile
is not None:
1460 gen_supprfile
.close()
1461 if outfile
is not None:
1464 if gen_suppr
is None:
1465 if output_buffer
!= "":
1466 output_buffer
+= "Explanations are available on " + howto_url
+ "\n"
1469 print(output_header
.rstrip() + "\n" + output_buffer
)
1472 if len(output_buffer
) > 0:
1473 print(output_header
.rstrip() + "\n" + output_buffer
)
1475 if __name__
== "__main__":
1478 except KeyboardInterrupt:
1481 # vim: set shiftwidth=4 softtabstop=4 expandtab: