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-2019 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',
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])
252 suppressions_to_line
= {}
258 mnemonic_for_elm
= {}
267 warn_orphan_labels
= True
280 # XML browsing and printing functions
283 def elm_parent(root
, elm
):
285 Return the parent of the element.
288 return elm
.getparent()
290 def find_parent(cur
, elm
):
294 parent
= find_parent(o
, elm
)
295 if parent
is not None:
298 return find_parent(root
, elm
)
302 Return the XML class path step corresponding to elm.
303 This can be empty if the elm does not have any class or id.
305 step
= elm
.attrib
.get('class')
308 oid
= elm
.attrib
.get('id')
310 oid
= oid
.encode('ascii','ignore').decode('ascii')
311 step
+= "[@id='%s']" % oid
316 def find_elm(root
, elm
):
318 Return the XML class path of the element from the given root.
319 This is the slow version used when getparent is not available.
324 path
= find_elm(o
, elm
)
330 def errpath(filename
, tree
, elm
):
332 Return the XML class path of the element
337 if 'class' in elm
.attrib
:
338 path
+= elm
.attrib
['class']
339 oid
= elm
.attrib
.get('id')
341 oid
= oid
.encode('ascii','ignore').decode('ascii')
342 path
= "//" + path
+ "[@id='%s']" % oid
345 elm
= elm
.getparent()
346 while elm
is not None:
349 elm
= elm
.getparent()
351 path
= find_elm(tree
.getroot(), elm
)[:-1]
352 path
= filename
+ ':' + path
356 # Warning/Error printing functions
359 def elm_prefix(filename
, elm
):
361 Return the display prefix of the element
363 if elm
== None or not lxml
:
364 return "%s:" % filename
366 return "%s:%u" % (filename
, elm
.sourceline
)
370 Return a display name of the element
374 if 'class' in elm
.attrib
:
375 name
= "'%s' " % elm
.attrib
['class']
376 if 'id' in elm
.attrib
:
377 id = elm
.attrib
['id'].encode('ascii','ignore').decode('ascii')
380 name
= "'" + elm
.tag
+ "'"
382 name
+= " line " + str(elm
.sourceline
)
386 def elm_name_line(elm
):
388 Return a display name of the element with line number
392 if lxml
and " line " not in name
:
393 name
+= "line " + str(elm
.sourceline
) + " "
399 Return the line for the given element.
402 return " line " + str(elm
.sourceline
)
406 def elms_lines(elms
):
408 Return the list of lines for the given elements.
411 return " lines " + ', '.join([str(l
.sourceline
) for l
in elms
])
415 def elms_names_lines(elms
):
417 Return the list of names and lines for the given elements.
419 return ', '.join([elm_name_line(elm
) for elm
in elms
])
421 def elm_suppr(filename
, tree
, elm
, msgtype
, dogen
):
423 Return the prefix to be displayed to the user and the suppression line for
424 the warning type "msgtype" for element "elm"
426 global gen_suppr
, gen_supprfile
, suppr_prefix
, pflag
428 if suppressions
or false_positives
or gen_suppr
is not None or pflag
:
429 prefix
= errpath(filename
, tree
, elm
)
430 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
431 prefix
= prefix
[len(suppr_prefix
):]
433 if suppressions
or false_positives
or gen_suppr
is not None:
434 suppr
= '%s %s' % (prefix
, msgtype
)
436 if gen_suppr
is not None and msgtype
is not None and dogen
:
437 if gen_supprfile
is None:
438 gen_supprfile
= open(gen_suppr
, 'w')
439 print(suppr
, file=gen_supprfile
)
444 # Use user-friendly line numbers
445 prefix
= elm_prefix(filename
, elm
)
446 if prefix
[0:len(suppr_prefix
)] == suppr_prefix
:
447 prefix
= prefix
[len(suppr_prefix
):]
449 return (prefix
, suppr
)
451 def is_enabled(elm
, msgtype
, l
, default
):
453 Test whether warning type msgtype is enabled for elm in l
456 for (enable
, thetype
, klass
) in l
:
458 if thetype
is not None:
459 if thetype
!= msgtype
:
462 if klass
is not None and elm
is not None:
463 if klass
!= elm
.attrib
.get('class'):
468 def err(filename
, tree
, elm
, msgtype
, msg
, error
= True):
470 Emit a warning or error for an element
472 global errors
, errexists
, warnings
, warnexists
, fatals
, fatalexists
474 # Let user tune whether a warning or error
475 fatal
= is_enabled(elm
, msgtype
, dofatals
, error
)
477 # By default warnings and errors are enabled, but let user tune it
478 if not is_enabled(elm
, msgtype
, enables
, True):
481 (prefix
, suppr
) = elm_suppr(filename
, tree
, elm
, msgtype
, True)
482 if suppr
in false_positives
:
483 # That was actually expected
485 if suppr
in suppressions
:
487 suppressions
[suppr
] = False
503 msg
= "%s %s%s: %s%s" % (prefix
,
504 "FATAL " if fatal
else "",
505 "ERROR" if error
else "WARNING",
508 if outfile
is not None:
509 print(msg
, file=outfile
)
511 def warn(filename
, tree
, elm
, msgtype
, msg
):
513 Emit a warning for an element
515 err(filename
, tree
, elm
, msgtype
, msg
, False)
518 # Labelling testing functions
521 def find_button_parent(root
, elm
):
523 Find a parent which is a button
526 parent
= elm
.getparent()
527 if parent
is not None:
528 if parent
.attrib
.get('class') in widgets_buttons
:
530 return find_button_parent(root
, parent
)
532 def find_parent(cur
, elm
):
535 if cur
.attrib
.get('class') in widgets_buttons
:
536 # we are the button, immediately above the target
539 # we aren't the button, but target is over there
541 parent
= find_parent(o
, elm
)
543 # It is over there, but didn't find a button yet
544 if cur
.attrib
.get('class') in widgets_buttons
:
549 if parent
is not None:
550 # we have the button parent over there
553 parent
= find_parent(root
, elm
)
559 def is_labelled_parent(elm
):
561 Return whether this element is a labelled parent
563 klass
= elm
.attrib
.get('class')
564 if klass
in widgets_toplevel
:
566 if klass
== 'GtkShortcutsGroup':
567 children
= elm
.findall("property[@name='title']")
568 if len(children
) >= 1:
570 if klass
== 'GtkFrame' or klass
== 'GtkNotebook':
571 children
= elm
.findall("child[@type='tab']") + elm
.findall("child[@type='label']")
572 if len(children
) >= 1:
576 def elm_labelled_parent(root
, elm
):
578 Return the first labelled parent of the element, which can thus be used as
579 the root of widgets with common labelled context
583 def find_labelled_parent(elm
):
584 if is_labelled_parent(elm
):
586 parent
= elm
.getparent()
589 return find_labelled_parent(parent
)
590 parent
= elm
.getparent()
593 return find_labelled_parent(elm
.getparent())
595 def find_labelled_parent(cur
, elm
):
597 # the target element is over there
600 parent
= find_labelled_parent(o
, elm
)
602 # target element is over there, check ourself
603 if is_labelled_parent(cur
):
604 # yes, and we are the first ancestor of the target element
607 # no, but target element is over there.
610 # the first ancestor of the target element was over there
613 parent
= find_labelled_parent(root
, elm
)
618 def is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, doprint
= False):
620 Check whether this label has no accessibility relation, or doubtful relation
621 because another label labels the same target
623 global label_for_elm
, labelled_by_elm
, mnemonic_for_elm
, warnexists
626 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
627 for rel
in label_for
:
628 target
= rel
.attrib
['target']
629 l
= label_for_elm
[target
]
634 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
635 obj
.findall("property[@name='mnemonic-widget']")
636 for rel
in mnemonic_for
:
638 l
= mnemonic_for_elm
[target
]
642 if len(label_for
) > 0:
643 # At least one label-for, we are not orphan.
646 if len(mnemonic_for
) > 0:
647 # At least one mnemonic_widget, we are not orphan.
650 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
651 if len(labelled_by
) > 0:
652 # Oh, a labelled label, probably not to be labelling anything
656 roles
= [x
.text
for x
in obj
.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
657 roles
+= [x
.attrib
.get("type") for x
in obj
.findall("accessibility/role")]
658 if len(roles
) > 1 and doprint
:
659 err(filename
, tree
, obj
, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
660 "%s" % elms_lines(children
))
662 if role
== 'static' or role
== 'ATK_ROLE_STATIC':
663 # This is static text, not meant to label anything
666 parent
= elm_parent(root
, obj
)
667 if parent
is not None:
668 childtype
= parent
.attrib
.get('type')
669 if childtype
is None:
670 childtype
= parent
.attrib
.get('internal-child')
671 if parent
.tag
== 'child' and childtype
== 'label' \
672 or childtype
== 'tab':
673 # This is a frame or a notebook label, not orphan.
676 if find_button_parent(root
, obj
) is not None:
677 # This label is part of a button
680 oid
= obj
.attrib
.get('oid')
682 if oid
in labelled_by_elm
:
683 # Some widget is labelled by us, we are not orphan.
684 # We should have had a label-for, will warn about it later.
687 # No label-for, no mnemonic-for, no labelled-by, we are orphan.
688 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "orphan-label", False)
689 if suppr
in false_positives
:
690 # That was actually expected
692 if suppr
in suppressions
:
693 # Warning suppressed for this label
694 if suppressions
[suppr
]:
696 suppressions
[suppr
] = False
700 context
= elm_name(orphan_root
)
702 context
= " within " + context
703 warn(filename
, tree
, obj
, "orphan-label", "does not specify what it labels" + context
)
706 def is_orphan_widget(filename
, tree
, root
, obj
, orphan
, orphan_root
, doprint
= False):
708 Check whether this widget has no accessibility relation.
711 if obj
.tag
!= 'object':
714 oid
= obj
.attrib
.get('id')
715 klass
= obj
.attrib
.get('class')
717 # "Don't care" special case
718 if klass
in widgets_ignored
:
720 for suffix
in widgets_suffixignored
:
721 if klass
[-len(suffix
):] == suffix
:
724 # Widgets usual do not strictly require a label, i.e. a labelled parent
725 # is enough for context, but some do always need one.
726 requires_label
= klass
in widgets_needlabel
728 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
730 # Labels special case
731 if klass
in widgets_labels
:
734 # Case 1: has an explicit <child internal-child="accessible"> sub-element
735 children
= obj
.findall("child[@internal-child='accessible']")
736 if len(children
) > 1 and doprint
:
737 err(filename
, tree
, obj
, "multiple-accessible", "has multiple <child internal-child='accessible'>"
738 "%s" % elms_lines(children
))
739 if len(children
) >= 1:
742 # Case 2: has an <accessibility> sub-element with a "labelled-by"
743 # <relation> pointing to an existing element.
744 if len(labelled_by
) > 0:
747 # Case 3: has a label-for
748 if oid
in label_for_elm
:
751 # Case 4: has a mnemonic
752 if oid
in mnemonic_for_elm
:
755 # Case 5: Has a <property name="tooltip_text">
756 tooltips
= obj
.findall("property[@name='tooltip_text']") + \
757 obj
.findall("property[@name='tooltip-text']")
758 if len(tooltips
) > 1 and doprint
:
759 err(filename
, tree
, obj
, "multiple-tooltip", "has multiple tooltip_text properties")
760 if len(tooltips
) >= 1 and klass
!= 'GtkCheckButton':
763 # Case 6: Has a <property name="placeholder_text">
764 placeholders
= obj
.findall("property[@name='placeholder_text']") + \
765 obj
.findall("property[@name='placeholder-text']")
766 if len(placeholders
) > 1 and doprint
:
767 err(filename
, tree
, obj
, "multiple-placeholder", "has multiple placeholder_text properties")
768 if len(placeholders
) >= 1:
771 # Buttons usually don't need an external label, their own is enough, (but they do need one)
772 if klass
in widgets_buttons
:
774 labels
= obj
.findall("property[@name='label']")
775 if len(labels
) > 1 and doprint
:
776 err(filename
, tree
, obj
, "multiple-label", "has multiple label properties")
778 # Has a <property name="label">
781 actions
= obj
.findall("property[@name='action_name']")
782 if len(actions
) > 1 and doprint
:
783 err(filename
, tree
, obj
, "multiple-action_name", "has multiple action_name properties")
784 if len(actions
) >= 1:
785 # Has a <property name="action_name">
788 # Uses id as an action_name
789 if 'id' in obj
.attrib
:
790 if obj
.attrib
['id'].startswith(".uno:"):
793 gtklabels
= obj
.findall(".//object[@class='GtkLabel']") + obj
.findall(".//object[@class='GtkAccelLabel']")
794 if len(gtklabels
) >= 1:
798 # no label for a button, warn
800 warn(filename
, tree
, obj
, "button-no-label", "does not have its own label");
801 if not is_enabled(obj
, "button-no-label", enables
, True):
804 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "button-no-label", False)
805 if suppr
in false_positives
:
806 # That was actually expected
808 if suppr
in suppressions
:
809 # Warning suppressed for this widget
810 if suppressions
[suppr
]:
812 suppressions
[suppr
] = False
816 # GtkImages special case
817 if klass
== "GtkImage":
818 uses
= [u
for u
in tree
.iterfind(".//object/property[@name='image']") if u
.text
== oid
]
820 # This image is just used by another element, don't warn
821 # about the image itself, we probably want the warning on
822 # the element instead.
825 if find_button_parent(root
, obj
) is not None:
826 # This image is part of a button, we want the warning on the button
830 # GtkEntry special case
831 if klass
== 'GtkEntry' or klass
== 'GtkSearchEntry':
832 parent
= elm_parent(root
, obj
)
833 if parent
is not None:
834 if parent
.tag
== 'child' and \
835 parent
.attrib
.get('internal-child') == "entry":
836 # This is an internal entry of another widget. Relations
837 # will be handled by that widget.
840 # GtkShortcutsShortcut special case
841 if klass
== 'GtkShortcutsShortcut':
842 children
= obj
.findall("property[@name='title']")
843 if len(children
) >= 1:
847 # Really no label, perhaps emit a warning
848 if not is_enabled(obj
, "no-labelled-by", enables
, True):
849 # Warnings disabled for this class of widgets
851 (_
, suppr
) = elm_suppr(filename
, tree
, obj
, "no-labelled-by", False)
852 if suppr
in false_positives
:
853 # That was actually expected
855 if suppr
in suppressions
:
856 # Warning suppressed for this widget
857 if suppressions
[suppr
]:
859 suppressions
[suppr
] = False
863 # No orphan label, so probably the labelled parent provides enough
866 # But these always need a label.
868 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label")
873 context
= elm_name(orphan_root
)
875 context
= " within " + context
876 warn(filename
, tree
, obj
, "no-labelled-by", "has no accessibility label while there are orphan labels" + context
)
879 def orphan_items(filename
, tree
, root
, elm
):
881 Check whether from some element there exists orphan labels and orphan widgets
883 orphan_labels
= False
884 orphan_widgets
= False
885 if elm
.attrib
.get('class') in widgets_labels
:
886 orphan_labels
= is_orphan_label(filename
, tree
, root
, elm
, None)
888 orphan_widgets
= is_orphan_widget(filename
, tree
, root
, elm
, True, None)
890 # We are not interested in orphan labels under another labelled
891 # parent. This also allows to keep linear complexity.
892 if not is_labelled_parent(obj
):
893 label
, widget
= orphan_items(filename
, tree
, root
, obj
)
897 orphan_widgets
= True
898 if orphan_labels
and orphan_widgets
:
899 # No need to look up more
901 return orphan_labels
, orphan_widgets
904 # UI accessibility checks
907 def check_props(filename
, tree
, root
, elm
, forward
):
909 Check the given list of relation properties
911 props
= elm
.findall("property[@name='" + forward
+ "']")
913 if prop
.text
not in ids
:
914 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % prop
.text
)
919 visible_prop
= obj
.findall("property[@name='visible']")
920 visible_len
= len(visible_prop
)
922 visible_txt
= visible_prop
[visible_len
- 1].text
923 if visible_txt
.lower() == "true":
925 elif visible_txt
.lower() == "false":
929 def check_rels(filename
, tree
, root
, elm
, forward
, backward
= None):
931 Check the relations given by forward
933 oid
= elm
.attrib
.get('id')
934 rels
= elm
.findall("accessibility/relation[@type='" + forward
+ "']")
936 target
= rel
.attrib
['target']
937 if target
not in ids
:
938 err(filename
, tree
, elm
, "undeclared-target", forward
+ " uses undeclared target '%s'" % target
)
939 elif backward
is not None:
941 backrels
= widget
.findall("accessibility/relation[@type='" + backward
+ "']")
942 if len([x
for x
in backrels
if x
.attrib
['target'] == oid
]) == 0:
943 err(filename
, tree
, elm
, "missing-" + backward
, "has " + forward
+ \
944 ", but is not " + backward
+ " by " + elm_name_line(widget
))
947 def check_a11y_relation(filename
, tree
):
949 Emit an error message if any of the 'object' elements of the XML
950 document represented by `root' doesn't comply with Accessibility
953 global widgets_ignored
, ids
, label_for_elm
, labelled_by_elm
, mnemonic_for_elm
955 def check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
957 Check one element, knowing that orphan_labels/widgets tell whether
958 there are orphan labels and widgets within orphan_root
961 oid
= obj
.attrib
.get('id')
962 klass
= obj
.attrib
.get('class')
964 # "Don't care" special case
965 if klass
in widgets_ignored
:
967 for suffix
in widgets_suffixignored
:
968 if klass
[-len(suffix
):] == suffix
:
971 # Widgets usual do not strictly require a label, i.e. a labelled parent
972 # is enough for context, but some do always need one.
973 requires_label
= klass
in widgets_needlabel
976 # Check that ids are unique
979 # We are the first, warn
980 duplicates
= tree
.findall(".//object[@id='" + oid
+ "']")
981 err(filename
, tree
, obj
, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates
))
983 # Check label-for and their dual labelled-by
984 label_for
= check_rels(filename
, tree
, root
, obj
, "label-for", "labelled-by")
986 # Check labelled-by and its dual label-for
987 labelled_by
= check_rels(filename
, tree
, root
, obj
, "labelled-by", "label-for")
989 visible
= is_visible(obj
)
991 # Should have only one label
992 if len(labelled_by
) >= 1:
993 if oid
in mnemonic_for_elm
:
994 warn(filename
, tree
, obj
, "labelled-by-and-mnemonic",
995 "has both a mnemonic " + elm_name_line(mnemonic_for_elm
[oid
][0]) + "and labelled-by relation")
996 if len(labelled_by
) > 1:
997 warn(filename
, tree
, obj
, "multiple-labelled-by", "has multiple labelled-by relations")
998 if oid
in label_for_elm
:
999 if len(label_for_elm
[oid
]) > 1:
1000 warn(filename
, tree
, obj
, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm
[oid
]))
1001 elif len(label_for_elm
[oid
]) == 1:
1002 paired
= label_for_elm
[oid
][0]
1003 if visible
!= is_visible(paired
):
1004 warn(filename
, tree
, obj
, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired
))
1005 if oid
in mnemonic_for_elm
:
1006 if len(mnemonic_for_elm
[oid
]) > 1:
1007 warn(filename
, tree
, obj
, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm
[oid
]))
1010 member_of
= check_rels(filename
, tree
, root
, obj
, "member-of")
1012 # Labels special case
1013 if klass
in widgets_labels
:
1014 properties
= check_props(filename
, tree
, root
, obj
, "mnemonic_widget") + \
1015 check_props(filename
, tree
, root
, obj
, "mnemonic-widget")
1016 if len(properties
) > 1:
1017 err(filename
, tree
, obj
, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
1018 "%s" % elms_lines(properties
))
1020 # Emit orphaning warnings
1021 if warn_orphan_labels
or orphan_widgets
:
1022 is_orphan_label(filename
, tree
, root
, obj
, orphan_root
, True)
1024 # We are done with the label
1027 # Not a label, will perhaps need one
1029 # Emit orphaning warnings
1030 is_orphan_widget(filename
, tree
, root
, obj
, orphan_labels
, orphan_root
, True)
1032 root
= tree
.getroot()
1034 # Flush ids and relations from previous files
1037 labelled_by_elm
= {}
1039 mnemonic_for_elm
= {}
1041 # First pass to get links into hash tables, no warning, just record duplicates
1042 for obj
in root
.iter('object'):
1043 oid
= obj
.attrib
.get('id')
1050 labelled_by
= obj
.findall("accessibility/relation[@type='labelled-by']")
1051 for rel
in labelled_by
:
1052 target
= rel
.attrib
.get('target')
1053 if target
is not None:
1054 if target
not in labelled_by_elm
:
1055 labelled_by_elm
[target
] = [ obj
]
1057 labelled_by_elm
[target
].append(obj
)
1059 label_for
= obj
.findall("accessibility/relation[@type='label-for']")
1060 for rel
in label_for
:
1061 target
= rel
.attrib
.get('target')
1062 if target
is not None:
1063 if target
not in label_for_elm
:
1064 label_for_elm
[target
] = [ obj
]
1066 label_for_elm
[target
].append(obj
)
1068 mnemonic_for
= obj
.findall("property[@name='mnemonic_widget']") + \
1069 obj
.findall("property[@name='mnemonic-widget']")
1070 for rel
in mnemonic_for
:
1072 if target
is not None:
1073 if target
not in mnemonic_for_elm
:
1074 mnemonic_for_elm
[target
] = [ obj
]
1076 mnemonic_for_elm
[target
].append(obj
)
1078 # Second pass, recursive depth-first, to be able to efficiently know whether
1079 # there are orphan labels within a part of the tree.
1080 def recurse(orphan_root
, obj
, orphan_labels
, orphan_widgets
):
1081 if obj
== root
or is_labelled_parent(obj
):
1083 orphan_labels
, orphan_widgets
= orphan_items(filename
, tree
, root
, obj
)
1085 if obj
.tag
== 'object':
1086 check_elm(orphan_root
, obj
, orphan_labels
, orphan_widgets
)
1089 recurse(orphan_root
, o
, orphan_labels
, orphan_widgets
)
1091 recurse(root
, root
, False, False)
1097 def usage(fatal
= True):
1098 print("`%s' checks accessibility of glade .ui files" % progname
)
1100 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname
)
1102 print(" -p Print XML class path instead of line number")
1103 print(" -g Generate suppression file SUPPR_FILE")
1104 print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
1105 print(" -f Suppress warnings given by file SUPPR_FILE completely")
1106 print(" -P Remove PREFIX from file names in warnings")
1107 print(" -o Also prints errors and warnings to given file")
1109 print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
1110 print(" Give or extend one of the lists of widget classes, where FOO can be:")
1111 print(" - toplevel : widgets to be considered toplevel windows")
1112 print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
1113 print(" - suffixignored : suffixes of widget classes which do not need labelling")
1114 print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
1115 print(" - buttons : widgets which need their own label but not more")
1116 print(" (e.g. GtkButton)")
1117 print(" - labels : widgets which provide labels (e.g. GtkLabel)")
1118 print(" --widgets-print print default widgets lists")
1120 print(" --enable-all enable all warnings/dofatals (default)")
1121 print(" --disable-all disable all warnings/dofatals")
1122 print(" --fatal-all make all warnings dofatals")
1123 print(" --not-fatal-all do not make all warnings dofatals (default)")
1125 print(" --enable-type=TYPE enable warning/fatal type TYPE")
1126 print(" --disable-type=TYPE disable warning/fatal type TYPE")
1127 print(" --fatal-type=TYPE make warning type TYPE a fatal")
1128 print(" --not-fatal-type=TYPE make warning type TYPE not a fatal")
1130 print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
1131 print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
1132 print(" --fatal-widgets=CLASS make warning type CLASS a fatal")
1133 print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal")
1135 print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
1136 print(" class CLASS")
1137 print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
1138 print(" class CLASS")
1139 print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget")
1140 print(" class CLASS")
1141 print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget")
1142 print(" class CLASS")
1144 print(" --disable-orphan-labels only warn about orphan labels when there are")
1145 print(" orphan widgets in the same context")
1147 print("Report bugs to <bugs@hypra.fr>")
1148 sys
.exit(2 if fatal
else 0)
1150 def widgets_opt(widgets_list
, arg
):
1152 Replace or extend `widgets_list' with the list of classes contained in `arg'
1154 append
= arg
and arg
[0] == '+'
1159 widgets
= arg
.split(',')
1166 widgets_list
.extend(widgets
)
1170 global pflag
, gen_suppr
, gen_supprfile
, suppressions
, suppr_prefix
, false_positives
, dofatals
, enables
, dofatals
, warn_orphan_labels
1171 global widgets_toplevel
, widgets_ignored
, widgets_suffixignored
, widgets_needlabel
, widgets_buttons
, widgets_labels
1175 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hpiIg:s:f:P:o:L:", [
1179 "widgets-toplevel=",
1181 "widgets-suffixignored=",
1182 "widgets-needlabel=",
1200 "not-fatal-widgets=",
1203 "disable-specific=",
1205 "not-fatal-specific=",
1207 "disable-orphan-labels",
1209 except getopt
.GetoptError
:
1218 if o
== "--help" or o
== "-h":
1220 if o
== "--version":
1238 elif o
== "--widgets-toplevel":
1239 widgets_opt(widgets_toplevel
, a
)
1240 elif o
== "--widgets-ignored":
1241 widgets_opt(widgets_ignored
, a
)
1242 elif o
== "--widgets-suffixignored":
1243 widgets_opt(widgets_suffixignored
, a
)
1244 elif o
== "--widgets-needlabel":
1245 widgets_opt(widgets_needlabel
, a
)
1246 elif o
== "--widgets-buttons":
1247 widgets_opt(widgets_buttons
, a
)
1248 elif o
== "--widgets-labels":
1249 widgets_opt(widgets_labels
, a
)
1250 elif o
== "--widgets-print":
1251 print("--widgets-toplevel '" + ','.join(widgets_toplevel
) + "'")
1252 print("--widgets-ignored '" + ','.join(widgets_ignored
) + "'")
1253 print("--widgets-suffixignored '" + ','.join(widgets_suffixignored
) + "'")
1254 print("--widgets-needlabel '" + ','.join(widgets_needlabel
) + "'")
1255 print("--widgets-buttons '" + ','.join(widgets_buttons
) + "'")
1256 print("--widgets-labels '" + ','.join(widgets_labels
) + "'")
1259 elif o
== '--enable-all':
1260 enables
.append( (True, None, None) )
1261 elif o
== '--disable-all':
1262 enables
.append( (False, None, None) )
1263 elif o
== '--fatal-all':
1264 dofatals
.append( (True, None, None) )
1265 elif o
== '--not-fatal-all':
1266 dofatals
.append( (False, None, None) )
1268 elif o
== '--enable-type':
1269 enables
.append( (True, a
, None) )
1270 elif o
== '--disable-type':
1271 enables
.append( (False, a
, None) )
1272 elif o
== '--fatal-type':
1273 dofatals
.append( (True, a
, None) )
1274 elif o
== '--not-fatal-type':
1275 dofatals
.append( (False, a
, None) )
1277 elif o
== '--enable-widgets':
1278 enables
.append( (True, None, a
) )
1279 elif o
== '--disable-widgets':
1280 enables
.append( (False, None, a
) )
1281 elif o
== '--fatal-widgets':
1282 dofatals
.append( (True, None, a
) )
1283 elif o
== '--not-fatal-widgets':
1284 dofatals
.append( (False, None, a
) )
1286 elif o
== '--enable-specific':
1287 (thetype
, klass
) = a
.split('.', 1)
1288 enables
.append( (True, thetype
, klass
) )
1289 elif o
== '--disable-specific':
1290 (thetype
, klass
) = a
.split('.', 1)
1291 enables
.append( (False, thetype
, klass
) )
1292 elif o
== '--fatal-specific':
1293 (thetype
, klass
) = a
.split('.', 1)
1294 dofatals
.append( (True, thetype
, klass
) )
1295 elif o
== '--not-fatal-specific':
1296 (thetype
, klass
) = a
.split('.', 1)
1297 dofatals
.append( (False, thetype
, klass
) )
1299 elif o
== '--disable-orphan-labels':
1300 warn_orphan_labels
= False
1302 # Read suppression file before overwriting it
1303 if suppr
is not None:
1305 supprfile
= open(suppr
, 'r')
1307 for line
in supprfile
.readlines():
1308 prefix
= line
.rstrip()
1309 suppressions
[prefix
] = True
1310 suppressions_to_line
[prefix
] = line_no
1311 line_no
= line_no
+ 1;
1316 # Read false positives file
1317 if false
is not None:
1319 falsefile
= open(false
, 'r')
1320 for line
in falsefile
.readlines():
1321 prefix
= line
.rstrip()
1322 false_positives
[prefix
] = True
1328 outfile
= open(out
, 'w')
1330 if filelist
is not None:
1332 filelistfile
= open(filelist
, 'r')
1333 for line
in filelistfile
.readlines():
1336 args
+= line
.split(' ')
1337 filelistfile
.close()
1339 err(filelist
, None, None, "unable to read file list file")
1341 for filename
in args
:
1343 tree
= ET
.parse(filename
)
1344 except ET
.ParseError
:
1345 err(filename
, None, None, "parse", "malformatted xml file")
1348 err(filename
, None, None, None, "unable to read file")
1352 check_a11y_relation(filename
, tree
)
1353 except Exception as error
:
1355 traceback
.print_exc()
1356 err(filename
, None, None, "parse", "error parsing file")
1358 if errors
> 0 or errexists
> 0:
1359 estr
= "%s new error%s" % (errors
, 's' if errors
> 1 else '')
1361 estr
+= " (%s suppressed by %s)" % (errexists
, suppr
)
1364 if warnings
> 0 or warnexists
> 0:
1365 wstr
= "%s new warning%s" % (warnings
, 's' if warnings
> 1 else '')
1367 wstr
+= " (%s suppressed by %s)" % (warnexists
, suppr
)
1370 if fatals
> 0 or fatalexists
> 0:
1371 wstr
= "%s new fatal%s" % (fatals
, 's' if fatals
> 1 else '')
1373 wstr
+= " (%s suppressed by %s)" % (fatalexists
, suppr
)
1377 for (suppr
,unused
) in suppressions
.items():
1382 print("%s suppression%s unused:" % (n
, 's' if n
> 1 else ''))
1383 for (suppr
,unused
) in suppressions
.items():
1385 print(" %s:%s" % (suppressions_to_line
[suppr
], suppr
))
1387 if gen_supprfile
is not None:
1388 gen_supprfile
.close()
1389 if outfile
is not None:
1391 if fatals
> 0 and gen_suppr
is None:
1392 print("Explanations are available on https://wiki.documentfoundation.org/Development/Accessibility")
1396 if __name__
== "__main__":
1399 except KeyboardInterrupt:
1402 # vim: set shiftwidth=4 softtabstop=4 expandtab: