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
+ [
96 'GtkShortcutsSection',
112 'GtkCellRendererGraph',
113 'GtkCellRendererPixbuf',
114 'GtkCellRendererProgress',
115 'GtkCellRendererSpin',
116 'GtkCellRendererText',
117 'GtkCellRendererToggle',
118 'GtkSeparatorMenuItem',
119 'GtkSeparatorToolItem',
124 'GtkTreeModelFilter',
136 'GtkEntryCompletion',
155 # These are actually labels
158 # This precisely give a11y information :)
162 widgets_suffixignored
= [
165 # These widgets always need a label
166 widgets_needlabel
= [
176 # These widgets normally have their own label
181 'GtkToggleToolButton',
183 'GtkRadioToolButton',
197 # These widgets are labels that can label other widgets
203 # The rest should probably be labelled if there are orphan labels
211 # GtkFileChooserButton
212 # GtkAppChooserButton
215 # GtkColorChooserWidget
234 # GtkFileChooserWidget ?
236 # GtkFontChooserWidget ?
243 # GtkPrinterOptionWidget ?
248 progname
= os
.path
.basename(sys
.argv
[0])
250 # This dictionary contains the set of suppression lines as read from the
251 # suppression file(s). It is merely indexed by the text of the suppression line
252 # and contains whether the suppressions was unused.
255 # This dictionary is indexed like suppressions and returns a "file:line" string
256 # to report where in the suppression file the suppression was read
257 suppressions_to_line
= {}
259 # This dictionary is similar to the suppressions dictionary, but for false
260 # positives rather than suppressions
263 # This dictionary is indexed by the xml id and returns the element object.
265 # This dictionary is indexed by the xml id and returns whether several objects
269 # This dictionary is indexed by the xml id of an element A and returns the list
270 # of objects which are labelled-by A.
273 # This dictionary is indexed by the xml id of an element A and returns the list
274 # of objects which are label-for A.
277 # This dictionary is indexed by the xml id of an element A and returns the list
278 # of objects which have a mnemonic-for A.
279 mnemonic_for_elm
= {}
281 # Possibly a file name to put generated suppression lines in
283 # The corresponding opened file
285 # A prefix to remove from file names in the generated suppression lines
288 # Possibly an opened file in which our output should also be written to.
291 # Whether -p option was set, i.e. print XML class path instead of line number in
295 # Whether we should warn about labels which are orphan
296 warn_orphan_labels
= True
300 # Number of suppressed errors
304 # Number of suppressed warnings
306 # Number of fatal errors
308 # Number of suppressed fatal errors
311 # List of warnings and errors which are fatal
313 # Format of each element: (enabled, type, class)
314 # See the is_enabled function: the list is traversed completely, each element
315 # can specify whether it enables or disables the warning, possibly the type of
316 # warning to be enabled/disabled, possibly the class of XML element for which it
319 # This mechanism matches the semantic of the parameters on the command line,
320 # each of which refining the semantic set by the previous parameters
323 # List of warnings and errors which are enabled
324 # Same format as dofatals
327 # buffers all printed output, so it isn't split in parallel builds
331 # XML browsing and printing functions
334 def elm_parent(root
, elm
):
336 Return the parent of the element.
339 return elm
.getparent()
341 def find_parent(cur
, elm
):
345 parent
= find_parent(o
, elm
)
346 if parent
is not None:
349 return find_parent(root
, elm
)
353 Return the XML class path step corresponding to elm.
354 This can be empty if the elm does not have any class or id.
356 step
= elm
.attrib
.get('class')
359 oid
= elm
.attrib
.get('id')
361 oid
= oid
.encode('ascii','ignore').decode('ascii')
362 step
+= "[@id='%s']" % oid
367 def find_elm(root
, elm
):
369 Return the XML class path of the element from the given root.
370 This is the slow version used when getparent is not available.
375 path
= find_elm(o
, elm
)
381 def errpath(filename
, tree
, elm
):
383 Return the XML class path of the element
388 if 'class' in elm
.attrib
:
389 path
+= elm
.attrib
['class']
390 oid
= elm
.attrib
.get('id')
392 oid
= oid
.encode('ascii','ignore').decode('ascii')
393 path
= "//" + path
+ "[@id='%s']" % oid
396 elm
= elm
.getparent()
397 while elm
is not None:
400 elm
= elm
.getparent()
402 path
= find_elm(tree
.getroot(), elm
)[:-1]
403 path
= filename
+ ':' + path
407 # Warning/Error printing functions
410 def elm_prefix(filename
, elm
):
412 Return the display prefix of the element
414 if elm
== None or not lxml
:
415 return "%s:" % filename
417 return "%s:%u" % (filename
, elm
.sourceline
)
421 Return a display name of the element
425 if 'class' in elm
.attrib
:
426 name
= "'%s' " % elm
.attrib
['class']
427 if 'id' in elm
.attrib
:
428 id = elm
.attrib
['id'].encode('ascii','ignore').decode('ascii')
431 name
= "'" + elm
.tag
+ "'"
433 name
+= " line " + str(elm
.sourceline
)
437 def elm_name_line(elm
):
439 Return a display name of the element with line number
443 if lxml
and " line " not in name
:
444 name
+= "line " + str(elm
.sourceline
) + " "
450 Return the line for the given element.
453 return " line " + str(elm
.sourceline
)
457 def elms_lines(elms
):
459 Return the list of lines for the given elements.
462 return " lines " + ', '.join([str(l
.sourceline
) for l
in elms
])
466 def elms_names_lines(elms
):
468 Return the list of names and lines for the given elements.
470 return ', '.join([elm_name_line(elm
) for elm
in elms
])
472 def elm_suppr(filename
, tree
, elm
, msgtype
, dogen
):
474 Return the prefix to be displayed to the user and the suppression line for
475 the warning type "msgtype" for element "elm"
477 global gen_suppr
, gen_supprfile
, suppr_prefix
, pflag
479 if suppressions
or false_positives
or gen_suppr
is not None or pflag
:
480 prefix
= errpath(filename
, tree
, elm
)
481 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
482 prefix
= prefix
[len(suppr_prefix
):]
484 if suppressions
or false_positives
or gen_suppr
is not None:
485 suppr
= '%s %s' % (prefix
, msgtype
)
487 if gen_suppr
is not None and msgtype
is not None and dogen
:
488 if gen_supprfile
is None:
489 gen_supprfile
= open(gen_suppr
, 'w')
490 print(suppr
, file=gen_supprfile
)
495 # Use user-friendly line numbers
496 prefix
= elm_prefix(filename
, elm
)
497 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
498 prefix
= prefix
[len(suppr_prefix
):]
500 return (prefix
, suppr
)
502 def is_enabled(elm
, msgtype
, l
, default
):
504 Test whether warning type msgtype is enabled for elm in l
507 for (enable
, thetype
, klass
) in l
:
509 if thetype
is not None:
510 if thetype
!= msgtype
:
513 if klass
is not None and elm
is not None:
514 if klass
!= elm
.attrib
.get('class'):
519 def err(filename
, tree
, elm
, msgtype
, msg
, error
= True):
521 Emit a warning or error for an element
523 global errors
, errexists
, warnings
, warnexists
, fatals
, fatalexists
, output_buffer
525 # Let user tune whether a warning or error
526 fatal
= is_enabled(elm
, msgtype
, dofatals
, error
)
528 # By default warnings and errors are enabled, but let user tune it
529 if not is_enabled(elm
, msgtype
, enables
, True):
532 (prefix
, suppr
) = elm_suppr(filename
, tree
, elm
, msgtype
, True)
533 if suppr
in false_positives
:
534 # That was actually expected
536 if suppr
in suppressions
:
538 suppressions
[suppr
] = False
554 msg
= "%s %s%s: %s%s" % (prefix
,
555 "FATAL " if fatal
else "",
556 "ERROR" if error
else "WARNING",
558 output_buffer
+= msg
+ "\n"
559 if outfile
is not None:
560 print(msg
, file=outfile
)
562 def warn(filename
, tree
, elm
, msgtype
, msg
):
564 Emit a warning for an element
566 err(filename
, tree
, elm
, msgtype
, msg
, False)
569 # Labelling testing functions
572 def find_button_parent(root
, elm
):
574 Find a parent which is a button
577 parent
= elm
.getparent()
578 if parent
is not None:
579 if parent
.attrib
.get('class') in widgets_buttons
:
581 return find_button_parent(root
, parent
)
583 def find_parent(cur
, elm
):
586 if cur
.attrib
.get('class') in widgets_buttons
:
587 # we are the button, immediately above the target
590 # we aren't the button, but target is over there
592 parent
= find_parent(o
, elm
)
594 # It is over there, but didn't find a button yet
595 if cur
.attrib
.get('class') in widgets_buttons
:
600 if parent
is not None:
601 # we have the button parent over there
604 parent
= find_parent(root
, elm
)
610 def is_labelled_parent(elm
):
612 Return whether this element is a labelled parent
614 klass
= elm
.attrib
.get('class')
615 if klass
in widgets_toplevel
:
617 if klass
== 'GtkShortcutsGroup':
618 children
= elm
.findall("property[@name='title']")
619 if len(children
) >= 1:
621 if klass
== 'GtkFrame' or klass
== 'GtkNotebook':
622 children
= elm
.findall("child[@type='tab']") + elm
.findall("child[@type='label']")
623 if len(children
) >= 1:
627 def elm_labelled_parent(root
, elm
):
629 Return the first labelled parent of the element, which can thus be used as
630 the root of widgets with common labelled context
634 def find_labelled_parent(elm
):
635 if is_labelled_parent(elm
):
637 parent
= elm
.getparent()
640 return find_labelled_parent(parent
)
641 parent
= elm
.getparent()
644 return find_labelled_parent(elm
.getparent())
646 def find_labelled_parent(cur
, elm
):
648 # the target element is over there
651 parent
= find_labelled_parent(o
, elm
)
653 # target element is over there, check ourself
654 if is_labelled_parent(cur
):
655 # yes, and we are the first ancestor of the target element
658 # no, but target element is over there.
661 # the first ancestor of the target element was over there
664 parent
= find_labelled_parent(root
, elm
)
669 def is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, doprint
= False):
671 Check whether this label has no accessibility relation, or doubtful relation
672 because another label labels the same target
674 global label_for_elm
, labelled_by_elm
, mnemonic_for_elm
, warnexists
677 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
678 for rel
in label_for
:
679 target
= rel
.attrib
['target']
680 l
= label_for_elm
[target
]
685 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
686 obj
.findall("property[@name='mnemonic-widget']")
687 for rel
in mnemonic_for
:
689 l
= mnemonic_for_elm
[target
]
693 if len(label_for
) > 0:
694 # At least one label-for, we are not orphan.
697 if len(mnemonic_for
) > 0:
698 # At least one mnemonic_widget, we are not orphan.
701 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
702 if len(labelled_by
) > 0:
703 # Oh, a labelled label, probably not to be labelling anything
707 roles
= [x
.text
for x
in obj
.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
708 roles
+= [x
.attrib
.get("type") for x
in obj
.findall("accessibility/role")]
709 if len(roles
) > 1 and doprint
:
710 err(filename
, tree
, obj
, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
711 "%s" % elms_lines(children
))
713 if role
== 'static' or role
== 'ATK_ROLE_STATIC':
714 # This is static text, not meant to label anything
717 parent
= elm_parent(root
, obj
)
718 if parent
is not None:
719 childtype
= parent
.attrib
.get('type')
720 if childtype
is None:
721 childtype
= parent
.attrib
.get('internal-child')
722 if parent
.tag
== 'child' and childtype
== 'label' \
723 or childtype
== 'tab':
724 # This is a frame or a notebook label, not orphan.
727 if find_button_parent(root
, obj
) is not None:
728 # This label is part of a button
731 oid
= obj
.attrib
.get('id')
733 if oid
in labelled_by_elm
:
734 # Some widget is labelled by us, we are not orphan.
735 # We should have had a label-for, will warn about it later.
738 # No label-for, no mnemonic-for, no labelled-by, we are orphan.
739 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "orphan-label", False)
740 if suppr
in false_positives
:
741 # That was actually expected
743 if suppr
in suppressions
:
744 # Warning suppressed for this label
745 if suppressions
[suppr
]:
747 suppressions
[suppr
] = False
751 context
= elm_name(orphan_root
)
753 context
= " within " + context
754 warn(filename
, tree
, obj
, "orphan-label", "does not specify what it labels" + context
)
757 def is_orphan_widget(filename
, tree
, root
, obj
, orphan
, orphan_root
, doprint
= False):
759 Check whether this widget has no accessibility relation.
762 if obj
.tag
!= 'object':
765 oid
= obj
.attrib
.get('id')
766 klass
= obj
.attrib
.get('class')
768 # "Don't care" special case
769 if klass
in widgets_ignored
:
771 for suffix
in widgets_suffixignored
:
772 if klass
[-len(suffix
):] == suffix
:
775 # Widgets usual do not strictly require a label, i.e. a labelled parent
776 # is enough for context, but some do always need one.
777 requires_label
= klass
in widgets_needlabel
779 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
781 # Labels special case
782 if klass
in widgets_labels
:
785 # Case 1: has an explicit <child internal-child="accessible"> sub-element
786 children
= obj
.findall("child[@internal-child='accessible']")
787 if len(children
) > 1 and doprint
:
788 err(filename
, tree
, obj
, "multiple-accessible", "has multiple <child internal-child='accessible'>"
789 "%s" % elms_lines(children
))
790 if len(children
) >= 1:
793 # Case 2: has an <accessibility> sub-element with a "labelled-by"
794 # <relation> pointing to an existing element.
795 if len(labelled_by
) > 0:
798 # Case 3: has a label-for
799 if oid
in label_for_elm
:
802 # Case 4: has a mnemonic
803 if oid
in mnemonic_for_elm
:
806 # Case 5: Has a <property name="tooltip_text">
807 tooltips
= obj
.findall("property[@name='tooltip_text']") + \
808 obj
.findall("property[@name='tooltip-text']")
809 if len(tooltips
) > 1 and doprint
:
810 err(filename
, tree
, obj
, "multiple-tooltip", "has multiple tooltip_text properties")
811 if len(tooltips
) >= 1 and klass
!= 'GtkCheckButton':
814 # Case 6: Has a <property name="placeholder_text">
815 placeholders
= obj
.findall("property[@name='placeholder_text']") + \
816 obj
.findall("property[@name='placeholder-text']")
817 if len(placeholders
) > 1 and doprint
:
818 err(filename
, tree
, obj
, "multiple-placeholder", "has multiple placeholder_text properties")
819 if len(placeholders
) >= 1:
822 # Buttons usually don't need an external label, their own is enough, (but they do need one)
823 if klass
in widgets_buttons
:
825 labels
= obj
.findall("property[@name='label']")
826 if len(labels
) > 1 and doprint
:
827 err(filename
, tree
, obj
, "multiple-label", "has multiple label properties")
829 # Has a <property name="label">
832 actions
= obj
.findall("property[@name='action_name']")
833 if len(actions
) > 1 and doprint
:
834 err(filename
, tree
, obj
, "multiple-action_name", "has multiple action_name properties")
835 if len(actions
) >= 1:
836 # Has a <property name="action_name">
839 # Uses id as an action_name
840 if 'id' in obj
.attrib
:
841 if obj
.attrib
['id'].startswith(".uno:"):
844 gtklabels
= obj
.findall(".//object[@class='GtkLabel']") + obj
.findall(".//object[@class='GtkAccelLabel']")
845 if len(gtklabels
) >= 1:
849 # no label for a button, warn
851 warn(filename
, tree
, obj
, "button-no-label", "does not have its own label")
852 if not is_enabled(obj
, "button-no-label", enables
, True):
855 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "button-no-label", False)
856 if suppr
in false_positives
:
857 # That was actually expected
859 if suppr
in suppressions
:
860 # Warning suppressed for this widget
861 if suppressions
[suppr
]:
863 suppressions
[suppr
] = False
867 # GtkImages special case
868 if klass
== "GtkImage":
869 uses
= [u
for u
in tree
.iterfind(".//object/property[@name='image']") if u
.text
== oid
]
871 # This image is just used by another element, don't warn
872 # about the image itself, we probably want the warning on
873 # the element instead.
876 if find_button_parent(root
, obj
) is not None:
877 # This image is part of a button, we want the warning on the button
881 # GtkEntry special case
882 if klass
== 'GtkEntry' or klass
== 'GtkSearchEntry':
883 parent
= elm_parent(root
, obj
)
884 if parent
is not None:
885 if parent
.tag
== 'child' and \
886 parent
.attrib
.get('internal-child') == "entry":
887 # This is an internal entry of another widget. Relations
888 # will be handled by that widget.
891 # GtkShortcutsShortcut special case
892 if klass
== 'GtkShortcutsShortcut':
893 children
= obj
.findall("property[@name='title']")
894 if len(children
) >= 1:
897 # Really no label, perhaps emit a warning
898 if not is_enabled(obj
, "no-labelled-by", enables
, True):
899 # Warnings disabled for this class of widgets
901 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "no-labelled-by", False)
902 if suppr
in false_positives
:
903 # That was actually expected
905 if suppr
in suppressions
:
906 # Warning suppressed for this widget
907 if suppressions
[suppr
]:
909 suppressions
[suppr
] = False
913 # No orphan label, so probably the labelled parent provides enough
916 # But these always need a label.
918 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label")
923 context
= elm_name(orphan_root
)
925 context
= " within " + context
926 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label while there are orphan labels" + context
)
929 def orphan_items(filename
, tree
, root
, elm
):
931 Check whether from some element there exists orphan labels and orphan widgets
933 orphan_labels
= False
934 orphan_widgets
= False
935 if elm
.attrib
.get('class') in widgets_labels
:
936 orphan_labels
= is_orphan_label(filename
, tree
, root
, elm
, None)
938 orphan_widgets
= is_orphan_widget(filename
, tree
, root
, elm
, True, None)
940 # We are not interested in orphan labels under another labelled
941 # parent. This also allows to keep linear complexity.
942 if not is_labelled_parent(obj
):
943 label
, widget
= orphan_items(filename
, tree
, root
, obj
)
947 orphan_widgets
= True
948 if orphan_labels
and orphan_widgets
:
949 # No need to look up more
951 return orphan_labels
, orphan_widgets
954 # UI accessibility checks
957 def check_props(filename
, tree
, root
, elm
, forward
):
959 Check the given list of relation properties
961 props
= elm
.findall("property[@name='" + forward
+ "']")
963 if prop
.text
not in ids
:
964 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % prop
.text
)
969 visible_prop
= obj
.findall("property[@name='visible']")
970 visible_len
= len(visible_prop
)
972 visible_txt
= visible_prop
[visible_len
- 1].text
973 if visible_txt
.lower() == "true":
975 elif visible_txt
.lower() == "false":
979 def check_rels(filename
, tree
, root
, elm
, forward
, backward
= None):
981 Check the relations given by forward
983 oid
= elm
.attrib
.get('id')
984 rels
= elm
.findall("accessibility/relation[@type='" + forward
+ "']")
986 target
= rel
.attrib
['target']
987 if target
not in ids
:
988 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % target
)
989 elif backward
is not None:
991 backrels
= widget
.findall("accessibility/relation[@type='" + backward
+ "']")
992 if len([x
for x
in backrels
if x
.attrib
['target'] == oid
]) == 0:
993 err(filename
, tree
, elm
, "missing-" + backward
, "has " + forward
+ \
994 ", but is not " + backward
+ " by " + elm_name_line(widget
))
997 def check_a11y_relation(filename
, tree
):
999 Emit an error message if any of the 'object' elements of the XML
1000 document represented by `root' doesn't comply with Accessibility
1003 global widgets_ignored
, ids
, label_for_elm
, labelled_by_elm
, mnemonic_for_elm
1005 def check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1007 Check one element, knowing that orphan_labels/widgets tell whether
1008 there are orphan labels and widgets within orphan_root
1011 oid
= obj
.attrib
.get('id')
1012 klass
= obj
.attrib
.get('class')
1014 # "Don't care" special case
1015 if klass
in widgets_ignored
:
1017 for suffix
in widgets_suffixignored
:
1018 if klass
[-len(suffix
):] == suffix
:
1021 # Widgets usual do not strictly require a label, i.e. a labelled parent
1022 # is enough for context, but some do always need one.
1023 requires_label
= klass
in widgets_needlabel
1026 # Check that ids are unique
1029 # We are the first, warn
1030 duplicates
= tree
.findall(".//object[@id='" + oid
+ "']")
1031 err(filename
, tree
, obj
, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates
))
1033 # Check label-for and their dual labelled-by
1034 label_for
= check_rels(filename
, tree
, root
, obj
, "label-for", "labelled-by")
1036 # Check labelled-by and its dual label-for
1037 labelled_by
= check_rels(filename
, tree
, root
, obj
, "labelled-by", "label-for")
1039 visible
= is_visible(obj
)
1041 # warning message type "syntax" used:
1043 # multiple-* => 2+ XML tags of the inspected element itself
1044 # duplicate-* => 2+ XML tags of other elements referencing this element
1046 # Should have only one label
1047 if len(labelled_by
) >= 1:
1048 if oid
in mnemonic_for_elm
:
1049 warn(filename
, tree
, obj
, "labelled-by-and-mnemonic",
1050 "has both a mnemonic " + elm_name_line(mnemonic_for_elm
[oid
][0]) + "and labelled-by relation")
1051 if len(labelled_by
) > 1:
1052 warn(filename
, tree
, obj
, "multiple-labelled-by", "has multiple labelled-by relations")
1054 if oid
in labelled_by_elm
:
1055 if len(labelled_by_elm
[oid
]) == 1:
1056 paired
= labelled_by_elm
[oid
][0]
1057 if paired
!= None and visible
!= is_visible(paired
):
1058 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1060 if oid
in label_for_elm
:
1061 if len(label_for_elm
[oid
]) > 1:
1062 warn(filename
, tree
, obj
, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm
[oid
]))
1063 elif len(label_for_elm
[oid
]) == 1:
1064 paired
= label_for_elm
[oid
][0]
1065 if visible
!= is_visible(paired
):
1066 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1068 if oid
in mnemonic_for_elm
:
1069 if len(mnemonic_for_elm
[oid
]) > 1:
1070 warn(filename
, tree
, obj
, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm
[oid
]))
1073 member_of
= check_rels(filename
, tree
, root
, obj
, "member-of")
1075 # Labels special case
1076 if klass
in widgets_labels
:
1077 properties
= check_props(filename
, tree
, root
, obj
, "mnemonic_widget") + \
1078 check_props(filename
, tree
, root
, obj
, "mnemonic-widget")
1079 if len(properties
) > 1:
1080 err(filename
, tree
, obj
, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
1081 "%s" % elms_lines(properties
))
1083 # Emit orphaning warnings
1084 if warn_orphan_labels
or orphan_widgets
:
1085 is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, True)
1087 # We are done with the label
1090 # Not a label, will perhaps need one
1092 # Emit orphaning warnings
1093 is_orphan_widget(filename
, tree
, root
, obj
, orphan_labels
, orphan_root
, True)
1095 root
= tree
.getroot()
1097 # Flush ids and relations from previous files
1100 labelled_by_elm
= {}
1102 mnemonic_for_elm
= {}
1104 # First pass to get links into hash tables, no warning, just record duplicates
1105 for obj
in root
.iter('object'):
1106 oid
= obj
.attrib
.get('id')
1113 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
1114 for rel
in labelled_by
:
1115 target
= rel
.attrib
.get('target')
1116 if target
is not None:
1117 if target
not in labelled_by_elm
:
1118 labelled_by_elm
[target
] = [ obj
]
1120 labelled_by_elm
[target
].append(obj
)
1122 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
1123 for rel
in label_for
:
1124 target
= rel
.attrib
.get('target')
1125 if target
is not None:
1126 if target
not in label_for_elm
:
1127 label_for_elm
[target
] = [ obj
]
1129 label_for_elm
[target
].append(obj
)
1131 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
1132 obj
.findall("property[@name='mnemonic-widget']")
1133 for rel
in mnemonic_for
:
1135 if target
is not None:
1136 if target
not in mnemonic_for_elm
:
1137 mnemonic_for_elm
[target
] = [ obj
]
1139 mnemonic_for_elm
[target
].append(obj
)
1141 # Second pass, recursive depth-first, to be able to efficiently know whether
1142 # there are orphan labels within a part of the tree.
1143 def recurse(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1144 if obj
== root
or is_labelled_parent(obj
):
1146 orphan_labels
, orphan_widgets
= orphan_items(filename
, tree
, root
, obj
)
1148 if obj
.tag
== 'object':
1149 check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
)
1152 recurse(orphan_root
, o
, orphan_labels
, orphan_widgets
)
1154 recurse(root
, root
, False, False)
1160 def usage(fatal
= True):
1161 print("`%s' checks accessibility of glade .ui files" % progname
)
1163 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname
)
1165 print(" -p Print XML class path instead of line number")
1166 print(" -g Generate suppression file SUPPR_FILE")
1167 print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
1168 print(" -f Suppress warnings given by file SUPPR_FILE completely")
1169 print(" -P Remove PREFIX from file names in warnings")
1170 print(" -o Also prints errors and warnings to given file")
1172 print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
1173 print(" Give or extend one of the lists of widget classes, where FOO can be:")
1174 print(" - toplevel : widgets to be considered toplevel windows")
1175 print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
1176 print(" - suffixignored : suffixes of widget classes which do not need labelling")
1177 print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
1178 print(" - buttons : widgets which need their own label but not more")
1179 print(" (e.g. GtkButton)")
1180 print(" - labels : widgets which provide labels (e.g. GtkLabel)")
1181 print(" --widgets-print print default widgets lists")
1183 print(" --enable-all enable all warnings/dofatals (default)")
1184 print(" --disable-all disable all warnings/dofatals")
1185 print(" --fatal-all make all warnings dofatals")
1186 print(" --not-fatal-all do not make all warnings dofatals (default)")
1188 print(" --enable-type=TYPE enable warning/fatal type TYPE")
1189 print(" --disable-type=TYPE disable warning/fatal type TYPE")
1190 print(" --fatal-type=TYPE make warning type TYPE a fatal")
1191 print(" --not-fatal-type=TYPE make warning type TYPE not a fatal")
1193 print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
1194 print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
1195 print(" --fatal-widgets=CLASS make warning type CLASS a fatal")
1196 print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal")
1198 print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
1199 print(" class CLASS")
1200 print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
1201 print(" class CLASS")
1202 print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget")
1203 print(" class CLASS")
1204 print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget")
1205 print(" class CLASS")
1207 print(" --disable-orphan-labels only warn about orphan labels when there are")
1208 print(" orphan widgets in the same context")
1210 print("Report bugs to <bugs@hypra.fr>")
1211 sys
.exit(2 if fatal
else 0)
1213 def widgets_opt(widgets_list
, arg
):
1215 Replace or extend `widgets_list' with the list of classes contained in `arg'
1217 append
= arg
and arg
[0] == '+'
1222 widgets
= arg
.split(',')
1229 widgets_list
.extend(widgets
)
1233 global pflag
, gen_suppr
, gen_supprfile
, suppressions
, suppr_prefix
, false_positives
, dofatals
, enables
, dofatals
, warn_orphan_labels
1234 global widgets_toplevel
, widgets_ignored
, widgets_suffixignored
, widgets_needlabel
, widgets_buttons
, widgets_labels
1235 global outfile
, output_buffer
1238 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hpiIg:s:f:P:o:L:", [
1242 "widgets-toplevel=",
1244 "widgets-suffixignored=",
1245 "widgets-needlabel=",
1263 "not-fatal-widgets=",
1266 "disable-specific=",
1268 "not-fatal-specific=",
1270 "disable-orphan-labels",
1272 except getopt
.GetoptError
:
1281 if o
== "--help" or o
== "-h":
1283 if o
== "--version":
1301 elif o
== "--widgets-toplevel":
1302 widgets_opt(widgets_toplevel
, a
)
1303 elif o
== "--widgets-ignored":
1304 widgets_opt(widgets_ignored
, a
)
1305 elif o
== "--widgets-suffixignored":
1306 widgets_opt(widgets_suffixignored
, a
)
1307 elif o
== "--widgets-needlabel":
1308 widgets_opt(widgets_needlabel
, a
)
1309 elif o
== "--widgets-buttons":
1310 widgets_opt(widgets_buttons
, a
)
1311 elif o
== "--widgets-labels":
1312 widgets_opt(widgets_labels
, a
)
1313 elif o
== "--widgets-print":
1314 print("--widgets-toplevel '" + ','.join(widgets_toplevel
) + "'")
1315 print("--widgets-ignored '" + ','.join(widgets_ignored
) + "'")
1316 print("--widgets-suffixignored '" + ','.join(widgets_suffixignored
) + "'")
1317 print("--widgets-needlabel '" + ','.join(widgets_needlabel
) + "'")
1318 print("--widgets-buttons '" + ','.join(widgets_buttons
) + "'")
1319 print("--widgets-labels '" + ','.join(widgets_labels
) + "'")
1322 elif o
== '--enable-all':
1323 enables
.append( (True, None, None) )
1324 elif o
== '--disable-all':
1325 enables
.append( (False, None, None) )
1326 elif o
== '--fatal-all':
1327 dofatals
.append( (True, None, None) )
1328 elif o
== '--not-fatal-all':
1329 dofatals
.append( (False, None, None) )
1331 elif o
== '--enable-type':
1332 enables
.append( (True, a
, None) )
1333 elif o
== '--disable-type':
1334 enables
.append( (False, a
, None) )
1335 elif o
== '--fatal-type':
1336 dofatals
.append( (True, a
, None) )
1337 elif o
== '--not-fatal-type':
1338 dofatals
.append( (False, a
, None) )
1340 elif o
== '--enable-widgets':
1341 enables
.append( (True, None, a
) )
1342 elif o
== '--disable-widgets':
1343 enables
.append( (False, None, a
) )
1344 elif o
== '--fatal-widgets':
1345 dofatals
.append( (True, None, a
) )
1346 elif o
== '--not-fatal-widgets':
1347 dofatals
.append( (False, None, a
) )
1349 elif o
== '--enable-specific':
1350 (thetype
, klass
) = a
.split('.', 1)
1351 enables
.append( (True, thetype
, klass
) )
1352 elif o
== '--disable-specific':
1353 (thetype
, klass
) = a
.split('.', 1)
1354 enables
.append( (False, thetype
, klass
) )
1355 elif o
== '--fatal-specific':
1356 (thetype
, klass
) = a
.split('.', 1)
1357 dofatals
.append( (True, thetype
, klass
) )
1358 elif o
== '--not-fatal-specific':
1359 (thetype
, klass
) = a
.split('.', 1)
1360 dofatals
.append( (False, thetype
, klass
) )
1362 elif o
== '--disable-orphan-labels':
1363 warn_orphan_labels
= False
1367 # Read suppression file before overwriting it
1368 if suppr
is not None:
1370 output_header
+= "Suppression file: " + suppr
+ "\n"
1371 supprfile
= open(suppr
, 'r')
1373 for line
in supprfile
.readlines():
1374 line_no
= line_no
+ 1
1375 if line
.startswith('#'):
1377 prefix
= line
.rstrip()
1378 suppressions
[prefix
] = True
1379 suppressions_to_line
[prefix
] = "%s:%u" % (suppr
, line_no
)
1384 # Read false positives file
1385 if false
is not None:
1387 output_header
+= "False positive file: " + false
+ "\n"
1388 falsefile
= open(false
, 'r')
1389 for line
in falsefile
.readlines():
1390 if line
.startswith('#'):
1392 prefix
= line
.rstrip()
1393 false_positives
[prefix
] = True
1399 outfile
= open(out
, 'w')
1401 if filelist
is not None:
1403 filelistfile
= open(filelist
, 'r')
1404 for line
in filelistfile
.readlines():
1407 args
+= line
.split(' ')
1408 filelistfile
.close()
1410 err(filelist
, None, None, "unable to read file list file")
1412 for filename
in args
:
1414 tree
= ET
.parse(filename
)
1415 except ET
.ParseError
:
1416 err(filename
, None, None, "parse", "malformatted xml file")
1419 err(filename
, None, None, None, "unable to read file")
1423 check_a11y_relation(filename
, tree
)
1424 except Exception as error
:
1426 output_buffer
+= traceback
.format_exc()
1427 err(filename
, None, None, "parse", "error parsing file")
1429 if errors
> 0 or errexists
> 0:
1430 output_buffer
+= "%s new error%s" % (errors
, 's' if errors
!= 1 else '')
1432 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (errexists
, suppr
, 'them' if errexists
> 1 else 'it')
1433 output_buffer
+= "\n"
1435 if warnings
> 0 or warnexists
> 0:
1436 output_buffer
+= "%s new warning%s" % (warnings
, 's' if warnings
!= 1 else '')
1438 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (warnexists
, suppr
, 'them' if warnexists
> 1 else 'it')
1439 output_buffer
+= "\n"
1441 if fatals
> 0 or fatalexists
> 0:
1442 output_buffer
+= "%s new fatal%s" % (fatals
, 's' if fatals
!= 1 else '')
1444 output_buffer
+= " (%s suppressed by %s, please fix %s)" % (fatalexists
, suppr
, 'them' if fatalexists
> 1 else 'it')
1445 output_buffer
+= "\n"
1448 for (suppr
,unused
) in suppressions
.items():
1453 output_buffer
+= "%s suppression%s unused:\n" % (n
, 's' if n
!= 1 else '')
1454 for (suppr
,unused
) in suppressions
.items():
1456 output_buffer
+= " %s:%s\n" % (suppressions_to_line
[suppr
], suppr
)
1458 if gen_supprfile
is not None:
1459 gen_supprfile
.close()
1460 if outfile
is not None:
1463 if gen_suppr
is None:
1464 if output_buffer
!= "":
1465 output_buffer
+= "Explanations are available on " + howto_url
+ "\n"
1468 print(output_header
.rstrip() + "\n" + output_buffer
)
1471 if len(output_buffer
) > 0:
1472 print(output_header
.rstrip() + "\n" + output_buffer
)
1474 if __name__
== "__main__":
1477 except KeyboardInterrupt:
1480 # vim: set shiftwidth=4 softtabstop=4 expandtab: