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_double_buffered(current
):
295 double_buffered
= None
296 for child
in current
:
297 remove_double_buffered(child
)
298 if child
.tag
== "property":
299 attributes
= child
.attrib
300 if attributes
.get("name") == "double_buffered" or attributes
.get("name") == "double-buffered":
301 double_buffered
= child
303 if double_buffered
!= None:
304 current
.remove(double_buffered
)
306 def remove_skip_pager_hint(current
):
307 skip_pager_hint
= None
308 for child
in current
:
309 remove_skip_pager_hint(child
)
310 if child
.tag
== "property":
311 attributes
= child
.attrib
312 if attributes
.get("name") == "skip_pager_hint" or attributes
.get("name") == "skip-pager-hint":
313 skip_pager_hint
= child
315 if skip_pager_hint
!= None:
316 current
.remove(skip_pager_hint
)
318 def remove_expander_label_fill(current
):
320 isexpander
= current
.get('class') == "GtkExpander"
321 for child
in current
:
322 remove_expander_label_fill(child
)
325 if child
.tag
== "property":
326 attributes
= child
.attrib
327 if attributes
.get("name") == "label_fill" or attributes
.get("name") == "label-fill":
330 if label_fill
!= None:
331 current
.remove(label_fill
)
333 def remove_expander_spacing(current
):
335 isexpander
= current
.get('class') == "GtkExpander"
336 for child
in current
:
337 remove_expander_spacing(child
)
340 if child
.tag
== "property":
341 attributes
= child
.attrib
342 if attributes
.get("name") == "spacing":
346 current
.remove(spacing
)
348 def enforce_menubutton_indicator_consistency(current
):
349 draw_indicator
= None
351 ismenubutton
= current
.get('class') == "GtkMenuButton"
353 for child
in current
:
354 enforce_menubutton_indicator_consistency(child
)
357 if child
.tag
== "property":
358 insertpos
= insertpos
+ 1;
359 attributes
= child
.attrib
360 if attributes
.get("name") == "draw_indicator" or attributes
.get("name") == "draw-indicator":
361 draw_indicator
= child
362 elif attributes
.get("name") == "image":
366 if draw_indicator
== None:
368 # if there is no draw indicator and no image there should be a draw indicator
369 draw_indicator
= etree
.Element("property")
370 attributes
= draw_indicator
.attrib
371 attributes
["name"] = "draw-indicator"
372 draw_indicator
.text
= "True"
373 current
.insert(insertpos
, draw_indicator
)
375 # if there is no draw indicator but there is an image that image should be open-menu-symbolic or x-office-calendar
376 for status_elem
in tree
.xpath("/interface/object[@id='" + image
.text
+ "']/property[@name='icon_name' or @name='icon-name']"):
377 if status_elem
.text
!= 'x-office-calendar':
378 status_elem
.text
= "open-menu-symbolic"
380 def enforce_active_in_group_consistency(current
):
383 isradiobutton
= current
.get('class') == "GtkRadioButton"
385 for child
in current
:
386 enforce_active_in_group_consistency(child
)
387 if not isradiobutton
:
389 if child
.tag
== "property":
390 insertpos
= insertpos
+ 1;
391 attributes
= child
.attrib
392 if attributes
.get("name") == "group":
394 if attributes
.get("name") == "active":
398 if active
!= None and active
.text
!= "True":
399 raise Exception(sys
.argv
[1] + ': non-standard active value', active
.text
)
400 if group
!= None and active
!= None:
401 # if there is a group then we are not the leader and should not be active
402 current
.remove(active
)
403 elif group
== None and active
== None:
404 # if there is no group then we are the leader and should be active
405 active
= etree
.Element("property")
406 attributes
= active
.attrib
407 attributes
["name"] = "active"
409 current
.insert(insertpos
, active
)
411 def enforce_entry_text_column_id_column_for_gtkcombobox(current
):
412 entrytextcolumn
= None
414 isgtkcombobox
= current
.get('class') == "GtkComboBox"
416 for child
in current
:
417 enforce_entry_text_column_id_column_for_gtkcombobox(child
)
418 if not isgtkcombobox
:
420 if child
.tag
== "property":
421 insertpos
= insertpos
+ 1;
422 attributes
= child
.attrib
423 if attributes
.get("name") == "entry_text_column" or attributes
.get("name") == "entry-text-column":
424 entrytextcolumn
= child
425 if attributes
.get("name") == "id_column" or attributes
.get("name") == "id-column":
429 if entrytextcolumn
!= None and entrytextcolumn
.text
!= "0":
430 raise Exception(sys
.argv
[1] + ': non-standard entry_text_column value', entrytextcolumn
.text
)
431 if idcolumn
!= None and idcolumn
.text
!= "1":
432 raise Exception(sys
.argv
[1] + ': non-standard id_column value', idcolumn
.text
)
433 if entrytextcolumn
== None:
434 # if there is no entry_text_column, create one
435 entrytextcolumn
= etree
.Element("property")
436 attributes
= entrytextcolumn
.attrib
437 attributes
["name"] = "entry-text-column"
438 entrytextcolumn
.text
= "0"
439 current
.insert(insertpos
, entrytextcolumn
)
440 insertpos
= insertpos
+ 1;
442 # if there is no id_column, create one
443 idcolumn
= etree
.Element("property")
444 attributes
= idcolumn
.attrib
445 attributes
["name"] = "id-column"
447 current
.insert(insertpos
, idcolumn
)
449 with
open(sys
.argv
[1], encoding
="utf-8") as f
:
450 header
= f
.readline()
452 # remove_blank_text so pretty-printed input doesn't disrupt pretty-printed
453 # output if nodes are added or removed
454 parser
= etree
.XMLParser(remove_blank_text
=True)
455 tree
= etree
.parse(f
, parser
)
456 # make sure <property name="label" translatable="no"></property> stays like that
457 # and doesn't change to <property name="label" translatable="no"/>
458 for status_elem
in tree
.xpath("//property[@name='label' and string() = '']"):
459 status_elem
.text
= ""
460 root
= tree
.getroot()
462 # do some targeted conversion here
463 # tdf#138848 Copy-and-Paste in input box should not append an ENTER character
464 if not sys
.argv
[1].endswith('/multiline.ui'): # let this one alone not truncate multiline pastes
465 add_truncate_multiline(root
)
466 replace_button_use_stock(root
)
467 replace_image_stock(root
)
468 remove_check_button_align(root
)
469 remove_check_button_relief(root
)
470 remove_check_button_image_position(root
)
471 remove_spin_button_input_purpose(root
)
472 remove_spin_button_max_length(root
)
473 remove_track_visited_links(root
)
474 remove_label_pad(root
)
475 remove_expander_label_fill(root
)
476 remove_expander_spacing(root
)
477 enforce_menubutton_indicator_consistency(root
)
478 enforce_active_in_group_consistency(root
)
479 enforce_entry_text_column_id_column_for_gtkcombobox(root
)
480 remove_double_buffered(root
)
481 remove_skip_pager_hint(root
)
484 with
open(sys
.argv
[1], 'wb') as o
:
485 # without encoding='unicode' (and the matching encode("utf8")) we get &#XXXX replacements for non-ascii characters
486 # which we don't want to see changed in the output
487 o
.write(etree
.tostring(tree
, pretty_print
=True, method
='xml', encoding
='unicode', doctype
=header
[0:-1]).encode("utf8"))
489 # vim: set shiftwidth=4 softtabstop=4 expandtab: