diff --git a/Repository.mk b/Repository.mk
index 0c00c1ea7443..9a765f5be26f 100644
--- a/Repository.mk
+++ b/Repository.mk
@@ -920,6 +920,7 @@ $(eval $(call gb_Helper_register_packages_for_install,sdk,\
odk_javadoc \
odk_uno_loader_classes \
) \
+ odk_scripts \
))
ifneq ($(ENABLE_WASM_STRIP_PINGUSER),TRUE)
diff --git a/odk/Module_odk.mk b/odk/Module_odk.mk
index 8b8ee8ccaac0..9cf68f5d025b 100644
--- a/odk/Module_odk.mk
+++ b/odk/Module_odk.mk
@@ -29,6 +29,7 @@ $(eval $(call gb_Module_add_targets,odk,\
Package_settings_generated \
Package_share_readme \
Package_share_readme_generated \
+ Package_scripts \
))
ifeq ($(OS),WNT)
diff --git a/odk/Package_scripts.mk b/odk/Package_scripts.mk
new file mode 100644
index 000000000000..a1de573aecf1
--- /dev/null
+++ b/odk/Package_scripts.mk
@@ -0,0 +1,16 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+#
+# 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/.
+#
+
+$(eval $(call gb_Package_Package,odk_scripts,$(SRCDIR)/odk/source/helper))
+
+$(eval $(call gb_Package_set_outdir,odk_scripts,$(INSTDIR)))
+
+$(eval $(call gb_Package_add_file,odk_scripts,$(SDKDIRNAME)/bin/addon_console.py,addon_console.py))
+
+# vim: set noet sw=4 ts=4:
diff --git a/odk/source/helper/addon_console.py b/odk/source/helper/addon_console.py
new file mode 100755
index 000000000000..a8961729a6a2
--- /dev/null
+++ b/odk/source/helper/addon_console.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python3
+
+# -*- 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/.
+#
+
+import xml.etree.ElementTree as ET
+import os
+from prompt_toolkit import prompt
+import xml.dom.minidom
+
+#-------------------------- generating the xml functions --------------------------
+
+def generate_menu_fragment(addon_name, merge_name, merge_point, merge_command, merge_fallback, merge_context, menu_items):
+ return ''.join(x for x in [
+ '',
+ f'',
+ f'',
+ f'{merge_point}' if merge_point else '',
+ f'{merge_command}' if merge_command else '',
+ f'{merge_fallback}' if merge_fallback else '',
+ f'{merge_context}' if merge_context else '',
+ '',
+ menu_items,
+ '',
+ '',
+ '',
+ ''
+ ] if x)
+
+def generate_menu_item_fragment(item_name, title, url, image_path, target, submenus=None):
+ submenu_fragment = None
+ if submenus:
+ submenu_items = ''.join(submenus)
+ submenu_fragment = f'{submenu_items}'
+
+ return ''.join(x for x in [
+ f'',
+ f'{title}' if title else '',
+ f'{url}' if url else '',
+ f'{image_path}' if image_path else '',
+ f'{target}' if target else '',
+ submenu_fragment or '',
+ ''
+ ] if x)
+
+def generate_submenu_item_fragment(submenu_name, title, url, image_path, target):
+ return ''.join(x for x in [
+ f'',
+ f'{title}' if title else '',
+ f'{url}' if url else '',
+ f'{image_path}' if image_path else '',
+ f'{target}' if target else '',
+ ''
+ ] if x)
+
+def generate_toolbar_fragment(addon_name, merge_name, merge_toolbar, merge_point, merge_command, merge_fallback, merge_context, toolbar_items):
+ return ''.join(x for x in [
+ '',
+ f'',
+ f'',
+ f'{merge_toolbar}' if merge_toolbar else '',
+ f'{merge_point}' if merge_point else '',
+ f'{merge_command}' if merge_command else '',
+ f'{merge_fallback}' if merge_fallback else '',
+ f'{merge_context}' if merge_context else '',
+ '',
+ toolbar_items,
+ '',
+ '',
+ '',
+ ''
+ ] if x)
+
+def generate_toolbar_item_fragment(item_name, title, url, image_path, target, separator_position=None):
+ return ''.join(x for x in [
+ f'',
+ f'{title}' if title else '',
+ f'{url}' if url else '',
+ f'{image_path}' if image_path else '',
+ f'{target}' if target else '',
+ f'{separator_position}' if separator_position else '',
+ ''
+ ] if x)
+
+def generate_images_fragment(image_small, image_big, image_small_hc, image_big_hc):
+ return ''.join(x for x in [
+ '',
+ f'{image_small}' if image_small else '',
+ f'{image_big}' if image_big else '',
+ f'{image_small_hc}' if image_small_hc else '',
+ f'{image_big_hc}' if image_big_hc else '',
+ ''
+ ] if x)
+
+def generate_addon_menu_fragment(addon_menu_name, title, url, image_path, target):
+ return ''.join(x for x in [
+ f'',
+ f'{title}' if title else '',
+ f'{url}' if url else '',
+ f'{image_path}' if image_path else '',
+ f'{target}' if target else '',
+ ''
+ ] if x)
+
+def generate_help_menu_fragment(help_menu_name, title, url, image_path, target):
+ return ''.join(x for x in [
+ f'',
+ f'{title}' if title else '',
+ f'{url}' if url else '',
+ f'{image_path}' if image_path else '',
+ f'{target}' if target else '',
+ ''
+ ] if x)
+
+def stitching_xcu(addon_name, merge_type, merge_name, merge_point, merge_command, merge_fallback, items, merge_toolbar, merge_context):
+ if merge_type == 'menu':
+ menu_items = ''.join(items)
+ merge_fragment = generate_menu_fragment(addon_name, merge_name, merge_point, merge_command, merge_fallback, merge_context, menu_items=menu_items)
+ elif merge_type == 'toolbar':
+ toolbar_items = ''.join(items)
+ merge_fragment = generate_toolbar_fragment(addon_name, merge_name, merge_toolbar, merge_point, merge_command, merge_fallback, merge_context, toolbar_items=toolbar_items)
+ else:
+ merge_fragment = ''
+
+ return merge_fragment
+
+def generate_xcu(merge_fragment, output_dir, addon_menu_fragment, help_menu_fragment, images_fragment):
+ # Determine the output directory
+ if not output_dir:
+ while True:
+ choice = input("\n\nSave Addons.xcu file to desktop or specify a path to custom directory? (d/c): ").lower()
+ if choice in ("d", "desktop"):
+ output_dir = os.path.join(os.path.expanduser('~'), 'Desktop')
+ break
+ elif choice == "c":
+ custom_dir = input("\nEnter custom directory path: ")
+ output_dir = validate_directory(custom_dir)
+ if output_dir:
+ break
+ else:
+ print("\nInvalid choice. Please choose 'd' or 'c'.")
+
+ final_fragment = f"""
+
+
+ {addon_menu_fragment}
+ {merge_fragment}
+ {help_menu_fragment}
+ {images_fragment}
+
+
+"""
+ file_path = os.path.join(output_dir, 'Addons.xcu')
+ with open(file_path, 'w', encoding='utf-8') as file:
+ dom = xml.dom.minidom.parseString(final_fragment)
+ pretty_xml_as_string = dom.toprettyxml()
+ # Post-process to remove unnecessary newlines
+ pretty_xml_as_string = os.linesep.join([s for s in pretty_xml_as_string.splitlines() if s.strip()])
+ file.write(pretty_xml_as_string)
+
+#-------------------------- prompt message functions --------------------------
+
+def prompt_merge_context():
+ print("\nSelect the application module context for the merge instruction:")
+ print("1. Writer")
+ print("2. Spreadsheet")
+ print("3. Presentation")
+ print("4. Draw")
+ print("5. Formula")
+ print("6. Chart")
+ print("7. Bibliography")
+ choice = input("Enter your choice (1-7): ").strip()
+ if choice == '1':
+ return "com.sun.star.text.TextDocument"
+ elif choice == '2':
+ return "com.sun.star.sheet.SpreadsheetDocument"
+ elif choice == '3':
+ return "com.sun.star.presentation.PresentationDocument"
+ elif choice == '4':
+ return "com.sun.star.drawing.DrawingDocument"
+ elif choice == '5':
+ return "com.sun.star.formula.FormulaProperties"
+ elif choice == '6':
+ return "com.sun.star.chart.ChartDocument"
+ elif choice == '7':
+ return "com.sun.star.frame.Bibliography"
+ else:
+ print("Invalid choice. Please select again.")
+ return prompt_merge_context()
+
+def prompt_images():
+ print("\nPlease provide paths to the [ .bmp ] images (type 'skip' to skip providing an image):")
+ image_small = input("Path to small image: ").strip()
+ if image_small.lower() == 'skip':
+ image_small = None
+ else:
+ image_small = validate_image_path(image_small)
+
+ image_big = input("Path to big image: ").strip()
+ if image_big.lower() == 'skip':
+ image_big = None
+ else:
+ image_big = validate_image_path(image_big)
+
+ image_small_hc = input("Path to small high-contrast image: ").strip()
+ if image_small_hc.lower() == 'skip':
+ image_small_hc = None
+ else:
+ image_small_hc = validate_image_path(image_small_hc)
+
+ image_big_hc = input("Path to big high-contrast image: ").strip()
+ if image_big_hc.lower() == 'skip':
+ image_big_hc = None
+ else:
+ image_big_hc = validate_image_path(image_big_hc)
+
+ return image_small, image_big, image_small_hc, image_big_hc
+
+def prompt_addon_name():
+ addon_name = prompt("\nEnter addon name (extID name, e.g., com...): ").strip()
+ return addon_name
+
+def prompt_menu_title(menu_type):
+ title = prompt(f"\nEnter {menu_type} item title (use '~' before the accelerator key, e.g., '~File'): ").strip()
+ return title
+
+def prompt_button_separator():
+ print("\nDo you want to add a button separator?")
+ print("1. Before the first button")
+ print("2. Between the buttons")
+ print("3. No separator")
+ choice = input("Enter your choice (1-3): ").strip()
+ if choice == '1':
+ return "before_first"
+ elif choice == '2':
+ return "between_buttons"
+ elif choice == '3':
+ return "none"
+ else:
+ print("Invalid choice. Please select again.")
+ return prompt_button_separator()
+
+def prompt_image_path(merge_type):
+ while True:
+ image_path = prompt(f"\nEnter path to {merge_type} item image (optional, enter 'skip' to skip): ").strip().lower()
+ if image_path == 'skip':
+ return None
+ image_path = validate_image_path(image_path)
+ if image_path:
+ return image_path
+ print("Invalid file type. Please try again or enter 'skip' to skip.")
+
+#-------------------------- validating provided path --------------------------
+
+def validate_directory(directory_path):
+ if not directory_path:
+ return None # No output directory provided
+ try:
+ if not os.path.isdir(directory_path):
+ raise ValueError("\nInvalid directory: Path is not a directory")
+ # Additional validations can be added here if needed
+ except ValueError as e:
+ print(f"Error: {e}")
+ return None
+ return directory_path
+
+def validate_image_path(image_path):
+ if not image_path:
+ return None # No image path provided
+ try:
+ if not os.path.exists(image_path):
+ raise ValueError("\nInvalid path: Path does not exist")
+ _, ext = os.path.splitext(image_path)
+ if ext.lower() != '.bmp':
+ raise ValueError("\nInvalid file type: Only .bmp files are allowed")
+ # Additional validations can be added here if needed
+ except ValueError as e:
+ print(f"Error: {e}")
+ return None
+ return image_path
+
+#-------------------------- MAIN --------------------------
+
+def main():
+ print("\nWelcome to the LibreOffice Addons Configuration Tool!\n")
+ merge_fragment = ''
+ addon_name = prompt_addon_name()
+ addon_menu_fragment = ''
+ help_menu_fragment = ''
+ images_fragment = ''
+
+ # Prompt for addon menu item
+ choice = prompt("\nDo you want to create the 'AddonMenu' item (Tools > Add-ons)? (y/n): ").strip().lower()
+ if choice == 'y':
+ addon_menu_name = prompt("\nEnter addon menu item name (Tools > Add-ons): ").strip()
+ addon_menu_title = prompt_menu_title("addon menu")
+ addon_menu_url = prompt("\nEnter addon menu item URL: ").strip()
+ addon_menu_target = prompt("\nEnter addon menu item target (_self, _default, _top, _parent, or _blank): ").strip()
+ addon_menu_image_path = prompt("\nEnter path to addon menu item image (optional): ").strip()
+ addon_menu_image_path = validate_image_path(addon_menu_image_path)
+ addon_menu_fragment = generate_addon_menu_fragment(addon_menu_name, addon_menu_title, addon_menu_url, addon_menu_image_path, addon_menu_target)
+
+ # Prompt for help menu item
+ choice = prompt("\nDo you want to create the 'OfficeHelp' item (Help menu)? (y/n): ").strip().lower()
+ if choice == 'y':
+ help_menu_name = prompt("\nEnter help menu item name (Help menu): ").strip()
+ help_menu_title = prompt_menu_title("help menu")
+ help_menu_url = prompt("\nEnter help menu item URL: ").strip()
+ help_menu_target = prompt("\nEnter help menu item target (_self, _default, _top, _parent, or _blank): ").strip()
+ help_menu_image_path = prompt("\nEnter path to help menu item image (optional): ").strip()
+ help_menu_image_path = validate_image_path(help_menu_image_path)
+ help_menu_fragment = generate_help_menu_fragment(help_menu_name, help_menu_title, help_menu_url, help_menu_image_path, help_menu_target)
+
+ # Prompt for images
+ choice = prompt("\nImages attribute defines the icons that appear next to the text in the menu and toolbar items.\nDo you want to provide images? (y/n): ").strip().lower()
+ if choice == 'y':
+ image_small, image_big, image_small_hc, image_big_hc = prompt_images()
+ images_fragment = generate_images_fragment(image_small, image_big, image_small_hc, image_big_hc)
+
+ while True: # Outer loop for gathering input for each merge
+ items = [] # Initialize items list for each merge type
+ merge_type = prompt("\n\nEnter merge type (menu or toolbar): ").strip()
+
+ while merge_type not in ['menu', 'toolbar']:
+ merge_type = prompt("\nInvalid input. \nEnter merge type (menu or toolbar): ").strip()
+
+ # Gather input for merge
+ if merge_type == 'menu':
+ merge_name = prompt("\nEnter merge name (m name for menu): \nFor example:\nFor a menu merge operation, you could enter something like 'm1' or 'm_print_options': ").strip()
+ else:
+ merge_name = prompt("\nEnter merge name (label[] for toolbar): \nFor a toolbar merge operation, you could enter something like [PrintButton] or [FormatToolbar]: ").strip()
+
+ merge_point = prompt("\nEnter merge point: ").strip()
+ merge_command = prompt("\nEnter merge command (possible merge commands are: AddAfter, AddBefore, Replace, Remove): ").strip()
+ merge_fallback = prompt("\nEnter merge fallback (possible merge fallbacks are: AddAfter, AddBefore, Replace, Remove): ").strip()
+ merge_toolbar = None
+ output_dir = None
+ merge_context = prompt_merge_context()
+
+ while True: # Inner loop for gathering input for items
+ item_name = prompt(f"\nEnter {merge_type} item name (or 'done' to finish): ").strip().lower()
+ if item_name.lower() == 'done':
+ break
+
+ title = prompt_menu_title(f"{merge_type} item")
+ url = prompt(f"\nEnter {merge_type} item URL (can be a macro path or dispatch command): ").strip()
+ target = prompt(f"\nEnter {merge_type} item target (_self, _default, _top, _parent, or _blank): ").strip()
+ image_path = prompt_image_path(merge_type)
+
+ # Prompt for submenus
+ submenus = []
+ add_submenu = prompt(f"\nAdd submenu for this {merge_type} item? (y/n): ").strip().lower()
+ if add_submenu == 'y':
+ while True:
+ submenu_name = prompt("\nEnter submenu name (or 'done' to finish): ").strip().lower()
+ if submenu_name.lower() == 'done':
+ break
+ submenu_title = prompt_menu_title("submenu")
+ submenu_url = prompt("\nEnter submenu URL: ").strip()
+ submenu_target = prompt("\nEnter submenu target (_self, _default, _top, _parent, or _blank): ").strip()
+ submenu_image_path = prompt("\nEnter path to submenu image (optional): ").strip()
+ submenu_image_path = validate_image_path(submenu_image_path)
+ submenus.append(generate_submenu_item_fragment(submenu_name, submenu_title, submenu_url, submenu_image_path, submenu_target))
+
+ # Generate item fragment based on merge type
+ if merge_type == 'menu':
+ items.append(generate_menu_item_fragment(item_name, title, url, image_path, target, submenus))
+ elif merge_type == 'toolbar':
+ merge_toolbar = prompt("\nEnter the Merge ToolBar name\n(specifies the appearance of the floating toolbar reached via View > Toolbars > Add-on) : ").strip()
+ separator_position = prompt_button_separator()
+ items.append(generate_toolbar_item_fragment(item_name, title, url, image_path, target, separator_position))
+
+ merge_fragment += stitching_xcu(addon_name, merge_type, merge_name, merge_point, merge_command, merge_fallback, items, merge_toolbar, merge_context)
+
+ keep_merging = prompt("\n\n\nContinue to modify Menu or Toolbar? ( y or n ): ").strip().lower()
+ if keep_merging == 'n':
+ # Generate .xcu file
+ generate_xcu(merge_fragment, output_dir, addon_menu_fragment, help_menu_fragment, images_fragment)
+ break
+ print("Thank you for using the Addons.xcu file generation. Have a Nice Day!")
+
+if __name__ == "__main__":
+ main()
+
+# vim: set shiftwidth=4 softtabstop=4 expandtab: