ofz#44991 don't skip over terminator
[LibreOffice.git] / bin / ui-rules-enforcer.py
blob7c20ee0a6db714b1a573dfacf215efda8732ae65
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-media-stop':
141 stock.text = "media-playback-stop"
142 elif stock.text == 'gtk-media-play':
143 stock.text = "media-playback-start"
144 elif stock.text == 'gtk-media-next':
145 stock.text = "media-skip-forward"
146 elif stock.text == 'gtk-media-previous':
147 stock.text = "media-skip-backward"
148 elif stock.text == 'gtk-close':
149 stock.text = "window-close"
150 elif stock.text == 'gtk-help':
151 stock.text = "help-browser"
152 else:
153 raise Exception(sys.argv[1] + ': unknown stock name', stock.text)
155 def replace_image_stock(current):
156 stock = None
157 isimage = current.get('class') == "GtkImage"
158 for child in current:
159 replace_image_stock(child)
160 if not isimage:
161 continue
162 if child.tag == "property":
163 attributes = child.attrib
164 if attributes.get("name") == "stock":
165 stock = child
167 if isimage and stock != None:
168 do_replace_image_stock(current, stock)
170 def remove_check_button_align(current):
171 xalign = None
172 yalign = None
173 ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
174 for child in current:
175 remove_check_button_align(child)
176 if not ischeckorradiobutton:
177 continue
178 if child.tag == "property":
179 attributes = child.attrib
180 if attributes.get("name") == "xalign":
181 xalign = child
182 if attributes.get("name") == "yalign":
183 yalign = child
185 if ischeckorradiobutton:
186 if xalign != None:
187 if xalign.text != "0":
188 raise Exception(sys.argv[1] + ': non-default xalign', xalign.text)
189 current.remove(xalign)
190 if yalign != None:
191 if yalign.text != "0.5":
192 raise Exception(sys.argv[1] + ': non-default yalign', yalign.text)
193 current.remove(yalign)
195 def remove_check_button_relief(current):
196 relief = None
197 ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
198 for child in current:
199 remove_check_button_relief(child)
200 if not ischeckorradiobutton:
201 continue
202 if child.tag == "property":
203 attributes = child.attrib
204 if attributes.get("name") == "relief":
205 relief = child
207 if ischeckorradiobutton:
208 if relief != None:
209 current.remove(relief)
211 def remove_check_button_image_position(current):
212 image_position = None
213 ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
214 for child in current:
215 remove_check_button_image_position(child)
216 if not ischeckorradiobutton:
217 continue
218 if child.tag == "property":
219 attributes = child.attrib
220 if attributes.get("name") == "image_position" or attributes.get("name") == "image-position":
221 image_position = child
223 if ischeckorradiobutton:
224 if image_position != None:
225 current.remove(image_position)
227 def remove_spin_button_input_purpose(current):
228 input_purpose = None
229 isspinbutton = current.get('class') == "GtkSpinButton"
230 for child in current:
231 remove_spin_button_input_purpose(child)
232 if not isspinbutton:
233 continue
234 if child.tag == "property":
235 attributes = child.attrib
236 if attributes.get("name") == "input_purpose" or attributes.get("name") == "input-purpose":
237 input_purpose = child
239 if isspinbutton:
240 if input_purpose != None:
241 current.remove(input_purpose)
243 def remove_spin_button_max_length(current):
244 max_length = None
245 isspinbutton = current.get('class') == "GtkSpinButton"
246 for child in current:
247 remove_spin_button_max_length(child)
248 if not isspinbutton:
249 continue
250 if child.tag == "property":
251 attributes = child.attrib
252 if attributes.get("name") == "max_length" or attributes.get("name") == "max-length":
253 max_length = child
255 if isspinbutton:
256 if max_length != None:
257 current.remove(max_length)
259 def remove_label_pad(current):
260 xpad = None
261 ypad = None
262 islabel = current.get('class') == "GtkLabel"
263 for child in current:
264 remove_label_pad(child)
265 if not islabel:
266 continue
267 if child.tag == "property":
268 attributes = child.attrib
269 if attributes.get("name") == "xpad":
270 xpad = child
271 elif attributes.get("name") == "ypad":
272 ypad = child
274 if xpad != None:
275 current.remove(xpad)
276 if ypad != None:
277 current.remove(ypad)
279 def remove_track_visited_links(current):
280 track_visited_links = None
281 islabel = current.get('class') == "GtkLabel"
282 for child in current:
283 remove_track_visited_links(child)
284 if not islabel:
285 continue
286 if child.tag == "property":
287 attributes = child.attrib
288 if attributes.get("name") == "track_visited_links" or attributes.get("name") == "track-visited-links":
289 track_visited_links = child
291 if track_visited_links != None:
292 current.remove(track_visited_links)
294 def remove_toolbutton_focus(current):
295 can_focus = None
296 classname = current.get('class');
297 istoolbutton = classname and classname.endswith("ToolButton");
298 for child in current:
299 remove_toolbutton_focus(child)
300 if not istoolbutton:
301 continue
302 if child.tag == "property":
303 attributes = child.attrib
304 if attributes.get("name") == "can_focus" or attributes.get("name") == "can-focus":
305 can_focus = child
307 if can_focus != None:
308 current.remove(can_focus)
310 def remove_double_buffered(current):
311 double_buffered = None
312 for child in current:
313 remove_double_buffered(child)
314 if child.tag == "property":
315 attributes = child.attrib
316 if attributes.get("name") == "double_buffered" or attributes.get("name") == "double-buffered":
317 double_buffered = child
319 if double_buffered != None:
320 current.remove(double_buffered)
322 def remove_skip_pager_hint(current):
323 skip_pager_hint = None
324 for child in current:
325 remove_skip_pager_hint(child)
326 if child.tag == "property":
327 attributes = child.attrib
328 if attributes.get("name") == "skip_pager_hint" or attributes.get("name") == "skip-pager-hint":
329 skip_pager_hint = child
331 if skip_pager_hint != None:
332 current.remove(skip_pager_hint)
334 def remove_expander_label_fill(current):
335 label_fill = None
336 isexpander = current.get('class') == "GtkExpander"
337 for child in current:
338 remove_expander_label_fill(child)
339 if not isexpander:
340 continue
341 if child.tag == "property":
342 attributes = child.attrib
343 if attributes.get("name") == "label_fill" or attributes.get("name") == "label-fill":
344 label_fill = child
346 if label_fill != None:
347 current.remove(label_fill)
349 def remove_expander_spacing(current):
350 spacing = None
351 isexpander = current.get('class') == "GtkExpander"
352 for child in current:
353 remove_expander_spacing(child)
354 if not isexpander:
355 continue
356 if child.tag == "property":
357 attributes = child.attrib
358 if attributes.get("name") == "spacing":
359 spacing = child
361 if spacing != None:
362 current.remove(spacing)
364 def enforce_menubutton_indicator_consistency(current):
365 draw_indicator = None
366 image = None
367 ismenubutton = current.get('class') == "GtkMenuButton"
368 insertpos = 0
369 for child in current:
370 enforce_menubutton_indicator_consistency(child)
371 if not ismenubutton:
372 continue
373 if child.tag == "property":
374 insertpos = insertpos + 1;
375 attributes = child.attrib
376 if attributes.get("name") == "draw_indicator" or attributes.get("name") == "draw-indicator":
377 draw_indicator = child
378 elif attributes.get("name") == "image":
379 image = child
381 if ismenubutton:
382 if draw_indicator == None:
383 if image == None:
384 # if there is no draw indicator and no image there should be a draw indicator
385 draw_indicator = etree.Element("property")
386 attributes = draw_indicator.attrib
387 attributes["name"] = "draw-indicator"
388 draw_indicator.text = "True"
389 current.insert(insertpos, draw_indicator)
390 else:
391 # if there is no draw indicator but there is an image that image should be open-menu-symbolic or x-office-calendar
392 for status_elem in tree.xpath("/interface/object[@id='" + image.text + "']/property[@name='icon_name' or @name='icon-name']"):
393 if status_elem.text != 'x-office-calendar':
394 status_elem.text = "open-menu-symbolic"
396 def enforce_active_in_group_consistency(current):
397 group = None
398 active = None
399 isradiobutton = current.get('class') == "GtkRadioButton"
400 insertpos = 0
401 for child in current:
402 enforce_active_in_group_consistency(child)
403 if not isradiobutton:
404 continue
405 if child.tag == "property":
406 insertpos = insertpos + 1;
407 attributes = child.attrib
408 if attributes.get("name") == "group":
409 group = child
410 if attributes.get("name") == "active":
411 active = child
413 if isradiobutton:
414 if active != None and active.text != "True":
415 raise Exception(sys.argv[1] + ': non-standard active value', active.text)
416 if group != None and active != None:
417 # if there is a group then we are not the leader and should not be active
418 current.remove(active)
419 elif group == None and active == None:
420 # if there is no group then we are the leader and should be active
421 active = etree.Element("property")
422 attributes = active.attrib
423 attributes["name"] = "active"
424 active.text = "True"
425 current.insert(insertpos, active)
427 def enforce_toolbar_can_focus(current):
428 can_focus = None
429 istoolbar = current.get('class') == "GtkToolbar"
430 insertpos = 0
431 for child in current:
432 enforce_toolbar_can_focus(child)
433 if not istoolbar:
434 continue
435 if child.tag == "property":
436 insertpos = insertpos + 1;
437 attributes = child.attrib
438 if attributes.get("name") == "can-focus" or attributes.get("name") == "can_focus":
439 can_focus = child
441 if istoolbar:
442 if can_focus == None:
443 can_focus = etree.Element("property")
444 attributes = can_focus.attrib
445 attributes["name"] = "can-focus"
446 can_focus.text = "True"
447 current.insert(insertpos, can_focus)
448 else:
449 can_focus.text = "True"
451 def enforce_entry_text_column_id_column_for_gtkcombobox(current):
452 entrytextcolumn = None
453 idcolumn = None
454 isgtkcombobox = current.get('class') == "GtkComboBox"
455 insertpos = 0
456 for child in current:
457 enforce_entry_text_column_id_column_for_gtkcombobox(child)
458 if not isgtkcombobox:
459 continue
460 if child.tag == "property":
461 insertpos = insertpos + 1;
462 attributes = child.attrib
463 if attributes.get("name") == "entry_text_column" or attributes.get("name") == "entry-text-column":
464 entrytextcolumn = child
465 if attributes.get("name") == "id_column" or attributes.get("name") == "id-column":
466 idcolumn = child
468 if isgtkcombobox:
469 if entrytextcolumn != None and entrytextcolumn.text != "0":
470 raise Exception(sys.argv[1] + ': non-standard entry_text_column value', entrytextcolumn.text)
471 if idcolumn != None and idcolumn.text != "1":
472 raise Exception(sys.argv[1] + ': non-standard id_column value', idcolumn.text)
473 if entrytextcolumn == None:
474 # if there is no entry_text_column, create one
475 entrytextcolumn = etree.Element("property")
476 attributes = entrytextcolumn.attrib
477 attributes["name"] = "entry-text-column"
478 entrytextcolumn.text = "0"
479 current.insert(insertpos, entrytextcolumn)
480 insertpos = insertpos + 1;
481 if idcolumn == None:
482 # if there is no id_column, create one
483 idcolumn = etree.Element("property")
484 attributes = idcolumn.attrib
485 attributes["name"] = "id-column"
486 idcolumn.text = "1"
487 current.insert(insertpos, idcolumn)
489 def enforce_button_always_show_image(current):
490 image = None
491 always_show_image = None
492 isbutton = current.get('class') == "GtkButton"
493 insertpos = 0
494 for child in current:
495 enforce_button_always_show_image(child)
496 if not isbutton:
497 continue
498 if child.tag == "property":
499 insertpos = insertpos + 1;
500 attributes = child.attrib
501 if attributes.get("name") == "always_show_image" or attributes.get("name") == "always-show-image":
502 always_show_image = child
503 elif attributes.get("name") == "image":
504 image = child
506 if isbutton and image is not None:
507 if always_show_image == None:
508 always_show_image = etree.Element("property")
509 attributes = always_show_image.attrib
510 attributes["name"] = "always-show-image"
511 always_show_image.text = "True"
512 current.insert(insertpos, always_show_image)
513 else:
514 always_show_image.text = "True"
516 def enforce_noshared_adjustments(current, adjustments):
517 for child in current:
518 enforce_noshared_adjustments(child, adjustments)
519 if child.tag == "property":
520 attributes = child.attrib
521 if attributes.get("name") == "adjustment":
522 if child.text in adjustments:
523 raise Exception(sys.argv[1] + ': adjustment used more than once', child.text)
524 adjustments.add(child.text)
526 with open(sys.argv[1], encoding="utf-8") as f:
527 header = f.readline()
528 f.seek(0)
529 # remove_blank_text so pretty-printed input doesn't disrupt pretty-printed
530 # output if nodes are added or removed
531 parser = etree.XMLParser(remove_blank_text=True)
532 tree = etree.parse(f, parser)
533 # make sure <property name="label" translatable="no"></property> stays like that
534 # and doesn't change to <property name="label" translatable="no"/>
535 for status_elem in tree.xpath("//property[@name='label' and string() = '']"):
536 status_elem.text = ""
537 root = tree.getroot()
539 # do some targeted conversion here
540 # tdf#138848 Copy-and-Paste in input box should not append an ENTER character
541 if not sys.argv[1].endswith('/multiline.ui'): # let this one alone not truncate multiline pastes
542 add_truncate_multiline(root)
543 replace_button_use_stock(root)
544 replace_image_stock(root)
545 remove_check_button_align(root)
546 remove_check_button_relief(root)
547 remove_check_button_image_position(root)
548 remove_spin_button_input_purpose(root)
549 remove_spin_button_max_length(root)
550 remove_track_visited_links(root)
551 remove_label_pad(root)
552 remove_expander_label_fill(root)
553 remove_expander_spacing(root)
554 enforce_menubutton_indicator_consistency(root)
555 enforce_active_in_group_consistency(root)
556 enforce_entry_text_column_id_column_for_gtkcombobox(root)
557 remove_double_buffered(root)
558 remove_skip_pager_hint(root)
559 remove_toolbutton_focus(root)
560 enforce_toolbar_can_focus(root)
561 enforce_button_always_show_image(root)
562 enforce_noshared_adjustments(root, set())
564 with open(sys.argv[1], 'wb') as o:
565 # without encoding='unicode' (and the matching encode("utf8")) we get &#XXXX replacements for non-ascii characters
566 # which we don't want to see changed in the output
567 o.write(etree.tostring(tree, pretty_print=True, method='xml', encoding='unicode', doctype=header[0:-1]).encode("utf8"))
569 # vim: set shiftwidth=4 softtabstop=4 expandtab: