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 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
48 'GtkApplicationWindow',
51 'GtkFileChooserDialog',
52 'GtkColorChooserDialog',
53 'GtkFontChooserDialog',
55 'GtkRecentChooserDialog',
57 'GtkAppChooserDialog',
62 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',
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
235 # GtkFileChooserWidget ?
237 # GtkFontChooserWidget ?
244 # GtkPrinterOptionWidget ?
249 progname
= os
.path
.basename(sys
.argv
[0])
257 mnemonic_for_elm
= {}
266 warn_orphan_labels
= True
279 # XML browsing and printing functions
282 def elm_parent(root
, elm
):
284 Return the parent of the element.
287 return elm
.getparent()
289 def find_parent(cur
, elm
):
293 parent
= find_parent(o
, elm
)
294 if parent
is not None:
297 return find_parent(root
, elm
)
301 Return the XML class path step corresponding to elm.
302 This can be empty if the elm does not have any class or id.
304 step
= elm
.attrib
.get('class')
307 oid
= elm
.attrib
.get('id')
309 oid
= oid
.encode('ascii','ignore').decode('ascii')
310 step
+= "[@id='%s']" % oid
315 def find_elm(root
, elm
):
317 Return the XML class path of the element from the given root.
318 This is the slow version used when getparent is not available.
323 path
= find_elm(o
, elm
)
329 def errpath(filename
, tree
, elm
):
331 Return the XML class path of the element
336 if 'class' in elm
.attrib
:
337 path
+= elm
.attrib
['class']
338 oid
= elm
.attrib
.get('id')
340 oid
= oid
.encode('ascii','ignore').decode('ascii')
341 path
= "//" + path
+ "[@id='%s']" % oid
344 elm
= elm
.getparent()
345 while elm
is not None:
348 elm
= elm
.getparent()
350 path
= find_elm(tree
.getroot(), elm
)[:-1]
351 path
= filename
+ ':' + path
355 # Warning/Error printing functions
358 def elm_prefix(filename
, elm
):
360 Return the display prefix of the element
362 if elm
== None or not lxml
:
363 return "%s:" % filename
365 return "%s:%u" % (filename
, elm
.sourceline
)
369 Return a display name of the element
373 if 'class' in elm
.attrib
:
374 name
= "'%s' " % elm
.attrib
['class']
375 if 'id' in elm
.attrib
:
376 id = elm
.attrib
['id'].encode('ascii','ignore').decode('ascii')
379 name
= "'" + elm
.tag
+ "'"
381 name
+= " line " + str(elm
.sourceline
)
385 def elm_name_line(elm
):
387 Return a display name of the element with line number
391 if lxml
and " line " not in name
:
392 name
+= "line " + str(elm
.sourceline
) + " "
398 Return the line for the given element.
401 return " line " + str(elm
.sourceline
)
405 def elms_lines(elms
):
407 Return the list of lines for the given elements.
410 return " lines " + ', '.join([str(l
.sourceline
) for l
in elms
])
414 def elms_names_lines(elms
):
416 Return the list of names and lines for the given elements.
418 return ', '.join([elm_name_line(elm
) for elm
in elms
])
420 def elm_suppr(filename
, tree
, elm
, msgtype
, dogen
):
422 Return the prefix to be displayed to the user and the suppression line for
423 the warning type "msgtype" for element "elm"
425 global gen_suppr
, gen_supprfile
, suppr_prefix
, pflag
427 if suppressions
or false_positives
or gen_suppr
is not None or pflag
:
428 prefix
= errpath(filename
, tree
, elm
)
429 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
430 prefix
= prefix
[len(suppr_prefix
):]
432 if suppressions
or false_positives
or gen_suppr
is not None:
433 suppr
= '%s %s' % (prefix
, msgtype
)
435 if gen_suppr
is not None and msgtype
is not None and dogen
:
436 if gen_supprfile
is None:
437 gen_supprfile
= open(gen_suppr
, 'w')
438 print(suppr
, file=gen_supprfile
)
443 # Use user-friendly line numbers
444 prefix
= elm_prefix(filename
, elm
)
445 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
446 prefix
= prefix
[len(suppr_prefix
):]
448 return (prefix
, suppr
)
450 def is_enabled(elm
, msgtype
, l
, default
):
452 Test whether warning type msgtype is enabled for elm in l
455 for (enable
, thetype
, klass
) in l
:
457 if thetype
is not None:
458 if thetype
!= msgtype
:
461 if klass
is not None and elm
is not None:
462 if klass
!= elm
.attrib
.get('class'):
467 def err(filename
, tree
, elm
, msgtype
, msg
, error
= True):
469 Emit a warning or error for an element
471 global errors
, errexists
, warnings
, warnexists
, fatals
, fatalexists
473 # Let user tune whether a warning or error
474 fatal
= is_enabled(elm
, msgtype
, dofatals
, error
)
476 # By default warnings and errors are enabled, but let user tune it
477 if not is_enabled(elm
, msgtype
, enables
, True):
480 (prefix
, suppr
) = elm_suppr(filename
, tree
, elm
, msgtype
, True)
481 if suppr
in false_positives
:
482 # That was actually expected
484 if suppr
in suppressions
:
486 suppressions
[suppr
] = False
502 msg
= "%s %s%s: %s%s" % (prefix
,
503 "FATAL " if fatal
else "",
504 "ERROR" if error
else "WARNING",
507 if outfile
is not None:
508 print(msg
, file=outfile
)
510 def warn(filename
, tree
, elm
, msgtype
, msg
):
512 Emit a warning for an element
514 err(filename
, tree
, elm
, msgtype
, msg
, False)
517 # Labelling testing functions
520 def find_button_parent(root
, elm
):
522 Find a parent which is a button
525 parent
= elm
.getparent()
526 if parent
is not None:
527 if parent
.attrib
.get('class') in widgets_buttons
:
529 return find_button_parent(root
, parent
)
531 def find_parent(cur
, elm
):
534 if cur
.attrib
.get('class') in widgets_buttons
:
535 # we are the button, immediately above the target
538 # we aren't the button, but target is over there
540 parent
= find_parent(o
, elm
)
542 # It is over there, but didn't find a button yet
543 if cur
.attrib
.get('class') in widgets_buttons
:
548 if parent
is not None:
549 # we have the button parent over there
552 parent
= find_parent(root
, elm
)
558 def is_labelled_parent(elm
):
560 Return whether this element is a labelled parent
562 klass
= elm
.attrib
.get('class')
563 if klass
in widgets_toplevel
:
565 if klass
== 'GtkShortcutsGroup':
566 children
= elm
.findall("property[@name='title']")
567 if len(children
) >= 1:
569 if klass
== 'GtkFrame' or klass
== 'GtkNotebook':
570 children
= elm
.findall("child[@type='tab']") + elm
.findall("child[@type='label']")
571 if len(children
) >= 1:
575 def elm_labelled_parent(root
, elm
):
577 Return the first labelled parent of the element, which can thus be used as
578 the root of widgets with common labelled context
582 def find_labelled_parent(elm
):
583 if is_labelled_parent(elm
):
585 parent
= elm
.getparent()
588 return find_labelled_parent(parent
)
589 parent
= elm
.getparent()
592 return find_labelled_parent(elm
.getparent())
594 def find_labelled_parent(cur
, elm
):
596 # the target element is over there
599 parent
= find_labelled_parent(o
, elm
)
601 # target element is over there, check ourself
602 if is_labelled_parent(cur
):
603 # yes, and we are the first ancestor of the target element
606 # no, but target element is over there.
609 # the first ancestor of the target element was over there
612 parent
= find_labelled_parent(root
, elm
)
617 def is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, doprint
= False):
619 Check whether this label has no accessibility relation, or doubtful relation
620 because another label labels the same target
622 global label_for_elm
, labelled_by_elm
, mnemonic_for_elm
, warnexists
625 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
626 for rel
in label_for
:
627 target
= rel
.attrib
['target']
628 l
= label_for_elm
[target
]
633 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
634 obj
.findall("property[@name='mnemonic-widget']")
635 for rel
in mnemonic_for
:
637 l
= mnemonic_for_elm
[target
]
641 if len(label_for
) > 0:
642 # At least one label-for, we are not orphan.
645 if len(mnemonic_for
) > 0:
646 # At least one mnemonic_widget, we are not orphan.
649 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
650 if len(labelled_by
) > 0:
651 # Oh, a labelled label, probably not to be labelling anything
655 roles
= [x
.text
for x
in obj
.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
656 roles
+= [x
.attrib
.get("type") for x
in obj
.findall("accessibility/role")]
657 if len(roles
) > 1 and doprint
:
658 err(filename
, tree
, obj
, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
659 "%s" % elms_lines(children
))
661 if role
== 'static' or role
== 'ATK_ROLE_STATIC':
662 # This is static text, not meant to label anything
665 parent
= elm_parent(root
, obj
)
666 if parent
is not None:
667 childtype
= parent
.attrib
.get('type')
668 if childtype
is None:
669 childtype
= parent
.attrib
.get('internal-child')
670 if parent
.tag
== 'child' and childtype
== 'label' \
671 or childtype
== 'tab':
672 # This is a frame or a notebook label, not orphan.
675 if find_button_parent(root
, obj
) is not None:
676 # This label is part of a button
679 oid
= obj
.attrib
.get('oid')
681 if oid
in labelled_by_elm
:
682 # Some widget is labelled by us, we are not orphan.
683 # We should have had a label-for, will warn about it later.
686 # No label-for, no mnemonic-for, no labelled-by, we are orphan.
687 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "orphan-label", False)
688 if suppr
in false_positives
:
689 # That was actually expected
691 if suppr
in suppressions
:
692 # Warning suppressed for this label
693 if suppressions
[suppr
]:
695 suppressions
[suppr
] = False
699 context
= elm_name(orphan_root
)
701 context
= " within " + context
702 warn(filename
, tree
, obj
, "orphan-label", "does not specify what it labels" + context
)
705 def is_orphan_widget(filename
, tree
, root
, obj
, orphan
, orphan_root
, doprint
= False):
707 Check whether this widget has no accessibility relation.
710 if obj
.tag
!= 'object':
713 oid
= obj
.attrib
.get('id')
714 klass
= obj
.attrib
.get('class')
716 # "Don't care" special case
717 if klass
in widgets_ignored
:
719 for suffix
in widgets_suffixignored
:
720 if klass
[-len(suffix
):] == suffix
:
723 # Widgets usual do not strictly require a label, i.e. a labelled parent
724 # is enough for context, but some do always need one.
725 requires_label
= klass
in widgets_needlabel
727 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
729 # Labels special case
730 if klass
in widgets_labels
:
733 # Case 1: has an explicit <child internal-child="accessible"> sub-element
734 children
= obj
.findall("child[@internal-child='accessible']")
735 if len(children
) > 1 and doprint
:
736 err(filename
, tree
, obj
, "multiple-accessible", "has multiple <child internal-child='accessible'>"
737 "%s" % elms_lines(children
))
738 if len(children
) >= 1:
741 # Case 2: has an <accessibility> sub-element with a "labelled-by"
742 # <relation> pointing to an existing element.
743 if len(labelled_by
) > 0:
746 # Case 3: has a label-for
747 if oid
in label_for_elm
:
750 # Case 4: has a mnemonic
751 if oid
in mnemonic_for_elm
:
754 # Case 5: Has a <property name="tooltip_text">
755 tooltips
= obj
.findall("property[@name='tooltip_text']") + \
756 obj
.findall("property[@name='tooltip-text']")
757 if len(tooltips
) > 1 and doprint
:
758 err(filename
, tree
, obj
, "multiple-tooltip", "has multiple tooltip_text properties")
759 if len(tooltips
) >= 1 and klass
!= 'GtkCheckButton':
762 # Case 6: Has a <property name="placeholder_text">
763 placeholders
= obj
.findall("property[@name='placeholder_text']") + \
764 obj
.findall("property[@name='placeholder-text']")
765 if len(placeholders
) > 1 and doprint
:
766 err(filename
, tree
, obj
, "multiple-placeholder", "has multiple placeholder_text properties")
767 if len(placeholders
) >= 1:
770 # Buttons usually don't need an external label, their own is enough, (but they do need one)
771 if klass
in widgets_buttons
:
773 labels
= obj
.findall("property[@name='label']")
774 if len(labels
) > 1 and doprint
:
775 err(filename
, tree
, obj
, "multiple-label", "has multiple label properties")
777 # Has a <property name="label">
780 actions
= obj
.findall("property[@name='action_name']")
781 if len(actions
) > 1 and doprint
:
782 err(filename
, tree
, obj
, "multiple-action_name", "has multiple action_name properties")
783 if len(actions
) >= 1:
784 # Has a <property name="action_name">
787 gtklabels
= obj
.findall(".//object[@class='GtkLabel']") + obj
.findall(".//object[@class='GtkAccelLabel']")
788 if len(gtklabels
) >= 1:
792 # no label for a button, warn
794 warn(filename
, tree
, obj
, "button-no-label", "does not have its own label");
795 if not is_enabled(obj
, "button-no-label", enables
, True):
798 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "button-no-label", False)
799 if suppr
in false_positives
:
800 # That was actually expected
802 if suppr
in suppressions
:
803 # Warning suppressed for this widget
804 if suppressions
[suppr
]:
806 suppressions
[suppr
] = False
810 # GtkImages special case
811 if klass
== "GtkImage":
812 uses
= [u
for u
in tree
.iterfind(".//object/property[@name='image']") if u
.text
== oid
]
814 # This image is just used by another element, don't warn
815 # about the image itself, we probably want the warning on
816 # the element instead.
819 if find_button_parent(root
, obj
) is not None:
820 # This image is part of a button, we want the warning on the button
824 # GtkEntry special case
825 if klass
== 'GtkEntry' or klass
== 'GtkSearchEntry':
826 parent
= elm_parent(root
, obj
)
827 if parent
is not None:
828 if parent
.tag
== 'child' and \
829 parent
.attrib
.get('internal-child') == "entry":
830 # This is an internal entry of another widget. Relations
831 # will be handled by that widget.
834 # GtkShortcutsShortcut special case
835 if klass
== 'GtkShortcutsShortcut':
836 children
= obj
.findall("property[@name='title']")
837 if len(children
) >= 1:
841 # Really no label, perhaps emit a warning
842 if not is_enabled(obj
, "no-labelled-by", enables
, True):
843 # Warnings disabled for this class of widgets
845 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "no-labelled-by", False)
846 if suppr
in false_positives
:
847 # That was actually expected
849 if suppr
in suppressions
:
850 # Warning suppressed for this widget
851 if suppressions
[suppr
]:
853 suppressions
[suppr
] = False
857 # No orphan label, so probably the labelled parent provides enough
860 # But these always need a label.
862 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label")
867 context
= elm_name(orphan_root
)
869 context
= " within " + context
870 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label while there are orphan labels" + context
)
873 def orphan_items(filename
, tree
, root
, elm
):
875 Check whether from some element there exists orphan labels and orphan widgets
877 orphan_labels
= False
878 orphan_widgets
= False
879 if elm
.attrib
.get('class') in widgets_labels
:
880 orphan_labels
= is_orphan_label(filename
, tree
, root
, elm
, None)
882 orphan_widgets
= is_orphan_widget(filename
, tree
, root
, elm
, True, None)
884 # We are not interested in orphan labels under another labelled
885 # parent. This also allows to keep linear complexity.
886 if not is_labelled_parent(obj
):
887 label
, widget
= orphan_items(filename
, tree
, root
, obj
)
891 orphan_widgets
= True
892 if orphan_labels
and orphan_widgets
:
893 # No need to look up more
895 return orphan_labels
, orphan_widgets
898 # UI accessibility checks
901 def check_props(filename
, tree
, root
, elm
, forward
):
903 Check the given list of relation properties
905 props
= elm
.findall("property[@name='" + forward
+ "']")
907 if prop
.text
not in ids
:
908 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % prop
.text
)
911 def check_rels(filename
, tree
, root
, elm
, forward
, backward
= None):
913 Check the relations given by forward
915 oid
= elm
.attrib
.get('id')
916 rels
= elm
.findall("accessibility/relation[@type='" + forward
+ "']")
918 target
= rel
.attrib
['target']
919 if target
not in ids
:
920 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % target
)
921 elif backward
is not None:
923 backrels
= widget
.findall("accessibility/relation[@type='" + backward
+ "']")
924 if len([x
for x
in backrels
if x
.attrib
['target'] == oid
]) == 0:
925 err(filename
, tree
, elm
, "missing-" + backward
, "has " + forward
+ \
926 ", but is not " + backward
+ " by " + elm_name_line(widget
))
929 def check_a11y_relation(filename
, tree
):
931 Emit an error message if any of the 'object' elements of the XML
932 document represented by `root' doesn't comply with Accessibility
935 global widgets_ignored
, ids
, label_for_elm
, labelled_by_elm
, mnemonic_for_elm
937 def check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
939 Check one element, knowing that orphan_labels/widgets tell whether
940 there are orphan labels and widgets within orphan_root
943 oid
= obj
.attrib
.get('id')
944 klass
= obj
.attrib
.get('class')
946 # "Don't care" special case
947 if klass
in widgets_ignored
:
949 for suffix
in widgets_suffixignored
:
950 if klass
[-len(suffix
):] == suffix
:
953 # Widgets usual do not strictly require a label, i.e. a labelled parent
954 # is enough for context, but some do always need one.
955 requires_label
= klass
in widgets_needlabel
958 # Check that ids are unique
961 # We are the first, warn
962 duplicates
= tree
.findall(".//object[@id='" + oid
+ "']")
963 err(filename
, tree
, obj
, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates
))
965 # Check label-for and their dual labelled-by
966 label_for
= check_rels(filename
, tree
, root
, obj
, "label-for", "labelled-by")
968 # Check labelled-by and its dual label-for
969 labelled_by
= check_rels(filename
, tree
, root
, obj
, "labelled-by", "label-for")
971 # Should have only one label
972 if len(labelled_by
) >= 1:
973 if oid
in mnemonic_for_elm
:
974 warn(filename
, tree
, obj
, "labelled-by-and-mnemonic",
975 "has both a mnemonic " + elm_name_line(mnemonic_for_elm
[oid
][0]) + "and labelled-by relation")
976 if len(labelled_by
) > 1:
977 warn(filename
, tree
, obj
, "multiple-labelled-by", "has multiple labelled-by relations")
978 if oid
in label_for_elm
:
979 if len(label_for_elm
[oid
]) > 1:
980 warn(filename
, tree
, obj
, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm
[oid
]))
981 if oid
in mnemonic_for_elm
:
982 if len(mnemonic_for_elm
[oid
]) > 1:
983 warn(filename
, tree
, obj
, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm
[oid
]))
986 member_of
= check_rels(filename
, tree
, root
, obj
, "member-of")
988 # Labels special case
989 if klass
in widgets_labels
:
990 properties
= check_props(filename
, tree
, root
, obj
, "mnemonic_widget") + \
991 check_props(filename
, tree
, root
, obj
, "mnemonic-widget")
992 if len(properties
) > 1:
993 err(filename
, tree
, obj
, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
994 "%s" % elms_lines(properties
))
996 # Emit orphaning warnings
997 if warn_orphan_labels
or orphan_widgets
:
998 is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, True)
1000 # We are done with the label
1003 # Not a label, will perhaps need one
1005 # Emit orphaning warnings
1006 is_orphan_widget(filename
, tree
, root
, obj
, orphan_labels
, orphan_root
, True)
1008 root
= tree
.getroot()
1010 # Flush ids and relations from previous files
1013 labelled_by_elm
= {}
1015 mnemonic_for_elm
= {}
1017 # First pass to get links into hash tables, no warning, just record duplicates
1018 for obj
in root
.iter('object'):
1019 oid
= obj
.attrib
.get('id')
1026 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
1027 for rel
in labelled_by
:
1028 target
= rel
.attrib
.get('target')
1029 if target
is not None:
1030 if target
not in labelled_by_elm
:
1031 labelled_by_elm
[target
] = [ obj
]
1033 labelled_by_elm
[target
].append(obj
)
1035 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
1036 for rel
in label_for
:
1037 target
= rel
.attrib
.get('target')
1038 if target
is not None:
1039 if target
not in label_for_elm
:
1040 label_for_elm
[target
] = [ obj
]
1042 label_for_elm
[target
].append(obj
)
1044 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
1045 obj
.findall("property[@name='mnemonic-widget']")
1046 for rel
in mnemonic_for
:
1048 if target
is not None:
1049 if target
not in mnemonic_for_elm
:
1050 mnemonic_for_elm
[target
] = [ obj
]
1052 mnemonic_for_elm
[target
].append(obj
)
1054 # Second pass, recursive depth-first, to be able to efficiently know whether
1055 # there are orphan labels within a part of the tree.
1056 def recurse(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1057 if obj
== root
or is_labelled_parent(obj
):
1059 orphan_labels
, orphan_widgets
= orphan_items(filename
, tree
, root
, obj
)
1061 if obj
.tag
== 'object':
1062 check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
)
1065 recurse(orphan_root
, o
, orphan_labels
, orphan_widgets
)
1067 recurse(root
, root
, False, False)
1073 def usage(fatal
= True):
1074 print("`%s' checks accessibility of glade .ui files" % progname
)
1076 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname
)
1078 print(" -p Print XML class path instead of line number")
1079 print(" -g Generate suppression file SUPPR_FILE")
1080 print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
1081 print(" -f Suppress warnings given by file SUPPR_FILE completely")
1082 print(" -P Remove PREFIX from file names in warnings")
1083 print(" -o Also prints errors and warnings to given file")
1085 print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
1086 print(" Give or extend one of the lists of widget classes, where FOO can be:")
1087 print(" - toplevel : widgets to be considered toplevel windows")
1088 print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
1089 print(" - suffixignored : suffixes of widget classes which do not need labelling")
1090 print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
1091 print(" - buttons : widgets which need their own label but not more")
1092 print(" (e.g. GtkButton)")
1093 print(" - labels : widgets which provide labels (e.g. GtkLabel)")
1094 print(" --widgets-print print default widgets lists")
1096 print(" --enable-all enable all warnings/dofatals (default)")
1097 print(" --disable-all disable all warnings/dofatals")
1098 print(" --fatal-all make all warnings dofatals")
1099 print(" --not-fatal-all do not make all warnings dofatals (default)")
1101 print(" --enable-type=TYPE enable warning/fatal type TYPE")
1102 print(" --disable-type=TYPE disable warning/fatal type TYPE")
1103 print(" --fatal-type=TYPE make warning type TYPE an fatal")
1104 print(" --not-fatal-type=TYPE make warning type TYPE not an fatal")
1106 print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
1107 print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
1108 print(" --fatal-widgets=CLASS make warning type CLASS an fatal")
1109 print(" --not-fatal-widgets=CLASS make warning type CLASS not an fatal")
1111 print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
1112 print(" class CLASS")
1113 print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
1114 print(" class CLASS")
1115 print(" --fatal-specific=TYPE.CLASS make warning type TYPE an fatal for widget")
1116 print(" class CLASS")
1117 print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not an fatal for widget")
1118 print(" class CLASS")
1120 print(" --disable-orphan-labels only warn about orphan labels when there are")
1121 print(" orphan widgets in the same context")
1123 print("Report bugs to <bugs@hypra.fr>")
1124 sys
.exit(2 if fatal
else 0)
1126 def widgets_opt(widgets_list
, arg
):
1128 Replace or extend `widgets_list' with the list of classes contained in `arg'
1130 append
= arg
and arg
[0] == '+'
1135 widgets
= arg
.split(',')
1142 widgets_list
.extend(widgets
)
1146 global pflag
, gen_suppr
, gen_supprfile
, suppressions
, suppr_prefix
, false_positives
, dofatals
, enables
, dofatals
, warn_orphan_labels
1147 global widgets_toplevel
, widgets_ignored
, widgets_suffixignored
, widgets_needlabel
, widgets_buttons
, widgets_labels
1151 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hpiIg:s:f:P:o:L:", [
1155 "widgets-toplevel=",
1157 "widgets-suffixignored=",
1158 "widgets-needlabel=",
1176 "not-fatal-widgets=",
1179 "disable-specific=",
1181 "not-fatal-specific=",
1183 "disable-orphan-labels",
1185 except getopt
.GetoptError
:
1194 if o
== "--help" or o
== "-h":
1196 if o
== "--version":
1214 elif o
== "--widgets-toplevel":
1215 widgets_opt(widgets_toplevel
, a
)
1216 elif o
== "--widgets-ignored":
1217 widgets_opt(widgets_ignored
, a
)
1218 elif o
== "--widgets-suffixignored":
1219 widgets_opt(widgets_suffixignored
, a
)
1220 elif o
== "--widgets-needlabel":
1221 widgets_opt(widgets_needlabel
, a
)
1222 elif o
== "--widgets-buttons":
1223 widgets_opt(widgets_buttons
, a
)
1224 elif o
== "--widgets-labels":
1225 widgets_opt(widgets_labels
, a
)
1226 elif o
== "--widgets-print":
1227 print("--widgets-toplevel '" + ','.join(widgets_toplevel
) + "'")
1228 print("--widgets-ignored '" + ','.join(widgets_ignored
) + "'")
1229 print("--widgets-suffixignored '" + ','.join(widgets_suffixignored
) + "'")
1230 print("--widgets-needlabel '" + ','.join(widgets_needlabel
) + "'")
1231 print("--widgets-buttons '" + ','.join(widgets_buttons
) + "'")
1232 print("--widgets-labels '" + ','.join(widgets_labels
) + "'")
1235 elif o
== '--enable-all':
1236 enables
.append( (True, None, None) )
1237 elif o
== '--disable-all':
1238 enables
.append( (False, None, None) )
1239 elif o
== '--fatal-all':
1240 dofatals
.append( (True, None, None) )
1241 elif o
== '--not-fatal-all':
1242 dofatals
.append( (False, None, None) )
1244 elif o
== '--enable-type':
1245 enables
.append( (True, a
, None) )
1246 elif o
== '--disable-type':
1247 enables
.append( (False, a
, None) )
1248 elif o
== '--fatal-type':
1249 dofatals
.append( (True, a
, None) )
1250 elif o
== '--not-fatal-type':
1251 dofatals
.append( (False, a
, None) )
1253 elif o
== '--enable-widgets':
1254 enables
.append( (True, None, a
) )
1255 elif o
== '--disable-widgets':
1256 enables
.append( (False, None, a
) )
1257 elif o
== '--fatal-widgets':
1258 dofatals
.append( (True, None, a
) )
1259 elif o
== '--not-fatal-widgets':
1260 dofatals
.append( (False, None, a
) )
1262 elif o
== '--enable-specific':
1263 (thetype
, klass
) = a
.split('.', 1)
1264 enables
.append( (True, thetype
, klass
) )
1265 elif o
== '--disable-specific':
1266 (thetype
, klass
) = a
.split('.', 1)
1267 enables
.append( (False, thetype
, klass
) )
1268 elif o
== '--fatal-specific':
1269 (thetype
, klass
) = a
.split('.', 1)
1270 dofatals
.append( (True, thetype
, klass
) )
1271 elif o
== '--not-fatal-specific':
1272 (thetype
, klass
) = a
.split('.', 1)
1273 dofatals
.append( (False, thetype
, klass
) )
1275 elif o
== '--disable-orphan-labels':
1276 warn_orphan_labels
= False
1278 # Read suppression file before overwriting it
1279 if suppr
is not None:
1281 supprfile
= open(suppr
, 'r')
1282 for line
in supprfile
.readlines():
1283 prefix
= line
.rstrip()
1284 suppressions
[prefix
] = True
1289 # Read false positives file
1290 if false
is not None:
1292 falsefile
= open(false
, 'r')
1293 for line
in falsefile
.readlines():
1294 prefix
= line
.rstrip()
1295 false_positives
[prefix
] = True
1301 outfile
= open(out
, 'w')
1303 if filelist
is not None:
1305 filelistfile
= open(filelist
, 'r')
1306 for line
in filelistfile
.readlines():
1309 args
+= line
.split(' ')
1310 filelistfile
.close()
1312 err(filelist
, None, None, "unable to read file list file")
1314 for filename
in args
:
1316 tree
= ET
.parse(filename
)
1317 except ET
.ParseError
:
1318 err(filename
, None, None, "parse", "malformatted xml file")
1321 err(filename
, None, None, None, "unable to read file")
1325 check_a11y_relation(filename
, tree
)
1326 except Exception as error
:
1328 traceback
.print_exc()
1329 err(filename
, None, None, "parse", "error parsing file")
1331 if errors
> 0 or errexists
> 0:
1332 estr
= "%s new error%s" % (errors
, 's' if errors
> 1 else '')
1334 estr
+= " (%s suppressed by %s)" % (errexists
, suppr
)
1337 if warnings
> 0 or warnexists
> 0:
1338 wstr
= "%s new warning%s" % (warnings
, 's' if warnings
> 1 else '')
1340 wstr
+= " (%s suppressed by %s)" % (warnexists
, suppr
)
1343 if fatals
> 0 or fatalexists
> 0:
1344 wstr
= "%s new fatal%s" % (fatals
, 's' if fatals
> 1 else '')
1346 wstr
+= " (%s suppressed by %s)" % (fatalexists
, suppr
)
1350 for (suppr
,unused
) in suppressions
.items():
1355 print("%s suppression%s unused" % (n
, 's' if n
> 1 else ''))
1357 if gen_supprfile
is not None:
1358 gen_supprfile
.close()
1359 if outfile
is not None:
1361 if fatals
> 0 and gen_suppr
is None:
1362 print("Explanations are available on https://wiki.documentfoundation.org/Development/Accessibility")
1366 if __name__
== "__main__":
1369 except KeyboardInterrupt:
1372 # vim: set shiftwidth=4 softtabstop=4 expandtab: