tdf#131370 chart: implement OOXML import/export of legend overlay feature
[LibreOffice.git] / bin / gla11y
blob1ae67b1739593ea2161ddd6396bda3c514de2a13
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 # Toplevel widgets
45 widgets_toplevel = [
46 'GtkWindow',
47 'GtkOffscreenWindow',
48 'GtkApplicationWindow',
49 'GtkDialog',
50 'GtkAboutDialog',
51 'GtkFileChooserDialog',
52 'GtkColorChooserDialog',
53 'GtkFontChooserDialog',
54 'GtkMessageDialog',
55 'GtkRecentChooserDialog',
56 'GtkAssistant',
57 'GtkAppChooserDialog',
58 'GtkPrintUnixDialog',
59 'GtkShortcutsWindow',
62 widgets_ignored = widgets_toplevel + [
63 # Containers
64 'GtkBox',
65 'GtkGrid',
66 'GtkNotebook',
67 'GtkFrame',
68 'GtkAspectFrame',
69 'GtkListBox',
70 'GtkFlowBox',
71 'GtkOverlay',
72 'GtkMenuBar',
73 'GtkToolbar',
74 'GtkToolpalette',
75 'GtkPaned',
76 'GtkHPaned',
77 'GtkVPaned',
78 'GtkButtonBox',
79 'GtkHButtonBox',
80 'GtkVButtonBox',
81 'GtkLayout',
82 'GtkFixed',
83 'GtkEventBox',
84 'GtkExpander',
85 'GtkViewport',
86 'GtkScrolledWindow',
87 'GtkAlignment',
88 'GtkRevealer',
89 'GtkSearchBar',
90 'GtkHeaderBar',
91 'GtkStack',
92 'GtkStackSwticher',
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 suppressions = {}
252 suppressions_to_line = {}
253 false_positives = {}
254 ids = {}
255 ids_dup = {}
256 labelled_by_elm = {}
257 label_for_elm = {}
258 mnemonic_for_elm = {}
260 gen_suppr = None
261 gen_supprfile = None
262 suppr_prefix = ""
263 outfile = None
265 pflag = False
267 warn_orphan_labels = True
269 errors = 0
270 errexists = 0
271 warnings = 0
272 warnexists = 0
273 fatals = 0
274 fatalexists = 0
276 enables = [ ]
277 dofatals = [ ]
280 # XML browsing and printing functions
283 def elm_parent(root, elm):
285 Return the parent of the element.
287 if lxml:
288 return elm.getparent()
289 else:
290 def find_parent(cur, elm):
291 for o in cur:
292 if o == elm:
293 return cur
294 parent = find_parent(o, elm)
295 if parent is not None:
296 return parent
297 return None
298 return find_parent(root, elm)
300 def step_elm(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')
306 if step is None:
307 step = ""
308 oid = elm.attrib.get('id')
309 if oid is not None:
310 oid = oid.encode('ascii','ignore').decode('ascii')
311 step += "[@id='%s']" % oid
312 if len(step) > 0:
313 step += '/'
314 return step
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.
321 if root == elm:
322 return ""
323 for o in root:
324 path = find_elm(o, elm)
325 if path is not None:
326 step = step_elm(o)
327 return step + path
328 return None
330 def errpath(filename, tree, elm):
332 Return the XML class path of the element
334 if elm is None:
335 return ""
336 path = ""
337 if 'class' in elm.attrib:
338 path += elm.attrib['class']
339 oid = elm.attrib.get('id')
340 if oid is not None:
341 oid = oid.encode('ascii','ignore').decode('ascii')
342 path = "//" + path + "[@id='%s']" % oid
343 else:
344 if lxml:
345 elm = elm.getparent()
346 while elm is not None:
347 step = step_elm(elm)
348 path = step + path
349 elm = elm.getparent()
350 else:
351 path = find_elm(tree.getroot(), elm)[:-1]
352 path = filename + ':' + path
353 return 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
365 else:
366 return "%s:%u" % (filename, elm.sourceline)
368 def elm_name(elm):
370 Return a display name of the element
372 if elm is not None:
373 name = ""
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')
378 name += "'%s' " % id
379 if not name:
380 name = "'" + elm.tag + "'"
381 if lxml:
382 name += " line " + str(elm.sourceline)
383 return name
384 return ""
386 def elm_name_line(elm):
388 Return a display name of the element with line number
390 if elm is not None:
391 name = elm_name(elm)
392 if lxml and " line " not in name:
393 name += "line " + str(elm.sourceline) + " "
394 return name
395 return ""
397 def elm_line(elm):
399 Return the line for the given element.
401 if lxml:
402 return " line " + str(elm.sourceline)
403 else:
404 return ""
406 def elms_lines(elms):
408 Return the list of lines for the given elements.
410 if lxml:
411 return " lines " + ', '.join([str(l.sourceline) for l in elms])
412 else:
413 return ""
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)
440 else:
441 suppr = None
443 if not pflag:
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
455 enabled = default
456 for (enable, thetype, klass) in l:
457 # Match warning type
458 if thetype is not None:
459 if thetype != msgtype:
460 continue
461 # Match elm class
462 if klass is not None and elm is not None:
463 if klass != elm.attrib.get('class'):
464 continue
465 enabled = enable
466 return enabled
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):
479 return
481 (prefix, suppr) = elm_suppr(filename, tree, elm, msgtype, True)
482 if suppr in false_positives:
483 # That was actually expected
484 return
485 if suppr in suppressions:
486 # Suppressed
487 suppressions[suppr] = False
488 if fatal:
489 fatalexists += 1
490 if error:
491 errexists += 1
492 else:
493 warnexists += 1
494 return
496 if error:
497 errors += 1
498 else:
499 warnings += 1
500 if fatal:
501 fatals += 1
503 msg = "%s %s%s: %s%s" % (prefix,
504 "FATAL " if fatal else "",
505 "ERROR" if error else "WARNING",
506 elm_name(elm), msg)
507 print(msg)
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
525 if lxml:
526 parent = elm.getparent()
527 if parent is not None:
528 if parent.attrib.get('class') in widgets_buttons:
529 return parent
530 return find_button_parent(root, parent)
531 else:
532 def find_parent(cur, elm):
533 for o in cur:
534 if o == elm:
535 if cur.attrib.get('class') in widgets_buttons:
536 # we are the button, immediately above the target
537 return cur
538 else:
539 # we aren't the button, but target is over there
540 return True
541 parent = find_parent(o, elm)
542 if parent == True:
543 # It is over there, but didn't find a button yet
544 if cur.attrib.get('class') in widgets_buttons:
545 # we are the button
546 return cur
547 else:
548 return True
549 if parent is not None:
550 # we have the button parent over there
551 return parent
552 return None
553 parent = find_parent(root, elm)
554 if parent == True:
555 parent = None
556 return parent
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:
565 return True
566 if klass == 'GtkShortcutsGroup':
567 children = elm.findall("property[@name='title']")
568 if len(children) >= 1:
569 return True
570 if klass == 'GtkFrame' or klass == 'GtkNotebook':
571 children = elm.findall("child[@type='tab']") + elm.findall("child[@type='label']")
572 if len(children) >= 1:
573 return True
574 return False
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
582 if lxml:
583 def find_labelled_parent(elm):
584 if is_labelled_parent(elm):
585 return elm
586 parent = elm.getparent()
587 if parent is None:
588 return None
589 return find_labelled_parent(parent)
590 parent = elm.getparent()
591 if parent is None:
592 return None
593 return find_labelled_parent(elm.getparent())
594 else:
595 def find_labelled_parent(cur, elm):
596 if cur == elm:
597 # the target element is over there
598 return True
599 for o in cur:
600 parent = find_labelled_parent(o, elm)
601 if parent == True:
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
605 return cur
606 else:
607 # no, but target element is over there.
608 return True
609 if parent != None:
610 # the first ancestor of the target element was over there
611 return parent
612 return None
613 parent = find_labelled_parent(root, elm)
614 if parent == True:
615 parent = None
616 return parent
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
625 # label-for
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]
630 if len(l) > 1:
631 return True
633 # mnemonic_widget
634 mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \
635 obj.findall("property[@name='mnemonic-widget']")
636 for rel in mnemonic_for:
637 target = rel.text
638 l = mnemonic_for_elm[target]
639 if len(l) > 1:
640 return True
642 if len(label_for) > 0:
643 # At least one label-for, we are not orphan.
644 return False
646 if len(mnemonic_for) > 0:
647 # At least one mnemonic_widget, we are not orphan.
648 return False
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
653 return False
655 # explicit role?
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))
661 for role in roles:
662 if role == 'static' or role == 'ATK_ROLE_STATIC':
663 # This is static text, not meant to label anything
664 return False
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.
674 return False
676 if find_button_parent(root, obj) is not None:
677 # This label is part of a button
678 return False
680 oid = obj.attrib.get('id')
681 if oid is not None:
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.
685 return False
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
691 return False
692 if suppr in suppressions:
693 # Warning suppressed for this label
694 if suppressions[suppr]:
695 warnexists += 1
696 suppressions[suppr] = False
697 return False
699 if doprint:
700 context = elm_name(orphan_root)
701 if context:
702 context = " within " + context
703 warn(filename, tree, obj, "orphan-label", "does not specify what it labels" + context)
704 return True
706 def is_orphan_widget(filename, tree, root, obj, orphan, orphan_root, doprint = False):
708 Check whether this widget has no accessibility relation.
710 global warnexists
711 if obj.tag != 'object':
712 return False
714 oid = obj.attrib.get('id')
715 klass = obj.attrib.get('class')
717 # "Don't care" special case
718 if klass in widgets_ignored:
719 return False
720 for suffix in widgets_suffixignored:
721 if klass[-len(suffix):] == suffix:
722 return False
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:
732 return False
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:
740 return False
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:
745 return False
747 # Case 3: has a label-for
748 if oid in label_for_elm:
749 return False
751 # Case 4: has a mnemonic
752 if oid in mnemonic_for_elm:
753 return False
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':
761 return False
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:
769 return False
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")
777 if len(labels) >= 1:
778 # Has a <property name="label">
779 return False
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">
786 return False
788 # Uses id as an action_name
789 if 'id' in obj.attrib:
790 if obj.attrib['id'].startswith(".uno:"):
791 return False
793 gtklabels = obj.findall(".//object[@class='GtkLabel']") + obj.findall(".//object[@class='GtkAccelLabel']")
794 if len(gtklabels) >= 1:
795 # Has a custom label
796 return False
798 # no label for a button, warn
799 if doprint:
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):
802 # Warnings disabled
803 return False
804 (_, suppr) = elm_suppr(filename, tree, obj, "button-no-label", False)
805 if suppr in false_positives:
806 # That was actually expected
807 return False
808 if suppr in suppressions:
809 # Warning suppressed for this widget
810 if suppressions[suppr]:
811 warnexists += 1
812 suppressions[suppr] = False
813 return False
814 return True
816 # GtkImages special case
817 if klass == "GtkImage":
818 uses = [u for u in tree.iterfind(".//object/property[@name='image']") if u.text == oid]
819 if len(uses) > 0:
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.
823 return False
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
827 # instead, if any.
828 return False
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.
838 return False
840 # GtkShortcutsShortcut special case
841 if klass == 'GtkShortcutsShortcut':
842 children = obj.findall("property[@name='title']")
843 if len(children) >= 1:
844 return False
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
850 return False
851 (_, suppr) = elm_suppr(filename, tree, obj, "no-labelled-by", False)
852 if suppr in false_positives:
853 # That was actually expected
854 return False
855 if suppr in suppressions:
856 # Warning suppressed for this widget
857 if suppressions[suppr]:
858 warnexists += 1
859 suppressions[suppr] = False
860 return False
862 if not orphan:
863 # No orphan label, so probably the labelled parent provides enough
864 # context.
865 if requires_label:
866 # But these always need a label.
867 if doprint:
868 warn(filename, tree, obj, "no-labelled-by", "has no accessibility label")
869 return True
870 return False
872 if doprint:
873 context = elm_name(orphan_root)
874 if context:
875 context = " within " + context
876 warn(filename, tree, obj, "no-labelled-by", "has no accessibility label while there are orphan labels" + context)
877 return True
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)
887 else:
888 orphan_widgets = is_orphan_widget(filename, tree, root, elm, True, None)
889 for obj in elm:
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)
894 if label:
895 orphan_labels = True
896 if widget:
897 orphan_widgets = True
898 if orphan_labels and orphan_widgets:
899 # No need to look up more
900 break
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 + "']")
912 for prop in props:
913 if prop.text not in ids:
914 err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % prop.text)
915 return props
917 def is_visible(obj):
918 visible = False
919 visible_prop = obj.findall("property[@name='visible']")
920 visible_len = len(visible_prop)
921 if visible_len:
922 visible_txt = visible_prop[visible_len - 1].text
923 if visible_txt.lower() == "true":
924 visible = True
925 elif visible_txt.lower() == "false":
926 visible = False
927 return visible
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 + "']")
935 for rel in rels:
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:
940 widget = ids[target]
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))
945 return rels
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
951 rules.
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:
966 return
967 for suffix in widgets_suffixignored:
968 if klass[-len(suffix):] == suffix:
969 return
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
975 if oid is not None:
976 # Check that ids are unique
977 if oid in ids_dup:
978 if ids[oid] == obj:
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]))
1009 # Check member-of
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
1025 return
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
1035 ids = {}
1036 ids_dup = {}
1037 labelled_by_elm = {}
1038 label_for_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')
1044 if oid is not None:
1045 if oid not in ids:
1046 ids[oid] = obj
1047 else:
1048 ids_dup[oid] = True
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 ]
1056 else:
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 ]
1065 else:
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:
1071 target = rel.text
1072 if target is not None:
1073 if target not in mnemonic_for_elm:
1074 mnemonic_for_elm[target] = [ obj ]
1075 else:
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):
1082 orphan_root = 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)
1088 for o in obj:
1089 recurse(orphan_root, o, orphan_labels, orphan_widgets)
1091 recurse(root, root, False, False)
1094 # Main
1097 def usage(fatal = True):
1098 print("`%s' checks accessibility of glade .ui files" % progname)
1099 print("")
1100 print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname)
1101 print("")
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")
1108 print("")
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")
1119 print("")
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)")
1124 print("")
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")
1129 print("")
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")
1134 print("")
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")
1143 print("")
1144 print(" --disable-orphan-labels only warn about orphan labels when there are")
1145 print(" orphan widgets in the same context")
1146 print("")
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] == '+'
1155 if append:
1156 arg = arg[1:]
1158 if arg:
1159 widgets = arg.split(',')
1160 else:
1161 widgets = []
1163 if not append:
1164 del widgets_list[:]
1166 widgets_list.extend(widgets)
1169 def main():
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
1172 global outfile
1174 try:
1175 opts, args = getopt.getopt(sys.argv[1:], "hpiIg:s:f:P:o:L:", [
1176 "help",
1177 "version",
1179 "widgets-toplevel=",
1180 "widgets-ignored=",
1181 "widgets-suffixignored=",
1182 "widgets-needlabel=",
1183 "widgets-buttons=",
1184 "widgets-labels=",
1185 "widgets-print",
1187 "enable-all",
1188 "disable-all",
1189 "fatal-all",
1190 "not-fatal-all",
1192 "enable-type=",
1193 "disable-type=",
1194 "fatal-type=",
1195 "not-fatal-type=",
1197 "enable-widgets=",
1198 "disable-widgets=",
1199 "fatal-widgets=",
1200 "not-fatal-widgets=",
1202 "enable-specific=",
1203 "disable-specific=",
1204 "fatal-specific=",
1205 "not-fatal-specific=",
1207 "disable-orphan-labels",
1209 except getopt.GetoptError:
1210 usage()
1212 suppr = None
1213 false = None
1214 out = None
1215 filelist = None
1217 for o, a in opts:
1218 if o == "--help" or o == "-h":
1219 usage(False)
1220 if o == "--version":
1221 print("0.1")
1222 sys.exit(0)
1223 elif o == "-p":
1224 pflag = True
1225 elif o == "-g":
1226 gen_suppr = a
1227 elif o == "-s":
1228 suppr = a
1229 elif o == "-f":
1230 false = a
1231 elif o == "-P":
1232 suppr_prefix = a
1233 elif o == "-o":
1234 out = a
1235 elif o == "-L":
1236 filelist = a
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) + "'")
1257 sys.exit(0)
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:
1304 try:
1305 supprfile = open(suppr, 'r')
1306 line_no = 1;
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;
1312 supprfile.close()
1313 except IOError:
1314 pass
1316 # Read false positives file
1317 if false is not None:
1318 try:
1319 falsefile = open(false, 'r')
1320 for line in falsefile.readlines():
1321 prefix = line.rstrip()
1322 false_positives[prefix] = True
1323 falsefile.close()
1324 except IOError:
1325 pass
1327 if out is not None:
1328 outfile = open(out, 'w')
1330 if filelist is not None:
1331 try:
1332 filelistfile = open(filelist, 'r')
1333 for line in filelistfile.readlines():
1334 line = line.strip()
1335 if line:
1336 args += line.split(' ')
1337 filelistfile.close()
1338 except IOError:
1339 err(filelist, None, None, "unable to read file list file")
1341 for filename in args:
1342 try:
1343 tree = ET.parse(filename)
1344 except ET.ParseError:
1345 err(filename, None, None, "parse", "malformatted xml file")
1346 continue
1347 except IOError:
1348 err(filename, None, None, None, "unable to read file")
1349 continue
1351 try:
1352 check_a11y_relation(filename, tree)
1353 except Exception as error:
1354 import traceback
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 '')
1360 if errexists > 0:
1361 estr += " (%s suppressed by %s)" % (errexists, suppr)
1362 print(estr)
1364 if warnings > 0 or warnexists > 0:
1365 wstr = "%s new warning%s" % (warnings, 's' if warnings > 1 else '')
1366 if warnexists > 0:
1367 wstr += " (%s suppressed by %s)" % (warnexists, suppr)
1368 print(wstr)
1370 if fatals > 0 or fatalexists > 0:
1371 wstr = "%s new fatal%s" % (fatals, 's' if fatals > 1 else '')
1372 if fatalexists > 0:
1373 wstr += " (%s suppressed by %s)" % (fatalexists, suppr)
1374 print(wstr)
1376 n = 0
1377 for (suppr,unused) in suppressions.items():
1378 if unused:
1379 n += 1
1381 if n > 0:
1382 print("%s suppression%s unused:" % (n, 's' if n > 1 else ''))
1383 for (suppr,unused) in suppressions.items():
1384 if unused:
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:
1390 outfile.close()
1391 if fatals > 0 and gen_suppr is None:
1392 print("Explanations are available on https://wiki.documentfoundation.org/Development/Accessibility")
1393 sys.exit(1)
1396 if __name__ == "__main__":
1397 try:
1398 main()
1399 except KeyboardInterrupt:
1400 pass
1402 # vim: set shiftwidth=4 softtabstop=4 expandtab: