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-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"
155 raise Exception(sys
.argv
[1] + ': unknown stock name', stock
.text
)
157 def replace_image_stock(current
):
159 isimage
= current
.get('class') == "GtkImage"
160 for child
in current
:
161 replace_image_stock(child
)
164 if child
.tag
== "property":
165 attributes
= child
.attrib
166 if attributes
.get("name") == "stock":
169 if isimage
and stock
!= None:
170 do_replace_image_stock(current
, stock
)
172 def remove_check_button_align(current
):
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
:
180 if child
.tag
== "property":
181 attributes
= child
.attrib
182 if attributes
.get("name") == "xalign":
184 if attributes
.get("name") == "yalign":
187 if ischeckorradiobutton
:
189 if xalign
.text
!= "0":
190 raise Exception(sys
.argv
[1] + ': non-default xalign', xalign
.text
)
191 current
.remove(xalign
)
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
):
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
:
204 if child
.tag
== "property":
205 attributes
= child
.attrib
206 if attributes
.get("name") == "relief":
209 if ischeckorradiobutton
:
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
:
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
):
231 isspinbutton
= current
.get('class') == "GtkSpinButton"
232 for child
in current
:
233 remove_spin_button_input_purpose(child
)
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
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
)
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
258 if caps_lock_warning
!= None:
259 current
.remove(caps_lock_warning
)
261 def remove_spin_button_max_length(current
):
263 isspinbutton
= current
.get('class') == "GtkSpinButton"
264 for child
in current
:
265 remove_spin_button_max_length(child
)
268 if child
.tag
== "property":
269 attributes
= child
.attrib
270 if attributes
.get("name") == "max_length" or attributes
.get("name") == "max-length":
274 if max_length
!= None:
275 current
.remove(max_length
)
277 def remove_entry_shadow_type(current
):
279 isentry
= current
.get('class') == "GtkEntry"
280 for child
in current
:
281 remove_entry_shadow_type(child
)
284 if child
.tag
== "property":
285 attributes
= child
.attrib
286 if attributes
.get("name") == "shadow_type" or attributes
.get("name") == "shadow-type":
290 if shadow_type
!= None:
291 current
.remove(shadow_type
)
293 def remove_label_pad(current
):
296 islabel
= current
.get('class') == "GtkLabel"
297 for child
in current
:
298 remove_label_pad(child
)
301 if child
.tag
== "property":
302 attributes
= child
.attrib
303 if attributes
.get("name") == "xpad":
305 elif attributes
.get("name") == "ypad":
313 def remove_label_angle(current
):
315 islabel
= current
.get('class') == "GtkLabel"
316 for child
in current
:
317 remove_label_angle(child
)
320 if child
.tag
== "property":
321 attributes
= child
.attrib
322 if attributes
.get("name") == "angle":
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
)
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
):
345 classname
= current
.get('class');
346 istoolbutton
= classname
and classname
.endswith("ToolButton");
347 for child
in current
:
348 remove_toolbutton_focus(child
)
351 if child
.tag
== "property":
352 attributes
= child
.attrib
353 if attributes
.get("name") == "can_focus" or attributes
.get("name") == "can-focus":
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
):
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":
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
):
397 for child
in current
:
398 remove_gravity(child
)
399 if child
.tag
== "property":
400 attributes
= child
.attrib
401 if attributes
.get("name") == "gravity":
405 current
.remove(gravity
)
407 def remove_expander_label_fill(current
):
409 isexpander
= current
.get('class') == "GtkExpander"
410 for child
in current
:
411 remove_expander_label_fill(child
)
414 if child
.tag
== "property":
415 attributes
= child
.attrib
416 if attributes
.get("name") == "label_fill" or attributes
.get("name") == "label-fill":
419 if label_fill
!= None:
420 current
.remove(label_fill
)
422 def remove_expander_spacing(current
):
424 isexpander
= current
.get('class') == "GtkExpander"
425 for child
in current
:
426 remove_expander_spacing(child
)
429 if child
.tag
== "property":
430 attributes
= child
.attrib
431 if attributes
.get("name") == "spacing":
435 current
.remove(spacing
)
437 def enforce_menubutton_indicator_consistency(current
):
438 draw_indicator
= None
440 ismenubutton
= current
.get('class') == "GtkMenuButton"
442 for child
in current
:
443 enforce_menubutton_indicator_consistency(child
)
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":
455 if draw_indicator
== 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
)
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
):
472 isradiobutton
= current
.get('class') == "GtkRadioButton"
474 for child
in current
:
475 enforce_active_in_group_consistency(child
)
476 if not isradiobutton
:
478 if child
.tag
== "property":
479 insertpos
= insertpos
+ 1;
480 attributes
= child
.attrib
481 if attributes
.get("name") == "group":
483 if attributes
.get("name") == "active":
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"
498 current
.insert(insertpos
, active
)
500 def enforce_toolbar_can_focus(current
):
502 istoolbar
= current
.get('class') == "GtkToolbar"
504 for child
in current
:
505 enforce_toolbar_can_focus(child
)
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":
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
)
522 can_focus
.text
= "True"
524 def enforce_entry_text_column_id_column_for_gtkcombobox(current
):
525 entrytextcolumn
= None
527 isgtkcombobox
= current
.get('class') == "GtkComboBox"
529 for child
in current
:
530 enforce_entry_text_column_id_column_for_gtkcombobox(child
)
531 if not isgtkcombobox
:
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":
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;
555 # if there is no id_column, create one
556 idcolumn
= etree
.Element("property")
557 attributes
= idcolumn
.attrib
558 attributes
["name"] = "id-column"
560 current
.insert(insertpos
, idcolumn
)
562 def enforce_button_always_show_image(current
):
564 always_show_image
= None
565 isbutton
= current
.get('class') == "GtkButton"
567 for child
in current
:
568 enforce_button_always_show_image(child
)
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":
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
)
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()
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
)
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: