Skip to content

Add custom UI (ribbons) to XlsxWriter#1161

Open
zindy wants to merge 7 commits intojmcnamara:mainfrom
zindy:feature-ribbons
Open

Add custom UI (ribbons) to XlsxWriter#1161
zindy wants to merge 7 commits intojmcnamara:mainfrom
zindy:feature-ribbons

Conversation

@zindy
Copy link
Copy Markdown

@zindy zindy commented Oct 18, 2025

This PR introduces custom UI ribbons to XlsxWriter, allowing users to define and add custom ribbon elements to Excel workbooks as discussed in #1086.

Pull request details:

  • Adds the add_custom_ui() method to XlsxWriter.
  • Includes documentation with example images (dev/docs/source/_images/ribbon.png) and usage instructions (working_with_customui_elements.rst).
  • Adds a test case (xlsxwriter/test/comparison/test_macro05.py) to validate generated Excel files.

Author and Co-authors:

This feature builds on previous PRs, consolidating ribbon functionality into a single, well-tested commit.

Testing:

Tested with pytest xlsxwriter\test\comparison\test_macro05.py:

================================================= test session starts =================================================
platform win32 -- Python 3.11.13, pytest-8.4.2, pluggy-1.6.0
rootdir: C:\Users\diapath\Projects\XlsxWriter
collected 1 item

xlsxwriter\test\comparison\test_macro05.py .                                                                     [100%]

================================================== 1 passed in 0.40s ==================================================

All existing tests still pass:

================================================= test session starts =================================================
platform win32 -- Python 3.11.13, pytest-8.4.2, pluggy-1.6.0
rootdir: C:\Users\diapath\Projects\XlsxWriter
plugins: anyio-4.10.0
collected 1744 items

[...]
================================================ 1744 passed in 44.14s ================================================

@jmcnamara jmcnamara requested a review from Copilot October 18, 2025 15:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Introduces support for embedding custom UI (RibbonX) XML into generated workbooks.

  • Adds Workbook.add_custom_ui() API and wires packaging to include customUI files and relationships.
  • Adds documentation and an example-driven test validating ribbons together with a signed VBA project.
  • Includes sample customUI XML fixtures used by the new test.

Reviewed Changes

Copilot reviewed 9 out of 13 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
xlsxwriter/workbook.py Adds add_custom_ui() API and internal storage for custom UI entries.
xlsxwriter/packager.py Copies customUI XML into the package and adds relationships in root .rels.
xlsxwriter/relationships.py Allows MS package relationship schema to vary by “version”.
xlsxwriter/test/comparison/test_macro05.py New comparison test that exercises custom UI and a signed VBA project.
xlsxwriter/test/comparison/xlsx_files/customUI-01.xml Test fixture for pre-2010 RibbonX namespace.
xlsxwriter/test/comparison/xlsx_files/customUI14-01.xml Test fixture for 2010+ RibbonX namespace.
dev/docs/source/working_with_customui_elements.rst New documentation for adding custom RibbonX to workbooks.
dev/docs/source/index.rst Adds the new docs page to the index.
dev/docs/source/contents.rst Adds the new docs page to the TOC.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

if not self.in_memory:
# In file mode we just copy the xml files.
os_filename = self._filename("customUI/%s" % xml_custom_ui_name)
copy(custom_ui, os_filename)
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In file mode, the copied customUI XML is never added to self.filenames, so it won’t be written into the zip. Append the file, similar to other assets, e.g. self.filenames.append((os_filename, f'customUI/{xml_custom_ui_name}', False)).

Suggested change
copy(custom_ui, os_filename)
copy(custom_ui, os_filename)
self.filenames.append(
(os_filename, "customUI/%s" % xml_custom_ui_name, False)
)

Copilot uses AI. Check for mistakes.
Comment on lines +902 to +910
# For in-memory mode we read the custom ui into a stream.
custom_ui_file = open(custom_ui, mode="r")
custom_ui_data = custom_ui_file.read()
os_filename = StringIO(custom_ui_data)
custom_ui_file.close()

self.filenames.append(
(os_filename, "customUI/%s" % xml_custom_ui_name, True)
)
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In-memory branch appends the XML stream with is_binary=True but uses a text StringIO, which will be treated as bytes later and can cause incorrect encoding. Either set is_binary=False for XML text streams or switch to BytesIO by reading the file in binary mode.

Copilot uses AI. Check for mistakes.
Comment on lines 573 to +581
if self.workbook.custom_properties:
rels._add_document_relationship("/custom-properties", "docProps/custom.xml")

for custom_ui, version in self.workbook.custom_uis:
xml_custom_ui_name = os.path.split(custom_ui)[1]
rels._add_ms_package_relationship(
"/ui/extensibility", "customUI/%s" % xml_custom_ui_name, version=version
)
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the relationships method is updated to accept only {2006, 2010}, validate or normalize version here before passing it through (e.g., map 2007→2010 for backward compatibility or raise a descriptive warning).

Copilot uses AI. Check for mistakes.
@tsillus
Copy link
Copy Markdown

tsillus commented Mar 12, 2026

@zindy, are you still working on this topic? It seems like everything is done except for resolving the merge conflicts.

@zindy
Copy link
Copy Markdown
Author

zindy commented Mar 12, 2026

Hello @tsillus

I need to get this sorted, you're absolutely right.

Can you give me your opinion on the version keyword which copilote proposes to change from what you had originally defined (2006 = pre-excel-2014, 2007 = excel 2014) to:

version: Indicates the RibbonX schema year for relationships. Use 2006 for Office 2007 RibbonX (http://schemas.microsoft.com/office/2006/01/customui) and 2010 for Office 2010+ RibbonX (http://schemas.microsoft.com/office/2009/07/customui).

I was thinking of using the keyword pre_2010=False (by default) instead. Change it to pre_2010=True if you really need the 2006 customui. This makes it a bit easier to remember than the exact year in the schema (I think) and would work until the next schema update.

I will look at all the changes proposed over the week-end.

@tsillus
Copy link
Copy Markdown

tsillus commented Mar 13, 2026

I would not go for a boolean value here, because once you have 3 options, you would have to change the API, which would be a breaking change, which then would require some deprecation period. More work for the person maintaining the code and more work for the people using the code (because they have to make changes to their own code as well).

If you want to limit the options in an extensible way, you could think of an Enum, or have some constants defined that people can use instead of magic numbers, but I think writing documentation explaining the parameter should be enough.

@jmcnamara
Copy link
Copy Markdown
Owner

Can you give me your opinion on the version keyword which copilote proposes to change from what you had originally defined (2006 = pre-excel-2014, 2007 = excel 2014) to:

I think that the version should be parsed from the XML so that the user doesn't need to know anything about it. That was a change that I was going to make after merge but maybe it is better to do it now if you are going to change it anyway.

@zindy
Copy link
Copy Markdown
Author

zindy commented Mar 13, 2026

@jmcnamara

I think that the version should be parsed from the XML

I really like the simplicity of this!

Torsten Sillus and others added 6 commits March 25, 2026 11:51
Add custom UI (ribbons) to XlsxWriter

Polish work from previous PRs and integrate into main.

Co-authored-by: Andreas Rueckle <[email protected]>
Co-authored-by: Egor Zindy <[email protected]>
@zindy zindy force-pushed the feature-ribbons branch from 68d5d5d to a77365a Compare March 25, 2026 13:09
@zindy
Copy link
Copy Markdown
Author

zindy commented Mar 25, 2026

The PR code is now up to date with main. I implemented pulling the excel version straight from the XML file namespace as @jmcnamara suggested. I also resolved all but three of copilot's suggested changes as I don't understand what they imply.

@jmcnamara
Copy link
Copy Markdown
Owner

Thanks @zindy . I will probably merge this up over the weekend. I may change things around a bit before release and I will need to do an implementation in Rust as well.

Thanks for the effort here. You can ignore the failing lint tests. I'll fix everything up after merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants