#!/bin/python # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*- # # This file is part of the LibreOffice project. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # ui-rules-enforcer enforces the .ui rules and properties used by LibreOffice # mostly the deprecations of # https://developer.gnome.org/gtk4/stable/gtk-migrating-3-to-4.html # and a few other home cooked rules # for any existing .ui this should parse it and overwrite it with the same content # e.g. for a in `git ls-files "*.ui"`; do bin/ui-rules-enforcer.py $a; done import lxml.etree as etree import sys def add_truncate_multiline(current): use_truncate_multiline = False istarget = current.get('class') == "GtkEntry" or current.get('class') == "GtkSpinButton" insertpos = 0 for child in current: add_truncate_multiline(child) insertpos = insertpos + 1 if not istarget: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "truncate_multiline" or attributes.get("name") == "truncate-multiline": use_truncate_multiline = True if istarget and not use_truncate_multiline: truncate_multiline = etree.Element("property") attributes = truncate_multiline.attrib attributes["name"] = "truncate-multiline" truncate_multiline.text = "True" current.insert(insertpos - 1, truncate_multiline) def do_replace_button_use_stock(current, use_stock, use_underline, label, insertpos): if not use_underline: underline = etree.Element("property") attributes = underline.attrib attributes["name"] = "use-underline" underline.text = "True" current.insert(insertpos - 1, underline) current.remove(use_stock) attributes = label.attrib attributes["translatable"] = "yes" attributes["context"] = "stock" if label.text == 'gtk-add': label.text = "_Add" elif label.text == 'gtk-apply': label.text = "_Apply" elif label.text == 'gtk-cancel': label.text = "_Cancel" elif label.text == 'gtk-close': label.text = "_Close" elif label.text == 'gtk-delete': label.text = "_Delete" elif label.text == 'gtk-edit': label.text = "_Edit" elif label.text == 'gtk-help': label.text = "_Help" elif label.text == 'gtk-new': label.text = "_New" elif label.text == 'gtk-no': label.text = "_No" elif label.text == 'gtk-ok': label.text = "_OK" elif label.text == 'gtk-remove': label.text = "_Remove" elif label.text == 'gtk-revert-to-saved': label.text = "_Reset" elif label.text == 'gtk-yes': label.text = "_Yes" else: raise Exception(sys.argv[1] + ': unknown label', label.text) def replace_button_use_stock(current): use_underline = False use_stock = None label = None isbutton = current.get('class') == "GtkButton" insertpos = 0 for child in current: replace_button_use_stock(child) insertpos = insertpos + 1 if not isbutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "use_underline" or attributes.get("name") == "use-underline": use_underline = True if attributes.get("name") == "use_stock" or attributes.get("name") == "use-stock": use_stock = child if attributes.get("name") == "label": label = child if isbutton and use_stock is not None: do_replace_button_use_stock(current, use_stock, use_underline, label, insertpos) def do_replace_image_stock(current, stock): attributes = stock.attrib attributes["name"] = "icon-name" if stock.text == 'gtk-add': stock.text = "list-add" elif stock.text == 'gtk-remove': stock.text = "list-remove" elif stock.text == 'gtk-paste': stock.text = "edit-paste" elif stock.text == 'gtk-index': stock.text = "vcl/res/index.png" elif stock.text == 'gtk-refresh': stock.text = "view-refresh" elif stock.text == 'gtk-dialog-error': stock.text = "dialog-error" elif stock.text == 'gtk-apply': stock.text = "sw/res/sc20558.png" elif stock.text == 'gtk-missing-image': stock.text = "missing-image" elif stock.text == 'gtk-copy': stock.text = "edit-copy" elif stock.text == 'gtk-go-back': stock.text = "go-previous" elif stock.text == 'gtk-go-forward': stock.text = "go-next" elif stock.text == 'gtk-go-down': stock.text = "go-down" elif stock.text == 'gtk-go-up': stock.text = "go-up" elif stock.text == 'gtk-goto-first': stock.text = "go-first" elif stock.text == 'gtk-goto-last': stock.text = "go-last" elif stock.text == 'gtk-new': stock.text = "document-new" elif stock.text == 'gtk-open': stock.text = "document-open" elif stock.text == 'gtk-media-stop': stock.text = "media-playback-stop" elif stock.text == 'gtk-media-play': stock.text = "media-playback-start" elif stock.text == 'gtk-media-next': stock.text = "media-skip-forward" elif stock.text == 'gtk-media-previous': stock.text = "media-skip-backward" elif stock.text == 'gtk-close': stock.text = "window-close" elif stock.text == 'gtk-help': stock.text = "help-browser" else: raise Exception(sys.argv[1] + ': unknown stock name', stock.text) def replace_image_stock(current): stock = None isimage = current.get('class') == "GtkImage" for child in current: replace_image_stock(child) if not isimage: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "stock": stock = child if isimage and stock is not None: do_replace_image_stock(current, stock) def remove_check_button_align(current): xalign = None yalign = None ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton" for child in current: remove_check_button_align(child) if not ischeckorradiobutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "xalign": xalign = child if attributes.get("name") == "yalign": yalign = child if ischeckorradiobutton: if xalign is not None: if xalign.text != "0": raise Exception(sys.argv[1] + ': non-default xalign', xalign.text) current.remove(xalign) if yalign is not None: if yalign.text != "0.5": raise Exception(sys.argv[1] + ': non-default yalign', yalign.text) current.remove(yalign) def remove_check_button_relief(current): relief = None ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton" for child in current: remove_check_button_relief(child) if not ischeckorradiobutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "relief": relief = child if ischeckorradiobutton: if relief is not None: current.remove(relief) def remove_check_button_image_position(current): image_position = None ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton" for child in current: remove_check_button_image_position(child) if not ischeckorradiobutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "image_position" or attributes.get("name") == "image-position": image_position = child if ischeckorradiobutton: if image_position is not None: current.remove(image_position) def remove_spin_button_input_purpose(current): input_purpose = None isspinbutton = current.get('class') == "GtkSpinButton" for child in current: remove_spin_button_input_purpose(child) if not isspinbutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "input_purpose" or attributes.get("name") == "input-purpose": input_purpose = child if isspinbutton: if input_purpose is not None: current.remove(input_purpose) def remove_caps_lock_warning(current): caps_lock_warning = None iscandidate = current.get('class') == "GtkSpinButton" or current.get('class') == "GtkEntry" for child in current: remove_caps_lock_warning(child) if not iscandidate: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "caps_lock_warning" or attributes.get("name") == "caps-lock-warning": caps_lock_warning = child if iscandidate: if caps_lock_warning is not None: current.remove(caps_lock_warning) def remove_spin_button_max_length(current): max_length = None isspinbutton = current.get('class') == "GtkSpinButton" for child in current: remove_spin_button_max_length(child) if not isspinbutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "max_length" or attributes.get("name") == "max-length": max_length = child if isspinbutton: if max_length is not None: current.remove(max_length) def remove_entry_shadow_type(current): shadow_type = None isentry = current.get('class') == "GtkEntry" for child in current: remove_entry_shadow_type(child) if not isentry: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "shadow_type" or attributes.get("name") == "shadow-type": shadow_type = child if isentry: if shadow_type is not None: current.remove(shadow_type) def remove_label_pad(current): xpad = None ypad = None islabel = current.get('class') == "GtkLabel" for child in current: remove_label_pad(child) if not islabel: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "xpad": xpad = child elif attributes.get("name") == "ypad": ypad = child if xpad is not None: current.remove(xpad) if ypad is not None: current.remove(ypad) def remove_label_angle(current): angle = None islabel = current.get('class') == "GtkLabel" for child in current: remove_label_angle(child) if not islabel: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "angle": angle = child if angle is not None: current.remove(angle) def remove_track_visited_links(current): track_visited_links = None islabel = current.get('class') == "GtkLabel" for child in current: remove_track_visited_links(child) if not islabel: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "track_visited_links" or attributes.get("name") == "track-visited-links": track_visited_links = child if track_visited_links is not None: current.remove(track_visited_links) def remove_toolbutton_focus(current): can_focus = None classname = current.get('class') istoolbutton = classname and classname.endswith("ToolButton") for child in current: remove_toolbutton_focus(child) if not istoolbutton: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "can_focus" or attributes.get("name") == "can-focus": can_focus = child if can_focus is not None: current.remove(can_focus) def remove_double_buffered(current): double_buffered = None for child in current: remove_double_buffered(child) if child.tag == "property": attributes = child.attrib if attributes.get("name") == "double_buffered" or attributes.get("name") == "double-buffered": double_buffered = child if double_buffered is not None: current.remove(double_buffered) def remove_label_yalign(current): label_yalign = None for child in current: remove_label_yalign(child) if child.tag == "property": attributes = child.attrib if attributes.get("name") == "label_yalign" or attributes.get("name") == "label-yalign": label_yalign = child if label_yalign is not None: current.remove(label_yalign) def remove_skip_pager_hint(current): skip_pager_hint = None for child in current: remove_skip_pager_hint(child) if child.tag == "property": attributes = child.attrib if attributes.get("name") == "skip_pager_hint" or attributes.get("name") == "skip-pager-hint": skip_pager_hint = child if skip_pager_hint is not None: current.remove(skip_pager_hint) def remove_gravity(current): gravity = None for child in current: remove_gravity(child) if child.tag == "property": attributes = child.attrib if attributes.get("name") == "gravity": gravity = child if gravity is not None: current.remove(gravity) def remove_expander_label_fill(current): label_fill = None isexpander = current.get('class') == "GtkExpander" for child in current: remove_expander_label_fill(child) if not isexpander: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "label_fill" or attributes.get("name") == "label-fill": label_fill = child if label_fill is not None: current.remove(label_fill) def remove_expander_spacing(current): spacing = None isexpander = current.get('class') == "GtkExpander" for child in current: remove_expander_spacing(child) if not isexpander: continue if child.tag == "property": attributes = child.attrib if attributes.get("name") == "spacing": spacing = child if spacing is not None: current.remove(spacing) def enforce_menubutton_indicator_consistency(current): draw_indicator = None image = None ismenubutton = current.get('class') == "GtkMenuButton" insertpos = 0 for child in current: enforce_menubutton_indicator_consistency(child) if not ismenubutton: continue if child.tag == "property": insertpos = insertpos + 1 attributes = child.attrib if attributes.get("name") == "draw_indicator" or attributes.get("name") == "draw-indicator": draw_indicator = child elif attributes.get("name") == "image": image = child if ismenubutton: if draw_indicator is None: if image is None: # if there is no draw indicator and no image there should be a draw indicator draw_indicator = etree.Element("property") attributes = draw_indicator.attrib attributes["name"] = "draw-indicator" draw_indicator.text = "True" current.insert(insertpos, draw_indicator) else: # if there is no draw indicator but there is an image that image should be open-menu-symbolic or x-office-calendar for status_elem in tree.xpath("/interface/object[@id='" + image.text + "']/property[@name='icon_name' or @name='icon-name']"): if status_elem.text != 'x-office-calendar': status_elem.text = "open-menu-symbolic" def enforce_active_in_group_consistency(current): group = None active = None isradiobutton = current.get('class') == "GtkRadioButton" insertpos = 0 for child in current: enforce_active_in_group_consistency(child) if not isradiobutton: continue if child.tag == "property": insertpos = insertpos + 1 attributes = child.attrib if attributes.get("name") == "group": group = child if attributes.get("name") == "active": active = child if isradiobutton: if active is not None and active.text != "True": raise Exception(sys.argv[1] + ': non-standard active value', active.text) if group is not None and active is not None: # if there is a group then we are not the leader and should not be active current.remove(active) elif group is None and active is None: # if there is no group then we are the leader and should be active active = etree.Element("property") attributes = active.attrib attributes["name"] = "active" active.text = "True" current.insert(insertpos, active) def enforce_toolbar_can_focus(current): can_focus = None istoolbar = current.get('class') == "GtkToolbar" insertpos = 0 for child in current: enforce_toolbar_can_focus(child) if not istoolbar: continue if child.tag == "property": insertpos = insertpos + 1 attributes = child.attrib if attributes.get("name") == "can-focus" or attributes.get("name") == "can_focus": can_focus = child if istoolbar: if can_focus is None: can_focus = etree.Element("property") attributes = can_focus.attrib attributes["name"] = "can-focus" can_focus.text = "True" current.insert(insertpos, can_focus) else: can_focus.text = "True" def enforce_entry_text_column_id_column_for_gtkcombobox(current): entrytextcolumn = None idcolumn = None isgtkcombobox = current.get('class') == "GtkComboBox" insertpos = 0 for child in current: enforce_entry_text_column_id_column_for_gtkcombobox(child) if not isgtkcombobox: continue if child.tag == "property": insertpos = insertpos + 1 attributes = child.attrib if attributes.get("name") == "entry_text_column" or attributes.get("name") == "entry-text-column": entrytextcolumn = child if attributes.get("name") == "id_column" or attributes.get("name") == "id-column": idcolumn = child if isgtkcombobox: if entrytextcolumn is not None and entrytextcolumn.text != "0": raise Exception(sys.argv[1] + ': non-standard entry_text_column value', entrytextcolumn.text) if idcolumn is not None and idcolumn.text != "1": raise Exception(sys.argv[1] + ': non-standard id_column value', idcolumn.text) if entrytextcolumn is None: # if there is no entry_text_column, create one entrytextcolumn = etree.Element("property") attributes = entrytextcolumn.attrib attributes["name"] = "entry-text-column" entrytextcolumn.text = "0" current.insert(insertpos, entrytextcolumn) insertpos = insertpos + 1 if idcolumn is None: # if there is no id_column, create one idcolumn = etree.Element("property") attributes = idcolumn.attrib attributes["name"] = "id-column" idcolumn.text = "1" current.insert(insertpos, idcolumn) def enforce_button_always_show_image(current): image = None always_show_image = None isbutton = current.get('class') == "GtkButton" insertpos = 0 for child in current: enforce_button_always_show_image(child) if not isbutton: continue if child.tag == "property": insertpos = insertpos + 1 attributes = child.attrib if attributes.get("name") == "always_show_image" or attributes.get("name") == "always-show-image": always_show_image = child elif attributes.get("name") == "image": image = child if isbutton and image is not None: if always_show_image is None: always_show_image = etree.Element("property") attributes = always_show_image.attrib attributes["name"] = "always-show-image" always_show_image.text = "True" current.insert(insertpos, always_show_image) else: always_show_image.text = "True" def enforce_noshared_adjustments(current, adjustments): for child in current: enforce_noshared_adjustments(child, adjustments) if child.tag == "property": attributes = child.attrib if attributes.get("name") == "adjustment": if child.text in adjustments: raise Exception(sys.argv[1] + ': adjustment used more than once', child.text) adjustments.add(child.text) def enforce_no_productname_in_accessible_description(current, adjustments): for child in current: enforce_no_productname_in_accessible_description(child, adjustments) if child.tag == "property": attributes = child.attrib if attributes.get("name") == "AtkObject::accessible-description": if "%PRODUCTNAME" in child.text: raise Exception(sys.argv[1] + ': %PRODUCTNAME used in accessible-description:' , child.text) with open(sys.argv[1], encoding="utf-8") as f: header = f.readline() f.seek(0) # remove_blank_text so pretty-printed input doesn't disrupt pretty-printed # output if nodes are added or removed parser = etree.XMLParser(remove_blank_text=True) tree = etree.parse(f, parser) # make sure stays like that # and doesn't change to for status_elem in tree.xpath("//property[@name='label' and string() = '']"): status_elem.text = "" root = tree.getroot() # do some targeted conversion here # tdf#138848 Copy-and-Paste in input box should not append an ENTER character if not sys.argv[1].endswith('/multiline.ui'): # let this one alone not truncate multiline pastes add_truncate_multiline(root) replace_button_use_stock(root) replace_image_stock(root) remove_check_button_align(root) remove_check_button_relief(root) remove_check_button_image_position(root) remove_spin_button_input_purpose(root) remove_caps_lock_warning(root) remove_spin_button_max_length(root) remove_track_visited_links(root) remove_label_pad(root) remove_label_angle(root) remove_expander_label_fill(root) remove_expander_spacing(root) enforce_menubutton_indicator_consistency(root) enforce_active_in_group_consistency(root) enforce_entry_text_column_id_column_for_gtkcombobox(root) remove_entry_shadow_type(root) remove_double_buffered(root) remove_label_yalign(root) remove_skip_pager_hint(root) remove_gravity(root) remove_toolbutton_focus(root) enforce_toolbar_can_focus(root) enforce_button_always_show_image(root) enforce_noshared_adjustments(root, set()) enforce_no_productname_in_accessible_description(root, set()) with open(sys.argv[1], 'wb') as o: # without encoding='unicode' (and the matching encode("utf8")) we get &#XXXX replacements for non-ascii characters # which we don't want to see changed in the output o.write(etree.tostring(tree, pretty_print=True, method='xml', encoding='unicode', doctype=header[0:-1]).encode("utf8")) # vim: set shiftwidth=4 softtabstop=4 expandtab: