tdf#131510 fix German autocorrect list for 'daß'
[LibreOffice.git] / bin / gla11y
blob7883a623319950e0fde3a1856edfb3d47ff37fb6
1 #!/usr/bin/env python
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
31 import os
32 import sys
33 import getopt
34 try:
35 import lxml.etree as ET
36 lxml = True
37 except ImportError:
38 if sys.version_info < (2,7):
39 print("gla11y needs lxml or python >= 2.7")
40 exit()
41 import xml.etree.ElementTree as ET
42 lxml = False
44 howto_url = "https://wiki.documentfoundation.org/Development/Accessibility"
46 # Toplevel widgets
47 widgets_toplevel = [
48 'GtkWindow',
49 'GtkOffscreenWindow',
50 'GtkApplicationWindow',
51 'GtkDialog',
52 'GtkFileChooserDialog',
53 'GtkColorChooserDialog',
54 'GtkFontChooserDialog',
55 'GtkMessageDialog',
56 'GtkRecentChooserDialog',
57 'GtkAssistant',
58 'GtkAppChooserDialog',
59 'GtkPrintUnixDialog',
60 'GtkShortcutsWindow',
63 widgets_ignored = widgets_toplevel + [
64 # Containers
65 'GtkBox',
66 'GtkGrid',
67 'GtkNotebook',
68 'GtkFrame',
69 'GtkAspectFrame',
70 'GtkListBox',
71 'GtkFlowBox',
72 'GtkOverlay',
73 'GtkMenuBar',
74 'GtkToolbar',
75 'GtkToolpalette',
76 'GtkPaned',
77 'GtkHPaned',
78 'GtkVPaned',
79 'GtkButtonBox',
80 'GtkHButtonBox',
81 'GtkVButtonBox',
82 'GtkLayout',
83 'GtkFixed',
84 'GtkEventBox',
85 'GtkExpander',
86 'GtkViewport',
87 'GtkScrolledWindow',
88 'GtkAlignment',
89 'GtkRevealer',
90 'GtkSearchBar',
91 'GtkHeaderBar',
92 'GtkStack',
93 'GtkPopover',
94 'GtkPopoverMenu',
95 'GtkActionBar',
96 'GtkHandleBox',
97 'GtkShortcutsSection',
98 'GtkShortcutsGroup',
99 'GtkTable',
101 'GtkVBox',
102 'GtkHBox',
103 'GtkToolItem',
104 'GtkMenu',
106 # Invisible actions
107 'GtkSeparator',
108 'GtkHSeparator',
109 'GtkVSeparator',
110 'GtkAction',
111 'GtkToggleAction',
112 'GtkActionGroup',
113 'GtkCellRendererGraph',
114 'GtkCellRendererPixbuf',
115 'GtkCellRendererProgress',
116 'GtkCellRendererSpin',
117 'GtkCellRendererText',
118 'GtkCellRendererToggle',
119 'GtkSeparatorMenuItem',
120 'GtkSeparatorToolItem',
122 # Storage objects
123 'GtkListStore',
124 'GtkTreeStore',
125 'GtkTreeModelFilter',
126 'GtkTreeModelSort',
128 'GtkEntryBuffer',
129 'GtkTextBuffer',
130 'GtkTextTag',
131 'GtkTextTagTable',
133 'GtkSizeGroup',
134 'GtkWindowGroup',
135 'GtkAccelGroup',
136 'GtkAdjustment',
137 'GtkEntryCompletion',
138 'GtkIconFactory',
139 'GtkStatusIcon',
140 'GtkFileFilter',
141 'GtkRecentFilter',
142 'GtkRecentManager',
143 'GThemedIcon',
145 'GtkTreeSelection',
147 'GtkListBoxRow',
148 'GtkTreeViewColumn',
150 # Useless to label
151 'GtkScrollbar',
152 'GtkHScrollbar',
153 'GtkStatusbar',
154 'GtkInfoBar',
156 # These are actually labels
157 'GtkLinkButton',
159 # This precisely give a11y information :)
160 'AtkObject',
163 widgets_suffixignored = [
166 # These widgets always need a label
167 widgets_needlabel = [
168 'GtkEntry',
169 'GtkSearchEntry',
170 'GtkScale',
171 'GtkHScale',
172 'GtkVScale',
173 'GtkSpinButton',
174 'GtkSwitch',
177 # These widgets normally have their own label
178 widgets_buttons = [
179 'GtkButton',
180 'GtkToolButton',
181 'GtkToggleButton',
182 'GtkToggleToolButton',
183 'GtkRadioButton',
184 'GtkRadioToolButton',
185 'GtkCheckButton',
186 'GtkModelButton',
187 'GtkLockButton',
188 'GtkColorButton',
189 'GtkMenuButton',
191 'GtkMenuItem',
192 'GtkImageMenuItem',
193 'GtkMenuToolButton',
194 'GtkRadioMenuItem',
195 'GtkCheckMenuItem',
198 # These widgets are labels that can label other widgets
199 widgets_labels = [
200 'GtkLabel',
201 'GtkAccelLabel',
204 # The rest should probably be labelled if there are orphan labels
206 # GtkSpinner
207 # GtkProgressBar
208 # GtkLevelBar
210 # GtkComboBox
211 # GtkComboBoxText
212 # GtkFileChooserButton
213 # GtkAppChooserButton
214 # GtkFontButton
215 # GtkCalendar
216 # GtkColorChooserWidget
218 # GtkCellView
219 # GtkTreeView
220 # GtkTextView
221 # GtkIconView
223 # GtkImage
224 # GtkArrow
225 # GtkDrawingArea
227 # GtkScaleButton
228 # GtkVolumeButton
231 # TODO:
232 # GtkColorPlane ?
233 # GtkColorScale ?
234 # GtkColorSwatch ?
235 # GtkFileChooserWidget ?
236 # GtkFishbowl ?
237 # GtkFontChooserWidget ?
238 # GtkIcon ?
239 # GtkInspector* ?
240 # GtkMagnifier ?
241 # GtkPathBar ?
242 # GtkPlacesSidebar ?
243 # GtkPlacesView ?
244 # GtkPrinterOptionWidget ?
245 # GtkStackCombo ?
246 # GtkStackSidebar ?
247 # GtkStackSwitcher ?
249 progname = os.path.basename(sys.argv[0])
251 # This dictionary contains the set of suppression lines as read from the
252 # suppression file(s). It is merely indexed by the text of the suppression line
253 # and contains whether the suppressions was unused.
254 suppressions = {}
256 # This dictionary is indexed like suppressions and returns a "file:line" string
257 # to report where in the suppression file the suppression was read
258 suppressions_to_line = {}
260 # This dictionary is similar to the suppressions dictionary, but for false
261 # positives rather than suppressions
262 false_positives = {}
264 # This dictionary is indexed by the xml id and returns the element object.
265 ids = {}
266 # This dictionary is indexed by the xml id and returns whether several objects
267 # have the same id.
268 ids_dup = {}
270 # This dictionary is indexed by the xml id of an element A and returns the list
271 # of objects which are labelled-by A.
272 labelled_by_elm = {}
274 # This dictionary is indexed by the xml id of an element A and returns the list
275 # of objects which are label-for A.
276 label_for_elm = {}
278 # This dictionary is indexed by the xml id of an element A and returns the list
279 # of objects which have a mnemonic-for A.
280 mnemonic_for_elm = {}
282 # Possibly a file name to put generated suppression lines in
283 gen_suppr = None
284 # The corresponding opened file
285 gen_supprfile = None
286 # A prefix to remove from file names in the generated suppression lines
287 suppr_prefix = ""
289 # Possibly an opened file in which our output should also be written to.
290 outfile = None
292 # Whether -p option was set, i.e. print XML class path instead of line number in
293 # the output
294 pflag = False
296 # Whether we should warn about labels which are orphan
297 warn_orphan_labels = True
299 # Number of errors
300 errors = 0
301 # Number of suppressed errors
302 errexists = 0
303 # Number of warnings
304 warnings = 0
305 # Number of suppressed warnings
306 warnexists = 0
307 # Number of fatal errors
308 fatals = 0
309 # Number of suppressed fatal errors
310 fatalexists = 0
312 # List of warnings and errors which are fatal
314 # Format of each element: (enabled, type, class)
315 # See the is_enabled function: the list is traversed completely, each element
316 # can specify whether it enables or disables the warning, possibly the type of
317 # warning to be enabled/disabled, possibly the class of XML element for which it
318 # should be enabled.
320 # This mechanism matches the semantic of the parameters on the command line,
321 # each of which refining the semantic set by the previous parameters
322 dofatals = [ ]
324 # List of warnings and errors which are enabled
325 # Same format as dofatals
326 enables = [ ]
328 # buffers all printed output, so it isn't split in parallel builds
329 output_buffer = ""
332 # XML browsing and printing functions
335 def elm_parent(root, elm):
337 Return the parent of the element.
339 if lxml:
340 return elm.getparent()
341 else:
342 def find_parent(cur, elm):
343 for o in cur:
344 if o == elm:
345 return cur
346 parent = find_parent(o, elm)
347 if parent is not None:
348 return parent
349 return None
350 return find_parent(root, elm)
352 def step_elm(elm):
354 Return the XML class path step corresponding to elm.
355 This can be empty if the elm does not have any class or id.
357 step = elm.attrib.get('class')
358 if step is None:
359 step = ""
360 oid = elm.attrib.get('id')
361 if oid is not None:
362 oid = oid.encode('ascii','ignore').decode('ascii')
363 step += "[@id='%s']" % oid
364 if len(step) > 0:
365 step += '/'
366 return step
368 def find_elm(root, elm):
370 Return the XML class path of the element from the given root.
371 This is the slow version used when getparent is not available.
373 if root == elm:
374 return ""
375 for o in root:
376 path = find_elm(o, elm)
377 if path is not None:
378 step = step_elm(o)
379 return step + path
380 return None
382 def errpath(filename, tree, elm):
384 Return the XML class path of the element
386 if elm is None:
387 return ""
388 path = ""
389 if 'class' in elm.attrib:
390 path += elm.attrib['class']
391 oid = elm.attrib.get('id')
392 if oid is not None:
393 oid = oid.encode('ascii','ignore').decode('ascii')
394 path = "//" + path + "[@id='%s']" % oid
395 else:
396 if lxml:
397 elm = elm.getparent()
398 while elm is not None:
399 step = step_elm(elm)
400 path = step + path
401 elm = elm.getparent()
402 else:
403 path = find_elm(tree.getroot(), elm)[:-1]
404 path = filename + ':' + path
405 return path
408 # Warning/Error printing functions
411 def elm_prefix(filename, elm):
413 Return the display prefix of the element
415 if elm == None or not lxml:
416 return "%s:" % filename
417 else:
418 return "%s:%u" % (filename, elm.sourceline)
420 def elm_name(elm):
422 Return a display name of the element
424 if elm is not None:
425 name = ""
426 if 'class' in elm.attrib:
427 name = "'%s' " % elm.attrib['class']
428 if 'id' in elm.attrib:
429 id = elm.attrib['id'].encode('ascii','ignore').decode('ascii')
430 name += "'%s' " % id
431 if not name:
432 name = "'" + elm.tag + "'"
433 if lxml:
434 name += " line " + str(elm.sourceline)
435 return name
436 return ""
438 def elm_name_line(elm):
440 Return a display name of the element with line number
442 if elm is not None:
443 name = elm_name(elm)
444 if lxml and " line " not in name:
445 name += "line " + str(elm.sourceline) + " "
446 return name
447 return ""
449 def elm_line(elm):
451 Return the line for the given element.
453 if lxml:
454 return " line " + str(elm.sourceline)
455 else:
456 return ""
458 def elms_lines(elms):
460 Return the list of lines for the given elements.
462 if lxml:
463 return " lines " + ', '.join([str(l.sourceline) for l in elms])
464 else:
465 return ""
467 def elms_names_lines(elms):
469 Return the list of names and lines for the given elements.
471 return ', '.join([elm_name_line(elm) for elm in elms])
473 def elm_suppr(filename, tree, elm, msgtype, dogen):
475 Return the prefix to be displayed to the user and the suppression line for
476 the warning type "msgtype" for element "elm"
478 global gen_suppr, gen_supprfile, suppr_prefix, pflag
480 if suppressions or false_positives or gen_suppr is not None or pflag:
481 prefix = errpath(filename, tree, elm)
482 if prefix[0:len(suppr_prefix)] == suppr_prefix:
483 prefix = prefix[len(suppr_prefix):]
485 if suppressions or false_positives or gen_suppr is not None:
486 suppr = '%s %s' % (prefix, msgtype)
488 if gen_suppr is not None and msgtype is not None and dogen:
489 if gen_supprfile is None:
490 gen_supprfile = open(gen_suppr, 'w')
491 print(suppr, file=gen_supprfile)
492 else:
493 suppr = None
495 if not pflag:
496 # Use user-friendly line numbers
497 prefix = elm_prefix(filename, elm)
498 if prefix[0:len(suppr_prefix)] == suppr_prefix:
499 prefix = prefix[len(suppr_prefix):]
501 return (prefix, suppr)
503 def is_enabled(elm, msgtype, l, default):
505 Test whether warning type msgtype is enabled for elm in l
507 enabled = default
508 for (enable, thetype, klass) in l:
509 # Match warning type
510 if thetype is not None:
511 if thetype != msgtype:
512 continue
513 # Match elm class
514 if klass is not None and elm is not None:
515 if klass != elm.attrib.get('class'):
516 continue
517 enabled = enable
518 return enabled
520 def err(filename, tree, elm, msgtype, msg, error = True):
522 Emit a warning or error for an element
524 global errors, errexists, warnings, warnexists, fatals, fatalexists, output_buffer
526 # Let user tune whether a warning or error
527 fatal = is_enabled(elm, msgtype, dofatals, error)
529 # By default warnings and errors are enabled, but let user tune it
530 if not is_enabled(elm, msgtype, enables, True):
531 return
533 (prefix, suppr) = elm_suppr(filename, tree, elm, msgtype, True)
534 if suppr in false_positives:
535 # That was actually expected
536 return
537 if suppr in suppressions:
538 # Suppressed
539 suppressions[suppr] = False
540 if fatal:
541 fatalexists += 1
542 if error:
543 errexists += 1
544 else:
545 warnexists += 1
546 return
548 if error:
549 errors += 1
550 else:
551 warnings += 1
552 if fatal:
553 fatals += 1
555 msg = "%s %s%s: %s%s" % (prefix,
556 "FATAL " if fatal else "",
557 "ERROR" if error else "WARNING",
558 elm_name(elm), msg)
559 output_buffer += msg + "\n"
560 if outfile is not None:
561 print(msg, file=outfile)
563 def warn(filename, tree, elm, msgtype, msg):
565 Emit a warning for an element
567 err(filename, tree, elm, msgtype, msg, False)
570 # Labelling testing functions
573 def find_button_parent(root, elm):
575 Find a parent which is a button
577 if lxml:
578 parent = elm.getparent()
579 if parent is not None:
580 if parent.attrib.get('class') in widgets_buttons:
581 return parent
582 return find_button_parent(root, parent)
583 else:
584 def find_parent(cur, elm):
585 for o in cur:
586 if o == elm:
587 if cur.attrib.get('class') in widgets_buttons:
588 # we are the button, immediately above the target
589 return cur
590 else:
591 # we aren't the button, but target is over there
592 return True
593 parent = find_parent(o, elm)
594 if parent == True:
595 # It is over there, but didn't find a button yet
596 if cur.attrib.get('class') in widgets_buttons:
597 # we are the button
598 return cur
599 else:
600 return True
601 if parent is not None:
602 # we have the button parent over there
603 return parent
604 return None
605 parent = find_parent(root, elm)
606 if parent == True:
607 parent = None
608 return parent
611 def is_labelled_parent(elm):
613 Return whether this element is a labelled parent
615 klass = elm.attrib.get('class')
616 if klass in widgets_toplevel:
617 return True
618 if klass == 'GtkShortcutsGroup':
619 children = elm.findall("property[@name='title']")
620 if len(children) >= 1:
621 return True
622 if klass == 'GtkFrame' or klass == 'GtkNotebook':
623 children = elm.findall("child[@type='tab']") + elm.findall("child[@type='label']")
624 if len(children) >= 1:
625 return True
626 return False
628 def elm_labelled_parent(root, elm):
630 Return the first labelled parent of the element, which can thus be used as
631 the root of widgets with common labelled context
634 if lxml:
635 def find_labelled_parent(elm):
636 if is_labelled_parent(elm):
637 return elm
638 parent = elm.getparent()
639 if parent is None:
640 return None
641 return find_labelled_parent(parent)
642 parent = elm.getparent()
643 if parent is None:
644 return None
645 return find_labelled_parent(elm.getparent())
646 else:
647 def find_labelled_parent(cur, elm):
648 if cur == elm:
649 # the target element is over there
650 return True
651 for o in cur:
652 parent = find_labelled_parent(o, elm)
653 if parent == True:
654 # target element is over there, check ourself
655 if is_labelled_parent(cur):
656 # yes, and we are the first ancestor of the target element
657 return cur
658 else:
659 # no, but target element is over there.
660 return True
661 if parent != None:
662 # the first ancestor of the target element was over there
663 return parent
664 return None
665 parent = find_labelled_parent(root, elm)
666 if parent == True:
667 parent = None
668 return parent
670 def is_orphan_label(filename, tree, root, obj, orphan_root, doprint = False):
672 Check whether this label has no accessibility relation, or doubtful relation
673 because another label labels the same target
675 global label_for_elm, labelled_by_elm, mnemonic_for_elm, warnexists
677 # label-for
678 label_for = obj.findall("accessibility/relation[@type='label-for']")
679 for rel in label_for:
680 target = rel.attrib['target']
681 l = label_for_elm[target]
682 if len(l) > 1:
683 return True
685 # mnemonic_widget
686 mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \
687 obj.findall("property[@name='mnemonic-widget']")
688 for rel in mnemonic_for:
689 target = rel.text
690 l = mnemonic_for_elm[target]
691 if len(l) > 1:
692 return True
694 if len(label_for) > 0:
695 # At least one label-for, we are not orphan.
696 return False
698 if len(mnemonic_for) > 0:
699 # At least one mnemonic_widget, we are not orphan.
700 return False
702 labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
703 if len(labelled_by) > 0:
704 # Oh, a labelled label, probably not to be labelling anything
705 return False
707 # explicit role?
708 roles = [x.text for x in obj.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")]
709 roles += [x.attrib.get("type") for x in obj.findall("accessibility/role")]
710 if len(roles) > 1 and doprint:
711 err(filename, tree, obj, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>"
712 "%s" % elms_lines(children))
713 for role in roles:
714 if role == 'static' or role == 'ATK_ROLE_STATIC':
715 # This is static text, not meant to label anything
716 return False
718 parent = elm_parent(root, obj)
719 if parent is not None:
720 childtype = parent.attrib.get('type')
721 if childtype is None:
722 childtype = parent.attrib.get('internal-child')
723 if parent.tag == 'child' and childtype == 'label' \
724 or childtype == 'tab':
725 # This is a frame or a notebook label, not orphan.
726 return False
728 if find_button_parent(root, obj) is not None:
729 # This label is part of a button
730 return False
732 oid = obj.attrib.get('id')
733 if oid is not None:
734 if oid in labelled_by_elm:
735 # Some widget is labelled by us, we are not orphan.
736 # We should have had a label-for, will warn about it later.
737 return False
739 # No label-for, no mnemonic-for, no labelled-by, we are orphan.
740 (_, suppr) = elm_suppr(filename, tree, obj, "orphan-label", False)
741 if suppr in false_positives:
742 # That was actually expected
743 return False
744 if suppr in suppressions:
745 # Warning suppressed for this label
746 if suppressions[suppr]:
747 warnexists += 1
748 suppressions[suppr] = False
749 return False
751 if doprint:
752 context = elm_name(orphan_root)
753 if context:
754 context = " within " + context
755 warn(filename, tree, obj, "orphan-label", "does not specify what it labels" + context)
756 return True
758 def is_orphan_widget(filename, tree, root, obj, orphan, orphan_root, doprint = False):
760 Check whether this widget has no accessibility relation.
762 global warnexists
763 if obj.tag != 'object':
764 return False
766 oid = obj.attrib.get('id')
767 klass = obj.attrib.get('class')
769 # "Don't care" special case
770 if klass in widgets_ignored:
771 return False
772 for suffix in widgets_suffixignored:
773 if klass[-len(suffix):] == suffix:
774 return False
776 # Widgets usual do not strictly require a label, i.e. a labelled parent
777 # is enough for context, but some do always need one.
778 requires_label = klass in widgets_needlabel
780 labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
782 # Labels special case
783 if klass in widgets_labels:
784 return False
786 # Case 1: has an explicit <child internal-child="accessible"> sub-element
787 children = obj.findall("child[@internal-child='accessible']")
788 if len(children) > 1 and doprint:
789 err(filename, tree, obj, "multiple-accessible", "has multiple <child internal-child='accessible'>"
790 "%s" % elms_lines(children))
791 if len(children) >= 1:
792 return False
794 # Case 2: has an <accessibility> sub-element with a "labelled-by"
795 # <relation> pointing to an existing element.
796 if len(labelled_by) > 0:
797 return False
799 # Case 3: has a label-for
800 if oid in label_for_elm:
801 return False
803 # Case 4: has a mnemonic
804 if oid in mnemonic_for_elm:
805 return False
807 # Case 5: Has a <property name="tooltip_text">
808 tooltips = obj.findall("property[@name='tooltip_text']") + \
809 obj.findall("property[@name='tooltip-text']")
810 if len(tooltips) > 1 and doprint:
811 err(filename, tree, obj, "multiple-tooltip", "has multiple tooltip_text properties")
812 if len(tooltips) >= 1 and klass != 'GtkCheckButton':
813 return False
815 # Case 6: Has a <property name="placeholder_text">
816 placeholders = obj.findall("property[@name='placeholder_text']") + \
817 obj.findall("property[@name='placeholder-text']")
818 if len(placeholders) > 1 and doprint:
819 err(filename, tree, obj, "multiple-placeholder", "has multiple placeholder_text properties")
820 if len(placeholders) >= 1:
821 return False
823 # Buttons usually don't need an external label, their own is enough, (but they do need one)
824 if klass in widgets_buttons:
826 labels = obj.findall("property[@name='label']")
827 if len(labels) > 1 and doprint:
828 err(filename, tree, obj, "multiple-label", "has multiple label properties")
829 if len(labels) >= 1:
830 # Has a <property name="label">
831 return False
833 actions = obj.findall("property[@name='action_name']")
834 if len(actions) > 1 and doprint:
835 err(filename, tree, obj, "multiple-action_name", "has multiple action_name properties")
836 if len(actions) >= 1:
837 # Has a <property name="action_name">
838 return False
840 # Uses id as an action_name
841 if 'id' in obj.attrib:
842 if obj.attrib['id'].startswith(".uno:"):
843 return False
845 gtklabels = obj.findall(".//object[@class='GtkLabel']") + obj.findall(".//object[@class='GtkAccelLabel']")
846 if len(gtklabels) >= 1:
847 # Has a custom label
848 return False
850 # no label for a button, warn
851 if doprint:
852 warn(filename, tree, obj, "button-no-label", "does not have its own label")
853 if not is_enabled(obj, "button-no-label", enables, True):
854 # Warnings disabled
855 return False
856 (_, suppr) = elm_suppr(filename, tree, obj, "button-no-label", False)
857 if suppr in false_positives:
858 # That was actually expected
859 return False
860 if suppr in suppressions:
861 # Warning suppressed for this widget
862 if suppressions[suppr]:
863 warnexists += 1
864 suppressions[suppr] = False
865 return False
866 return True
868 # GtkImages special case
869 if klass == "GtkImage":
870 uses = [u for u in tree.iterfind(".//object/property[@name='image']") if u.text == oid]
871 if len(uses) > 0:
872 # This image is just used by another element, don't warn
873 # about the image itself, we probably want the warning on
874 # the element instead.
875 return False
877 if find_button_parent(root, obj) is not None:
878 # This image is part of a button, we want the warning on the button
879 # instead, if any.
880 return False
882 # GtkEntry special case
883 if klass == 'GtkEntry' or klass == 'GtkSearchEntry':
884 parent = elm_parent(root, obj)
885 if parent is not None:
886 if parent.tag == 'child' and \
887 parent.attrib.get('internal-child') == "entry":
888 # This is an internal entry of another widget. Relations
889 # will be handled by that widget.
890 return False
892 # GtkShortcutsShortcut special case
893 if klass == 'GtkShortcutsShortcut':
894 children = obj.findall("property[@name='title']")
895 if len(children) >= 1:
896 return False
898 # Really no label, perhaps emit a warning
899 if not is_enabled(obj, "no-labelled-by", enables, True):
900 # Warnings disabled for this class of widgets
901 return False
902 (_, suppr) = elm_suppr(filename, tree, obj, "no-labelled-by", False)
903 if suppr in false_positives:
904 # That was actually expected
905 return False
906 if suppr in suppressions:
907 # Warning suppressed for this widget
908 if suppressions[suppr]:
909 warnexists += 1
910 suppressions[suppr] = False
911 return False
913 if not orphan:
914 # No orphan label, so probably the labelled parent provides enough
915 # context.
916 if requires_label:
917 # But these always need a label.
918 if doprint:
919 warn(filename, tree, obj, "no-labelled-by", "has no accessibility label")
920 return True
921 return False
923 if doprint:
924 context = elm_name(orphan_root)
925 if context:
926 context = " within " + context
927 warn(filename, tree, obj, "no-labelled-by", "has no accessibility label while there are orphan labels" + context)
928 return True
930 def orphan_items(filename, tree, root, elm):
932 Check whether from some element there exists orphan labels and orphan widgets
934 orphan_labels = False
935 orphan_widgets = False
936 if elm.attrib.get('class') in widgets_labels:
937 orphan_labels = is_orphan_label(filename, tree, root, elm, None)
938 else:
939 orphan_widgets = is_orphan_widget(filename, tree, root, elm, True, None)
940 for obj in elm:
941 # We are not interested in orphan labels under another labelled
942 # parent. This also allows to keep linear complexity.
943 if not is_labelled_parent(obj):
944 label, widget = orphan_items(filename, tree, root, obj)
945 if label:
946 orphan_labels = True
947 if widget:
948 orphan_widgets = True
949 if orphan_labels and orphan_widgets:
950 # No need to look up more
951 break
952 return orphan_labels, orphan_widgets
955 # UI accessibility checks
958 def check_props(filename, tree, root, elm, forward):
960 Check the given list of relation properties
962 props = elm.findall("property[@name='" + forward + "']")
963 for prop in props:
964 if prop.text not in ids:
965 err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % prop.text)
966 return props
968 def is_visible(obj):
969 visible = False
970 visible_prop = obj.findall("property[@name='visible']")
971 visible_len = len(visible_prop)
972 if visible_len:
973 visible_txt = visible_prop[visible_len - 1].text
974 if visible_txt.lower() == "true":
975 visible = True
976 elif visible_txt.lower() == "false":
977 visible = False
978 return visible
980 def check_rels(filename, tree, root, elm, forward, backward = None):
982 Check the relations given by forward
984 oid = elm.attrib.get('id')
985 rels = elm.findall("accessibility/relation[@type='" + forward + "']")
986 for rel in rels:
987 target = rel.attrib['target']
988 if target not in ids:
989 err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % target)
990 elif backward is not None:
991 widget = ids[target]
992 backrels = widget.findall("accessibility/relation[@type='" + backward + "']")
993 if len([x for x in backrels if x.attrib['target'] == oid]) == 0:
994 err(filename, tree, elm, "missing-" + backward, "has " + forward + \
995 ", but is not " + backward + " by " + elm_name_line(widget))
996 return rels
998 def check_a11y_relation(filename, tree):
1000 Emit an error message if any of the 'object' elements of the XML
1001 document represented by `root' doesn't comply with Accessibility
1002 rules.
1004 global widgets_ignored, ids, label_for_elm, labelled_by_elm, mnemonic_for_elm
1006 def check_elm(orphan_root, obj, orphan_labels, orphan_widgets):
1008 Check one element, knowing that orphan_labels/widgets tell whether
1009 there are orphan labels and widgets within orphan_root
1012 oid = obj.attrib.get('id')
1013 klass = obj.attrib.get('class')
1015 # "Don't care" special case
1016 if klass in widgets_ignored:
1017 return
1018 for suffix in widgets_suffixignored:
1019 if klass[-len(suffix):] == suffix:
1020 return
1022 # Widgets usual do not strictly require a label, i.e. a labelled parent
1023 # is enough for context, but some do always need one.
1024 requires_label = klass in widgets_needlabel
1026 if oid is not None:
1027 # Check that ids are unique
1028 if oid in ids_dup:
1029 if ids[oid] == obj:
1030 # We are the first, warn
1031 duplicates = tree.findall(".//object[@id='" + oid + "']")
1032 err(filename, tree, obj, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates))
1034 # Check label-for and their dual labelled-by
1035 label_for = check_rels(filename, tree, root, obj, "label-for", "labelled-by")
1037 # Check labelled-by and its dual label-for
1038 labelled_by = check_rels(filename, tree, root, obj, "labelled-by", "label-for")
1040 visible = is_visible(obj)
1042 # warning message type "syntax" used:
1044 # multiple-* => 2+ XML tags of the inspected element itself
1045 # duplicate-* => 2+ XML tags of other elements referencing this element
1047 # Should have only one label
1048 if len(labelled_by) >= 1:
1049 if oid in mnemonic_for_elm:
1050 warn(filename, tree, obj, "labelled-by-and-mnemonic",
1051 "has both a mnemonic " + elm_name_line(mnemonic_for_elm[oid][0]) + "and labelled-by relation")
1052 if len(labelled_by) > 1:
1053 warn(filename, tree, obj, "multiple-labelled-by", "has multiple labelled-by relations")
1055 if oid in labelled_by_elm:
1056 if len(labelled_by_elm[oid]) == 1:
1057 paired = labelled_by_elm[oid][0]
1058 if paired != None and visible != is_visible(paired):
1059 warn(filename, tree, obj, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired))
1061 if oid in label_for_elm:
1062 if len(label_for_elm[oid]) > 1:
1063 warn(filename, tree, obj, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm[oid]))
1064 elif len(label_for_elm[oid]) == 1:
1065 paired = label_for_elm[oid][0]
1066 if visible != is_visible(paired):
1067 warn(filename, tree, obj, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired))
1069 if oid in mnemonic_for_elm:
1070 if len(mnemonic_for_elm[oid]) > 1:
1071 warn(filename, tree, obj, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm[oid]))
1073 # Check member-of
1074 member_of = check_rels(filename, tree, root, obj, "member-of")
1076 # Labels special case
1077 if klass in widgets_labels:
1078 properties = check_props(filename, tree, root, obj, "mnemonic_widget") + \
1079 check_props(filename, tree, root, obj, "mnemonic-widget")
1080 if len(properties) > 1:
1081 err(filename, tree, obj, "multiple-mnemonic", "has multiple mnemonic_widgets properties"
1082 "%s" % elms_lines(properties))
1084 # Emit orphaning warnings
1085 if warn_orphan_labels or orphan_widgets:
1086 is_orphan_label(filename, tree, root, obj, orphan_root, True)
1088 # We are done with the label
1089 return
1091 # Not a label, will perhaps need one
1093 # Emit orphaning warnings
1094 is_orphan_widget(filename, tree, root, obj, orphan_labels, orphan_root, True)
1096 root = tree.getroot()
1098 # Flush ids and relations from previous files
1099 ids = {}
1100 ids_dup = {}
1101 labelled_by_elm = {}
1102 label_for_elm = {}
1103 mnemonic_for_elm = {}
1105 # First pass to get links into hash tables, no warning, just record duplicates
1106 for obj in root.iter('object'):
1107 oid = obj.attrib.get('id')
1108 if oid is not None:
1109 if oid not in ids:
1110 ids[oid] = obj
1111 else:
1112 ids_dup[oid] = True
1114 labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
1115 for rel in labelled_by:
1116 target = rel.attrib.get('target')
1117 if target is not None:
1118 if target not in labelled_by_elm:
1119 labelled_by_elm[target] = [ obj ]
1120 else:
1121 labelled_by_elm[target].append(obj)
1123 label_for = obj.findall("accessibility/relation[@type='label-for']")
1124 for rel in label_for:
1125 target = rel.attrib.get('target')
1126 if target is not None:
1127 if target not in label_for_elm:
1128 label_for_elm[target] = [ obj ]
1129 else:
1130 label_for_elm[target].append(obj)
1132 mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \
1133 obj.findall("property[@name='mnemonic-widget']")
1134 for rel in mnemonic_for:
1135 target = rel.text
1136 if target is not None:
1137 if target not in mnemonic_for_elm:
1138 mnemonic_for_elm[target] = [ obj ]
1139 else:
1140 mnemonic_for_elm[target].append(obj)
1142 # Second pass, recursive depth-first, to be able to efficiently know whether
1143 # there are orphan labels within a part of the tree.
1144 def recurse(orphan_root, obj, orphan_labels, orphan_widgets):
1145 if obj == root or is_labelled_parent(obj):
1146 orphan_root = obj
1147 orphan_labels, orphan_widgets = orphan_items(filename, tree, root, obj)
1149 if obj.tag == 'object':
1150 check_elm(orphan_root, obj, orphan_labels, orphan_widgets)
1152 for o in obj:
1153 recurse(orphan_root, o, orphan_labels, orphan_widgets)
1155 recurse(root, root, False, False)
1158 # Main
1161 def usage(fatal = True):
1162 print("`%s' checks accessibility of glade .ui files" % progname)
1163 print("")
1164 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname)
1165 print("")
1166 print(" -p Print XML class path instead of line number")
1167 print(" -g Generate suppression file SUPPR_FILE")
1168 print(" -s Suppress warnings given by file SUPPR_FILE, but count them")
1169 print(" -f Suppress warnings given by file SUPPR_FILE completely")
1170 print(" -P Remove PREFIX from file names in warnings")
1171 print(" -o Also prints errors and warnings to given file")
1172 print("")
1173 print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]")
1174 print(" Give or extend one of the lists of widget classes, where FOO can be:")
1175 print(" - toplevel : widgets to be considered toplevel windows")
1176 print(" - ignored : widgets which do not need labelling (e.g. GtkBox)")
1177 print(" - suffixignored : suffixes of widget classes which do not need labelling")
1178 print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)")
1179 print(" - buttons : widgets which need their own label but not more")
1180 print(" (e.g. GtkButton)")
1181 print(" - labels : widgets which provide labels (e.g. GtkLabel)")
1182 print(" --widgets-print print default widgets lists")
1183 print("")
1184 print(" --enable-all enable all warnings/dofatals (default)")
1185 print(" --disable-all disable all warnings/dofatals")
1186 print(" --fatal-all make all warnings dofatals")
1187 print(" --not-fatal-all do not make all warnings dofatals (default)")
1188 print("")
1189 print(" --enable-type=TYPE enable warning/fatal type TYPE")
1190 print(" --disable-type=TYPE disable warning/fatal type TYPE")
1191 print(" --fatal-type=TYPE make warning type TYPE a fatal")
1192 print(" --not-fatal-type=TYPE make warning type TYPE not a fatal")
1193 print("")
1194 print(" --enable-widgets=CLASS enable warning/fatal type CLASS")
1195 print(" --disable-widgets=CLASS disable warning/fatal type CLASS")
1196 print(" --fatal-widgets=CLASS make warning type CLASS a fatal")
1197 print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal")
1198 print("")
1199 print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget")
1200 print(" class CLASS")
1201 print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget")
1202 print(" class CLASS")
1203 print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget")
1204 print(" class CLASS")
1205 print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget")
1206 print(" class CLASS")
1207 print("")
1208 print(" --disable-orphan-labels only warn about orphan labels when there are")
1209 print(" orphan widgets in the same context")
1210 print("")
1211 print("Report bugs to <bugs@hypra.fr>")
1212 sys.exit(2 if fatal else 0)
1214 def widgets_opt(widgets_list, arg):
1216 Replace or extend `widgets_list' with the list of classes contained in `arg'
1218 append = arg and arg[0] == '+'
1219 if append:
1220 arg = arg[1:]
1222 if arg:
1223 widgets = arg.split(',')
1224 else:
1225 widgets = []
1227 if not append:
1228 del widgets_list[:]
1230 widgets_list.extend(widgets)
1233 def main():
1234 global pflag, gen_suppr, gen_supprfile, suppressions, suppr_prefix, false_positives, dofatals, enables, dofatals, warn_orphan_labels
1235 global widgets_toplevel, widgets_ignored, widgets_suffixignored, widgets_needlabel, widgets_buttons, widgets_labels
1236 global outfile, output_buffer
1238 try:
1239 opts, args = getopt.getopt(sys.argv[1:], "hpiIg:s:f:P:o:L:", [
1240 "help",
1241 "version",
1243 "widgets-toplevel=",
1244 "widgets-ignored=",
1245 "widgets-suffixignored=",
1246 "widgets-needlabel=",
1247 "widgets-buttons=",
1248 "widgets-labels=",
1249 "widgets-print",
1251 "enable-all",
1252 "disable-all",
1253 "fatal-all",
1254 "not-fatal-all",
1256 "enable-type=",
1257 "disable-type=",
1258 "fatal-type=",
1259 "not-fatal-type=",
1261 "enable-widgets=",
1262 "disable-widgets=",
1263 "fatal-widgets=",
1264 "not-fatal-widgets=",
1266 "enable-specific=",
1267 "disable-specific=",
1268 "fatal-specific=",
1269 "not-fatal-specific=",
1271 "disable-orphan-labels",
1273 except getopt.GetoptError:
1274 usage()
1276 suppr = None
1277 false = None
1278 out = None
1279 filelist = None
1281 for o, a in opts:
1282 if o == "--help" or o == "-h":
1283 usage(False)
1284 if o == "--version":
1285 print("0.1")
1286 sys.exit(0)
1287 elif o == "-p":
1288 pflag = True
1289 elif o == "-g":
1290 gen_suppr = a
1291 elif o == "-s":
1292 suppr = a
1293 elif o == "-f":
1294 false = a
1295 elif o == "-P":
1296 suppr_prefix = a
1297 elif o == "-o":
1298 out = a
1299 elif o == "-L":
1300 filelist = a
1302 elif o == "--widgets-toplevel":
1303 widgets_opt(widgets_toplevel, a)
1304 elif o == "--widgets-ignored":
1305 widgets_opt(widgets_ignored, a)
1306 elif o == "--widgets-suffixignored":
1307 widgets_opt(widgets_suffixignored, a)
1308 elif o == "--widgets-needlabel":
1309 widgets_opt(widgets_needlabel, a)
1310 elif o == "--widgets-buttons":
1311 widgets_opt(widgets_buttons, a)
1312 elif o == "--widgets-labels":
1313 widgets_opt(widgets_labels, a)
1314 elif o == "--widgets-print":
1315 print("--widgets-toplevel '" + ','.join(widgets_toplevel) + "'")
1316 print("--widgets-ignored '" + ','.join(widgets_ignored) + "'")
1317 print("--widgets-suffixignored '" + ','.join(widgets_suffixignored) + "'")
1318 print("--widgets-needlabel '" + ','.join(widgets_needlabel) + "'")
1319 print("--widgets-buttons '" + ','.join(widgets_buttons) + "'")
1320 print("--widgets-labels '" + ','.join(widgets_labels) + "'")
1321 sys.exit(0)
1323 elif o == '--enable-all':
1324 enables.append( (True, None, None) )
1325 elif o == '--disable-all':
1326 enables.append( (False, None, None) )
1327 elif o == '--fatal-all':
1328 dofatals.append( (True, None, None) )
1329 elif o == '--not-fatal-all':
1330 dofatals.append( (False, None, None) )
1332 elif o == '--enable-type':
1333 enables.append( (True, a, None) )
1334 elif o == '--disable-type':
1335 enables.append( (False, a, None) )
1336 elif o == '--fatal-type':
1337 dofatals.append( (True, a, None) )
1338 elif o == '--not-fatal-type':
1339 dofatals.append( (False, a, None) )
1341 elif o == '--enable-widgets':
1342 enables.append( (True, None, a) )
1343 elif o == '--disable-widgets':
1344 enables.append( (False, None, a) )
1345 elif o == '--fatal-widgets':
1346 dofatals.append( (True, None, a) )
1347 elif o == '--not-fatal-widgets':
1348 dofatals.append( (False, None, a) )
1350 elif o == '--enable-specific':
1351 (thetype, klass) = a.split('.', 1)
1352 enables.append( (True, thetype, klass) )
1353 elif o == '--disable-specific':
1354 (thetype, klass) = a.split('.', 1)
1355 enables.append( (False, thetype, klass) )
1356 elif o == '--fatal-specific':
1357 (thetype, klass) = a.split('.', 1)
1358 dofatals.append( (True, thetype, klass) )
1359 elif o == '--not-fatal-specific':
1360 (thetype, klass) = a.split('.', 1)
1361 dofatals.append( (False, thetype, klass) )
1363 elif o == '--disable-orphan-labels':
1364 warn_orphan_labels = False
1366 output_header = ""
1368 # Read suppression file before overwriting it
1369 if suppr is not None:
1370 try:
1371 output_header += "Suppression file: " + suppr + "\n"
1372 supprfile = open(suppr, 'r')
1373 line_no = 0
1374 for line in supprfile.readlines():
1375 line_no = line_no + 1
1376 if line.startswith('#'):
1377 continue
1378 prefix = line.rstrip()
1379 suppressions[prefix] = True
1380 suppressions_to_line[prefix] = "%s:%u" % (suppr, line_no)
1381 supprfile.close()
1382 except IOError:
1383 pass
1385 # Read false positives file
1386 if false is not None:
1387 try:
1388 output_header += "False positive file: " + false + "\n"
1389 falsefile = open(false, 'r')
1390 for line in falsefile.readlines():
1391 if line.startswith('#'):
1392 continue
1393 prefix = line.rstrip()
1394 false_positives[prefix] = True
1395 falsefile.close()
1396 except IOError:
1397 pass
1399 if out is not None:
1400 outfile = open(out, 'w')
1402 if filelist is not None:
1403 try:
1404 filelistfile = open(filelist, 'r')
1405 for line in filelistfile.readlines():
1406 line = line.strip()
1407 if line:
1408 args += line.split(' ')
1409 filelistfile.close()
1410 except IOError:
1411 err(filelist, None, None, "unable to read file list file")
1413 for filename in args:
1414 try:
1415 tree = ET.parse(filename)
1416 except ET.ParseError:
1417 err(filename, None, None, "parse", "malformatted xml file")
1418 continue
1419 except IOError:
1420 err(filename, None, None, None, "unable to read file")
1421 continue
1423 try:
1424 check_a11y_relation(filename, tree)
1425 except Exception as error:
1426 import traceback
1427 output_buffer += traceback.format_exc()
1428 err(filename, None, None, "parse", "error parsing file")
1430 if errors > 0 or errexists > 0:
1431 output_buffer += "%s new error%s" % (errors, 's' if errors != 1 else '')
1432 if errexists > 0:
1433 output_buffer += " (%s suppressed by %s, please fix %s)" % (errexists, suppr, 'them' if errexists > 1 else 'it')
1434 output_buffer += "\n"
1436 if warnings > 0 or warnexists > 0:
1437 output_buffer += "%s new warning%s" % (warnings, 's' if warnings != 1 else '')
1438 if warnexists > 0:
1439 output_buffer += " (%s suppressed by %s, please fix %s)" % (warnexists, suppr, 'them' if warnexists > 1 else 'it')
1440 output_buffer += "\n"
1442 if fatals > 0 or fatalexists > 0:
1443 output_buffer += "%s new fatal%s" % (fatals, 's' if fatals != 1 else '')
1444 if fatalexists > 0:
1445 output_buffer += " (%s suppressed by %s, please fix %s)" % (fatalexists, suppr, 'them' if fatalexists > 1 else 'it')
1446 output_buffer += "\n"
1448 n = 0
1449 for (suppr,unused) in suppressions.items():
1450 if unused:
1451 n += 1
1453 if n > 0:
1454 output_buffer += "%s suppression%s unused:\n" % (n, 's' if n != 1 else '')
1455 for (suppr,unused) in suppressions.items():
1456 if unused:
1457 output_buffer += " %s:%s\n" % (suppressions_to_line[suppr], suppr)
1459 if gen_supprfile is not None:
1460 gen_supprfile.close()
1461 if outfile is not None:
1462 outfile.close()
1464 if gen_suppr is None:
1465 if output_buffer != "":
1466 output_buffer += "Explanations are available on " + howto_url + "\n"
1468 if fatals > 0:
1469 print(output_header.rstrip() + "\n" + output_buffer)
1470 sys.exit(1)
1472 if len(output_buffer) > 0:
1473 print(output_header.rstrip() + "\n" + output_buffer)
1475 if __name__ == "__main__":
1476 try:
1477 main()
1478 except KeyboardInterrupt:
1479 pass
1481 # vim: set shiftwidth=4 softtabstop=4 expandtab: