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
21 def add_truncate_multiline(current
):
22 use_truncate_multiline
= False
23 istarget
= current
.get('class') == "GtkEntry" or current
.get('class') == "GtkSpinButton"
26 add_truncate_multiline(child
)
27 insertpos
= insertpos
+ 1;
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
):
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':
55 elif label
.text
== 'gtk-apply':
57 elif label
.text
== 'gtk-cancel':
58 label
.text
= "_Cancel"
59 elif label
.text
== 'gtk-close':
61 elif label
.text
== 'gtk-delete':
62 label
.text
= "_Delete"
63 elif label
.text
== 'gtk-edit':
65 elif label
.text
== 'gtk-help':
67 elif label
.text
== 'gtk-new':
69 elif label
.text
== 'gtk-no':
71 elif label
.text
== 'gtk-ok':
73 elif label
.text
== 'gtk-remove':
74 label
.text
= "_Remove"
75 elif label
.text
== 'gtk-revert-to-saved':
77 elif label
.text
== 'gtk-yes':
80 raise Exception(sys
.argv
[1] + ': unknown label', label
.text
)
82 def replace_button_use_stock(current
):
86 isbutton
= current
.get('class') == "GtkButton"
89 replace_button_use_stock(child
)
90 insertpos
= insertpos
+ 1;
93 if child
.tag
== "property":
94 attributes
= child
.attrib
95 if attributes
.get("name") == "use_underline" or attributes
.get("name") == "use-underline":
97 if attributes
.get("name") == "use_stock" or attributes
.get("name") == "use-stock":
99 if attributes
.get("name") == "label":
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':
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"
153 raise Exception(sys
.argv
[1] + ': unknown stock name', stock
.text
)
155 def replace_image_stock(current
):
157 isimage
= current
.get('class') == "GtkImage"
158 for child
in current
:
159 replace_image_stock(child
)
162 if child
.tag
== "property":
163 attributes
= child
.attrib
164 if attributes
.get("name") == "stock":
167 if isimage
and stock
!= None:
168 do_replace_image_stock(current
, stock
)
170 def remove_check_button_align(current
):
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
:
178 if child
.tag
== "property":
179 attributes
= child
.attrib
180 if attributes
.get("name") == "xalign":
182 if attributes
.get("name") == "yalign":
185 if ischeckorradiobutton
:
187 if xalign
.text
!= "0":
188 raise Exception(sys
.argv
[1] + ': non-default xalign', xalign
.text
)
189 current
.remove(xalign
)
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
):
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
:
202 if child
.tag
== "property":
203 attributes
= child
.attrib
204 if attributes
.get("name") == "relief":
207 if ischeckorradiobutton
:
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
:
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
):
229 isspinbutton
= current
.get('class') == "GtkSpinButton"
230 for child
in current
:
231 remove_spin_button_input_purpose(child
)
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
240 if input_purpose
!= None:
241 current
.remove(input_purpose
)
243 def remove_spin_button_max_length(current
):
245 isspinbutton
= current
.get('class') == "GtkSpinButton"
246 for child
in current
:
247 remove_spin_button_max_length(child
)
250 if child
.tag
== "property":
251 attributes
= child
.attrib
252 if attributes
.get("name") == "max_length" or attributes
.get("name") == "max-length":
256 if max_length
!= None:
257 current
.remove(max_length
)
259 def remove_label_pad(current
):
262 islabel
= current
.get('class') == "GtkLabel"
263 for child
in current
:
264 remove_label_pad(child
)
267 if child
.tag
== "property":
268 attributes
= child
.attrib
269 if attributes
.get("name") == "xpad":
271 elif attributes
.get("name") == "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
)
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
):
296 classname
= current
.get('class');
297 istoolbutton
= classname
and classname
.endswith("ToolButton");
298 for child
in current
:
299 remove_toolbutton_focus(child
)
302 if child
.tag
== "property":
303 attributes
= child
.attrib
304 if attributes
.get("name") == "can_focus" or attributes
.get("name") == "can-focus":
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
):
336 isexpander
= current
.get('class') == "GtkExpander"
337 for child
in current
:
338 remove_expander_label_fill(child
)
341 if child
.tag
== "property":
342 attributes
= child
.attrib
343 if attributes
.get("name") == "label_fill" or attributes
.get("name") == "label-fill":
346 if label_fill
!= None:
347 current
.remove(label_fill
)
349 def remove_expander_spacing(current
):
351 isexpander
= current
.get('class') == "GtkExpander"
352 for child
in current
:
353 remove_expander_spacing(child
)
356 if child
.tag
== "property":
357 attributes
= child
.attrib
358 if attributes
.get("name") == "spacing":
362 current
.remove(spacing
)
364 def enforce_menubutton_indicator_consistency(current
):
365 draw_indicator
= None
367 ismenubutton
= current
.get('class') == "GtkMenuButton"
369 for child
in current
:
370 enforce_menubutton_indicator_consistency(child
)
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":
382 if draw_indicator
== 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
)
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
):
399 isradiobutton
= current
.get('class') == "GtkRadioButton"
401 for child
in current
:
402 enforce_active_in_group_consistency(child
)
403 if not isradiobutton
:
405 if child
.tag
== "property":
406 insertpos
= insertpos
+ 1;
407 attributes
= child
.attrib
408 if attributes
.get("name") == "group":
410 if attributes
.get("name") == "active":
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"
425 current
.insert(insertpos
, active
)
427 def enforce_toolbar_can_focus(current
):
429 istoolbar
= current
.get('class') == "GtkToolbar"
431 for child
in current
:
432 enforce_toolbar_can_focus(child
)
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":
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
)
449 can_focus
.text
= "True"
451 def enforce_entry_text_column_id_column_for_gtkcombobox(current
):
452 entrytextcolumn
= None
454 isgtkcombobox
= current
.get('class') == "GtkComboBox"
456 for child
in current
:
457 enforce_entry_text_column_id_column_for_gtkcombobox(child
)
458 if not isgtkcombobox
:
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":
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;
482 # if there is no id_column, create one
483 idcolumn
= etree
.Element("property")
484 attributes
= idcolumn
.attrib
485 attributes
["name"] = "id-column"
487 current
.insert(insertpos
, idcolumn
)
489 def enforce_button_always_show_image(current
):
491 always_show_image
= None
492 isbutton
= current
.get('class') == "GtkButton"
494 for child
in current
:
495 enforce_button_always_show_image(child
)
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":
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
)
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()
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: