Fix typo
[LibreOffice.git] / bin / ui-rules-enforcer.py
blob8c222793d700d3c6900f889bad90f15bccc1fcce
1 #!/bin/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 # ui-rules-enforcer enforces the .ui rules and properties used by LibreOffice
11 # mostly the deprecations of
12 # https://developer.gnome.org/gtk4/stable/gtk-migrating-3-to-4.html
13 # and a few other home cooked rules
15 # for any existing .ui this should parse it and overwrite it with the same content
16 # e.g. for a in `git ls-files "*.ui"`; do bin/ui-rules-enforcer.py $a; done
18 import lxml.etree as etree
19 import sys
21 def add_truncate_multiline(current):
22 use_truncate_multiline = False
23 istarget = current.get('class') == "GtkEntry" or current.get('class') == "GtkSpinButton"
24 insertpos = 0
25 for child in current:
26 add_truncate_multiline(child)
27 insertpos = insertpos + 1;
28 if not istarget:
29 continue
30 if child.tag == "property":
31 attributes = child.attrib
32 if attributes.get("name") == "truncate_multiline" or attributes.get("name") == "truncate-multiline":
33 use_truncate_multiline = True
35 if istarget and not use_truncate_multiline:
36 truncate_multiline = etree.Element("property")
37 attributes = truncate_multiline.attrib
38 attributes["name"] = "truncate-multiline"
39 truncate_multiline.text = "True"
40 current.insert(insertpos - 1, truncate_multiline)
42 def do_replace_button_use_stock(current, use_stock, use_underline, label, insertpos):
43 if not use_underline:
44 underline = etree.Element("property")
45 attributes = underline.attrib
46 attributes["name"] = "use-underline"
47 underline.text = "True"
48 current.insert(insertpos - 1, underline)
49 current.remove(use_stock)
50 attributes = label.attrib
51 attributes["translatable"] = "yes"
52 attributes["context"] = "stock"
53 if label.text == 'gtk-add':
54 label.text = "_Add"
55 elif label.text == 'gtk-apply':
56 label.text = "_Apply"
57 elif label.text == 'gtk-cancel':
58 label.text = "_Cancel"
59 elif label.text == 'gtk-close':
60 label.text = "_Close"
61 elif label.text == 'gtk-delete':
62 label.text = "_Delete"
63 elif label.text == 'gtk-edit':
64 label.text = "_Edit"
65 elif label.text == 'gtk-help':
66 label.text = "_Help"
67 elif label.text == 'gtk-new':
68 label.text = "_New"
69 elif label.text == 'gtk-no':
70 label.text = "_No"
71 elif label.text == 'gtk-ok':
72 label.text = "_OK"
73 elif label.text == 'gtk-remove':
74 label.text = "_Remove"
75 elif label.text == 'gtk-revert-to-saved':
76 label.text = "_Reset"
77 elif label.text == 'gtk-yes':
78 label.text = "_Yes"
79 else:
80 raise Exception(sys.argv[1] + ': unknown label', label.text)
82 def replace_button_use_stock(current):
83 use_underline = False
84 use_stock = None
85 label = None
86 isbutton = current.get('class') == "GtkButton"
87 insertpos = 0
88 for child in current:
89 replace_button_use_stock(child)
90 insertpos = insertpos + 1;
91 if not isbutton:
92 continue
93 if child.tag == "property":
94 attributes = child.attrib
95 if attributes.get("name") == "use_underline" or attributes.get("name") == "use-underline":
96 use_underline = True
97 if attributes.get("name") == "use_stock" or attributes.get("name") == "use-stock":
98 use_stock = child
99 if attributes.get("name") == "label":
100 label = child
102 if isbutton and use_stock != None:
103 do_replace_button_use_stock(current, use_stock, use_underline, label, insertpos)
105 def do_replace_image_stock(current, stock):
106 attributes = stock.attrib
107 attributes["name"] = "icon-name"
108 if stock.text == 'gtk-add':
109 stock.text = "list-add"
110 elif stock.text == 'gtk-remove':
111 stock.text = "list-remove"
112 elif stock.text == 'gtk-paste':
113 stock.text = "edit-paste"
114 elif stock.text == 'gtk-index':
115 stock.text = "vcl/res/index.png"
116 elif stock.text == 'gtk-refresh':
117 stock.text = "view-refresh"
118 elif stock.text == 'gtk-dialog-error':
119 stock.text = "dialog-error"
120 elif stock.text == 'gtk-apply':
121 stock.text = "sw/res/sc20558.png"
122 elif stock.text == 'gtk-missing-image':
123 stock.text = "missing-image"
124 elif stock.text == 'gtk-copy':
125 stock.text = "edit-copy"
126 elif stock.text == 'gtk-go-back':
127 stock.text = "go-previous"
128 elif stock.text == 'gtk-go-forward':
129 stock.text = "go-next"
130 elif stock.text == 'gtk-go-down':
131 stock.text = "go-down"
132 elif stock.text == 'gtk-go-up':
133 stock.text = "go-up"
134 elif stock.text == 'gtk-goto-first':
135 stock.text = "go-first"
136 elif stock.text == 'gtk-goto-last':
137 stock.text = "go-last"
138 elif stock.text == 'gtk-new':
139 stock.text = "document-new"
140 elif stock.text == 'gtk-open':
141 stock.text = "document-open"
142 elif stock.text == 'gtk-media-stop':
143 stock.text = "media-playback-stop"
144 elif stock.text == 'gtk-media-play':
145 stock.text = "media-playback-start"
146 elif stock.text == 'gtk-media-next':
147 stock.text = "media-skip-forward"
148 elif stock.text == 'gtk-media-previous':
149 stock.text = "media-skip-backward"
150 elif stock.text == 'gtk-close':
151 stock.text = "window-close"
152 elif stock.text == 'gtk-help':
153 stock.text = "help-browser"
154 else:
155 raise Exception(sys.argv[1] + ': unknown stock name', stock.text)
157 def replace_image_stock(current):
158 stock = None
159 isimage = current.get('class') == "GtkImage"
160 for child in current:
161 replace_image_stock(child)
162 if not isimage:
163 continue
164 if child.tag == "property":
165 attributes = child.attrib
166 if attributes.get("name") == "stock":
167 stock = child
169 if isimage and stock != None:
170 do_replace_image_stock(current, stock)
172 def remove_check_button_align(current):
173 xalign = None
174 yalign = None
175 ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
176 for child in current:
177 remove_check_button_align(child)
178 if not ischeckorradiobutton:
179 continue
180 if child.tag == "property":
181 attributes = child.attrib
182 if attributes.get("name") == "xalign":
183 xalign = child
184 if attributes.get("name") == "yalign":
185 yalign = child
187 if ischeckorradiobutton:
188 if xalign != None:
189 if xalign.text != "0":
190 raise Exception(sys.argv[1] + ': non-default xalign', xalign.text)
191 current.remove(xalign)
192 if yalign != None:
193 if yalign.text != "0.5":
194 raise Exception(sys.argv[1] + ': non-default yalign', yalign.text)
195 current.remove(yalign)
197 def remove_check_button_relief(current):
198 relief = None
199 ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
200 for child in current:
201 remove_check_button_relief(child)
202 if not ischeckorradiobutton:
203 continue
204 if child.tag == "property":
205 attributes = child.attrib
206 if attributes.get("name") == "relief":
207 relief = child
209 if ischeckorradiobutton:
210 if relief != None:
211 current.remove(relief)
213 def remove_check_button_image_position(current):
214 image_position = None
215 ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
216 for child in current:
217 remove_check_button_image_position(child)
218 if not ischeckorradiobutton:
219 continue
220 if child.tag == "property":
221 attributes = child.attrib
222 if attributes.get("name") == "image_position" or attributes.get("name") == "image-position":
223 image_position = child
225 if ischeckorradiobutton:
226 if image_position != None:
227 current.remove(image_position)
229 def remove_spin_button_input_purpose(current):
230 input_purpose = None
231 isspinbutton = current.get('class') == "GtkSpinButton"
232 for child in current:
233 remove_spin_button_input_purpose(child)
234 if not isspinbutton:
235 continue
236 if child.tag == "property":
237 attributes = child.attrib
238 if attributes.get("name") == "input_purpose" or attributes.get("name") == "input-purpose":
239 input_purpose = child
241 if isspinbutton:
242 if input_purpose != None:
243 current.remove(input_purpose)
245 def remove_caps_lock_warning(current):
246 caps_lock_warning = None
247 iscandidate = current.get('class') == "GtkSpinButton" or current.get('class') == "GtkEntry"
248 for child in current:
249 remove_caps_lock_warning(child)
250 if not iscandidate:
251 continue
252 if child.tag == "property":
253 attributes = child.attrib
254 if attributes.get("name") == "caps_lock_warning" or attributes.get("name") == "caps-lock-warning":
255 caps_lock_warning = child
257 if iscandidate:
258 if caps_lock_warning != None:
259 current.remove(caps_lock_warning)
261 def remove_spin_button_max_length(current):
262 max_length = None
263 isspinbutton = current.get('class') == "GtkSpinButton"
264 for child in current:
265 remove_spin_button_max_length(child)
266 if not isspinbutton:
267 continue
268 if child.tag == "property":
269 attributes = child.attrib
270 if attributes.get("name") == "max_length" or attributes.get("name") == "max-length":
271 max_length = child
273 if isspinbutton:
274 if max_length != None:
275 current.remove(max_length)
277 def remove_entry_shadow_type(current):
278 shadow_type = None
279 isentry = current.get('class') == "GtkEntry"
280 for child in current:
281 remove_entry_shadow_type(child)
282 if not isentry:
283 continue
284 if child.tag == "property":
285 attributes = child.attrib
286 if attributes.get("name") == "shadow_type" or attributes.get("name") == "shadow-type":
287 shadow_type = child
289 if isentry:
290 if shadow_type!= None:
291 current.remove(shadow_type)
293 def remove_label_pad(current):
294 xpad = None
295 ypad = None
296 islabel = current.get('class') == "GtkLabel"
297 for child in current:
298 remove_label_pad(child)
299 if not islabel:
300 continue
301 if child.tag == "property":
302 attributes = child.attrib
303 if attributes.get("name") == "xpad":
304 xpad = child
305 elif attributes.get("name") == "ypad":
306 ypad = child
308 if xpad != None:
309 current.remove(xpad)
310 if ypad != None:
311 current.remove(ypad)
313 def remove_label_angle(current):
314 angle = None
315 islabel = current.get('class') == "GtkLabel"
316 for child in current:
317 remove_label_angle(child)
318 if not islabel:
319 continue
320 if child.tag == "property":
321 attributes = child.attrib
322 if attributes.get("name") == "angle":
323 angle = child
325 if angle != None:
326 current.remove(angle)
328 def remove_track_visited_links(current):
329 track_visited_links = None
330 islabel = current.get('class') == "GtkLabel"
331 for child in current:
332 remove_track_visited_links(child)
333 if not islabel:
334 continue
335 if child.tag == "property":
336 attributes = child.attrib
337 if attributes.get("name") == "track_visited_links" or attributes.get("name") == "track-visited-links":
338 track_visited_links = child
340 if track_visited_links != None:
341 current.remove(track_visited_links)
343 def remove_toolbutton_focus(current):
344 can_focus = None
345 classname = current.get('class');
346 istoolbutton = classname and classname.endswith("ToolButton");
347 for child in current:
348 remove_toolbutton_focus(child)
349 if not istoolbutton:
350 continue
351 if child.tag == "property":
352 attributes = child.attrib
353 if attributes.get("name") == "can_focus" or attributes.get("name") == "can-focus":
354 can_focus = child
356 if can_focus != None:
357 current.remove(can_focus)
359 def remove_double_buffered(current):
360 double_buffered = None
361 for child in current:
362 remove_double_buffered(child)
363 if child.tag == "property":
364 attributes = child.attrib
365 if attributes.get("name") == "double_buffered" or attributes.get("name") == "double-buffered":
366 double_buffered = child
368 if double_buffered != None:
369 current.remove(double_buffered)
371 def remove_label_yalign(current):
372 label_yalign = None
373 for child in current:
374 remove_label_yalign(child)
375 if child.tag == "property":
376 attributes = child.attrib
377 if attributes.get("name") == "label_yalign" or attributes.get("name") == "label-yalign":
378 label_yalign = child
380 if label_yalign != None:
381 current.remove(label_yalign)
383 def remove_skip_pager_hint(current):
384 skip_pager_hint = None
385 for child in current:
386 remove_skip_pager_hint(child)
387 if child.tag == "property":
388 attributes = child.attrib
389 if attributes.get("name") == "skip_pager_hint" or attributes.get("name") == "skip-pager-hint":
390 skip_pager_hint = child
392 if skip_pager_hint != None:
393 current.remove(skip_pager_hint)
395 def remove_gravity(current):
396 gravity = None
397 for child in current:
398 remove_gravity(child)
399 if child.tag == "property":
400 attributes = child.attrib
401 if attributes.get("name") == "gravity":
402 gravity = child
404 if gravity != None:
405 current.remove(gravity)
407 def remove_expander_label_fill(current):
408 label_fill = None
409 isexpander = current.get('class') == "GtkExpander"
410 for child in current:
411 remove_expander_label_fill(child)
412 if not isexpander:
413 continue
414 if child.tag == "property":
415 attributes = child.attrib
416 if attributes.get("name") == "label_fill" or attributes.get("name") == "label-fill":
417 label_fill = child
419 if label_fill != None:
420 current.remove(label_fill)
422 def remove_expander_spacing(current):
423 spacing = None
424 isexpander = current.get('class') == "GtkExpander"
425 for child in current:
426 remove_expander_spacing(child)
427 if not isexpander:
428 continue
429 if child.tag == "property":
430 attributes = child.attrib
431 if attributes.get("name") == "spacing":
432 spacing = child
434 if spacing != None:
435 current.remove(spacing)
437 def enforce_menubutton_indicator_consistency(current):
438 draw_indicator = None
439 image = None
440 ismenubutton = current.get('class') == "GtkMenuButton"
441 insertpos = 0
442 for child in current:
443 enforce_menubutton_indicator_consistency(child)
444 if not ismenubutton:
445 continue
446 if child.tag == "property":
447 insertpos = insertpos + 1;
448 attributes = child.attrib
449 if attributes.get("name") == "draw_indicator" or attributes.get("name") == "draw-indicator":
450 draw_indicator = child
451 elif attributes.get("name") == "image":
452 image = child
454 if ismenubutton:
455 if draw_indicator == None:
456 if image == None:
457 # if there is no draw indicator and no image there should be a draw indicator
458 draw_indicator = etree.Element("property")
459 attributes = draw_indicator.attrib
460 attributes["name"] = "draw-indicator"
461 draw_indicator.text = "True"
462 current.insert(insertpos, draw_indicator)
463 else:
464 # if there is no draw indicator but there is an image that image should be open-menu-symbolic or x-office-calendar
465 for status_elem in tree.xpath("/interface/object[@id='" + image.text + "']/property[@name='icon_name' or @name='icon-name']"):
466 if status_elem.text != 'x-office-calendar':
467 status_elem.text = "open-menu-symbolic"
469 def enforce_active_in_group_consistency(current):
470 group = None
471 active = None
472 isradiobutton = current.get('class') == "GtkRadioButton"
473 insertpos = 0
474 for child in current:
475 enforce_active_in_group_consistency(child)
476 if not isradiobutton:
477 continue
478 if child.tag == "property":
479 insertpos = insertpos + 1;
480 attributes = child.attrib
481 if attributes.get("name") == "group":
482 group = child
483 if attributes.get("name") == "active":
484 active = child
486 if isradiobutton:
487 if active != None and active.text != "True":
488 raise Exception(sys.argv[1] + ': non-standard active value', active.text)
489 if group != None and active != None:
490 # if there is a group then we are not the leader and should not be active
491 current.remove(active)
492 elif group == None and active == None:
493 # if there is no group then we are the leader and should be active
494 active = etree.Element("property")
495 attributes = active.attrib
496 attributes["name"] = "active"
497 active.text = "True"
498 current.insert(insertpos, active)
500 def enforce_toolbar_can_focus(current):
501 can_focus = None
502 istoolbar = current.get('class') == "GtkToolbar"
503 insertpos = 0
504 for child in current:
505 enforce_toolbar_can_focus(child)
506 if not istoolbar:
507 continue
508 if child.tag == "property":
509 insertpos = insertpos + 1;
510 attributes = child.attrib
511 if attributes.get("name") == "can-focus" or attributes.get("name") == "can_focus":
512 can_focus = child
514 if istoolbar:
515 if can_focus == None:
516 can_focus = etree.Element("property")
517 attributes = can_focus.attrib
518 attributes["name"] = "can-focus"
519 can_focus.text = "True"
520 current.insert(insertpos, can_focus)
521 else:
522 can_focus.text = "True"
524 def enforce_entry_text_column_id_column_for_gtkcombobox(current):
525 entrytextcolumn = None
526 idcolumn = None
527 isgtkcombobox = current.get('class') == "GtkComboBox"
528 insertpos = 0
529 for child in current:
530 enforce_entry_text_column_id_column_for_gtkcombobox(child)
531 if not isgtkcombobox:
532 continue
533 if child.tag == "property":
534 insertpos = insertpos + 1;
535 attributes = child.attrib
536 if attributes.get("name") == "entry_text_column" or attributes.get("name") == "entry-text-column":
537 entrytextcolumn = child
538 if attributes.get("name") == "id_column" or attributes.get("name") == "id-column":
539 idcolumn = child
541 if isgtkcombobox:
542 if entrytextcolumn != None and entrytextcolumn.text != "0":
543 raise Exception(sys.argv[1] + ': non-standard entry_text_column value', entrytextcolumn.text)
544 if idcolumn != None and idcolumn.text != "1":
545 raise Exception(sys.argv[1] + ': non-standard id_column value', idcolumn.text)
546 if entrytextcolumn == None:
547 # if there is no entry_text_column, create one
548 entrytextcolumn = etree.Element("property")
549 attributes = entrytextcolumn.attrib
550 attributes["name"] = "entry-text-column"
551 entrytextcolumn.text = "0"
552 current.insert(insertpos, entrytextcolumn)
553 insertpos = insertpos + 1;
554 if idcolumn == None:
555 # if there is no id_column, create one
556 idcolumn = etree.Element("property")
557 attributes = idcolumn.attrib
558 attributes["name"] = "id-column"
559 idcolumn.text = "1"
560 current.insert(insertpos, idcolumn)
562 def enforce_button_always_show_image(current):
563 image = None
564 always_show_image = None
565 isbutton = current.get('class') == "GtkButton"
566 insertpos = 0
567 for child in current:
568 enforce_button_always_show_image(child)
569 if not isbutton:
570 continue
571 if child.tag == "property":
572 insertpos = insertpos + 1;
573 attributes = child.attrib
574 if attributes.get("name") == "always_show_image" or attributes.get("name") == "always-show-image":
575 always_show_image = child
576 elif attributes.get("name") == "image":
577 image = child
579 if isbutton and image is not None:
580 if always_show_image == None:
581 always_show_image = etree.Element("property")
582 attributes = always_show_image.attrib
583 attributes["name"] = "always-show-image"
584 always_show_image.text = "True"
585 current.insert(insertpos, always_show_image)
586 else:
587 always_show_image.text = "True"
589 def enforce_noshared_adjustments(current, adjustments):
590 for child in current:
591 enforce_noshared_adjustments(child, adjustments)
592 if child.tag == "property":
593 attributes = child.attrib
594 if attributes.get("name") == "adjustment":
595 if child.text in adjustments:
596 raise Exception(sys.argv[1] + ': adjustment used more than once', child.text)
597 adjustments.add(child.text)
599 def enforce_no_productname_in_accessible_description(current, adjustments):
600 for child in current:
601 enforce_no_productname_in_accessible_description(child, adjustments)
602 if child.tag == "property":
603 attributes = child.attrib
604 if attributes.get("name") == "AtkObject::accessible-description":
605 if "%PRODUCTNAME" in child.text:
606 raise Exception(sys.argv[1] + ': %PRODUCTNAME used in accessible-description:' , child.text)
608 with open(sys.argv[1], encoding="utf-8") as f:
609 header = f.readline()
610 f.seek(0)
611 # remove_blank_text so pretty-printed input doesn't disrupt pretty-printed
612 # output if nodes are added or removed
613 parser = etree.XMLParser(remove_blank_text=True)
614 tree = etree.parse(f, parser)
615 # make sure <property name="label" translatable="no"></property> stays like that
616 # and doesn't change to <property name="label" translatable="no"/>
617 for status_elem in tree.xpath("//property[@name='label' and string() = '']"):
618 status_elem.text = ""
619 root = tree.getroot()
621 # do some targeted conversion here
622 # tdf#138848 Copy-and-Paste in input box should not append an ENTER character
623 if not sys.argv[1].endswith('/multiline.ui'): # let this one alone not truncate multiline pastes
624 add_truncate_multiline(root)
625 replace_button_use_stock(root)
626 replace_image_stock(root)
627 remove_check_button_align(root)
628 remove_check_button_relief(root)
629 remove_check_button_image_position(root)
630 remove_spin_button_input_purpose(root)
631 remove_caps_lock_warning(root)
632 remove_spin_button_max_length(root)
633 remove_track_visited_links(root)
634 remove_label_pad(root)
635 remove_label_angle(root)
636 remove_expander_label_fill(root)
637 remove_expander_spacing(root)
638 enforce_menubutton_indicator_consistency(root)
639 enforce_active_in_group_consistency(root)
640 enforce_entry_text_column_id_column_for_gtkcombobox(root)
641 remove_entry_shadow_type(root)
642 remove_double_buffered(root)
643 remove_label_yalign(root)
644 remove_skip_pager_hint(root)
645 remove_gravity(root)
646 remove_toolbutton_focus(root)
647 enforce_toolbar_can_focus(root)
648 enforce_button_always_show_image(root)
649 enforce_noshared_adjustments(root, set())
650 enforce_no_productname_in_accessible_description(root, set())
652 with open(sys.argv[1], 'wb') as o:
653 # without encoding='unicode' (and the matching encode("utf8")) we get &#XXXX replacements for non-ascii characters
654 # which we don't want to see changed in the output
655 o.write(etree.tostring(tree, pretty_print=True, method='xml', encoding='unicode', doctype=header[0:-1]).encode("utf8"))
657 # vim: set shiftwidth=4 softtabstop=4 expandtab: