LibreOffice Extension: How to Export the Custom Palette

Good usability means keeping software simple by supporting only those use cases that are relevant for the majority of users. But not everyone is happy with changes and misses functions for special workflows. That happened for the color palette, which was revised in the last release of LibreOffice – we reported here about color palettes. In particular the ability to save and load palettes was removed intentionally. The function has been superseded by the custom palette and the possibility to install palettes as an extension. What we can do to help the few users who need easy means to export a palette is to provide a special extension.

If you are just interested how the extension itself works, jump over to the last chapter.

Basic first

LibreOffice Basic is an easy to learn programming language. There are a plenty of good resources on the Internet, for example at the Apache OpenOffice wiki. To learn how the code works, Basic has the function to set a breakpoint (F9) where your can run the code (F5) until this position and analyze variables (aka watch F7). Be careful with the editor. It has no auto save functionality, and the workflow is not always clear.

We start with a new library per Tools > Macro > Organize dialogs… > Libraries > New (a macro could also be written with the Standard library but we want to export the code later). With edit you enter the editor where a friendly “Sub Main .. End Sub” welcomes you.

The idea is to a) read values from the custom palette, b) save it to a simple .soc files, and c) pack it into a zip file with the needed overhead for extensions.

Save data to a file

Let’s start with simple file access. Looking for the topic with your preferred search engine reveals how it works in general:

'create an object the has all methods from the class SimpleFileAccess
oSFA = createUNOService ("com.sun.star.ucb.SimpleFileAccess")
'assign a file
oOutStream = oSFA.openFileWrite(“c:\hello.txt”)
'connect an output stream to write data 
oOutText = createUNOService ("com.sun.star.io.TextOutputStream")
oOutText.setOutputStream(oOutStream)
'write data
oOutText.WriteString("”Hello World”)
'close the stream
oOutText.closeOutput()

Pretty easy, isn’t it. The extension requires xml files, for example the description.xml, which can be done by this simple method (although we need to deal with xml later). Values are typically double-quoted, and since this character is reserved in Basic we have to concatenate the string using “& Chr(34) &”.

Read values

Normal palettes such as html.soc are stored in a separate path – but not the user-defined values. To find out how these colors are handled we can save a custom color under a certain name and grep the user directory; searching with your file browser does the trick as well. It turns out that we have to process the file registrymodifications.xcu, an XML file.

<item oor:path="/org.openoffice.Office.Common/UserColors">
  <prop oor:name="CustomColor" oor:op="fuse">
    <value>7512015 958228</value>
  </prop>
</item>
<item oor:path="/org.openoffice.Office.Common/UserColors">
  <prop oor:name="CustomColorName" oor:op="fuse">
    <value>
      <it>Fooblue</it>
      <it>Bargreen</it>
    </value>
  </prop>
</item>

LibreOffice Basic has the simple API for XML (SAX) implemented, however not really simple for non-developers. But with help of the AOO Fastparser page we only need to listen for the right line and collect the values. Two functions are interesting:

Private ColorValues() As Long
Private ColorNames(0) As String
Dim aColorEntry As Integer

Sub DocHandler_startElement( cName As String, oAttributes As com.sun.star.xml.sax.XAttributeList )
  'we are looking for nodes with the keyword item
  If cName = "item" Then
    aName = oAttributes.getNameByIndex(0)
    aValue = oAttributes.getValueByIndex(0)
    if aValue = "/org.openoffice.Office.Common/UserColors" Then
      aColorEntry = 1 'the current line should be analyzed
    Else
      aColorEntry = -1
    End if
  End If
  'and nodes with the keyword prop
  If cName = "prop" And aColorEntry = 1 Then
    aName = oAttributes.getNameByIndex(0)
    aValue = oAttributes.getValueByIndex(0)
    if aValue = "CustomColor" Then
      aColorEntry = 2 'key contains a custom color value
    End If
    If aValue = "CustomColorName" Then
      aColorEntry = 3 'key contains the color names
    End If
  End If
End Sub

Sub DocHandler_characters( cChars As String )
  If Len(cChars)>1 Then 'no empty strings
    If aColorEntry = 2 Then 'it’s the custom color values, separated by spaces
      ColorValues = Split(cChars," ")
    End If
    If aColorEntry = 3 Then 'it’s a custom color name, one of the sequential items
      z = UBound(ColorNames)
      ColorNames(z) = cChars
      ReDim Preserve ColorNames(z+1)
    End If
  End If 'empty String
End Sub

So we have all together to read out the custom colors and write it to external files. Packing into a zip is the last piece.

Pack the files

That’s surprisingly easy, compared to the other tasks.

'create a package
oZipPackage = createUnoService("com.sun.star.packages.Package")
'define the package to be a standard zip since we create our own manifest.xml
Dim aArg As New com.sun.star.beans.NamedValue
aArg.Name = "PackageFormat"
aArg.Value = False 'plain Zip format
oZipPackage.initialize(Array(sFileName,aArg))
'add files to the package
addToZip(oZipPackage,<path>,<file>)

Sub addToZip(oZipPackage As Object, ByVal sPath As String, ByVal sFile As String)
  Dim aArgs(0) As Variant
  aArgs(0) = False
  'connect a stream with simple, read-only file access
  oZipPackageStream = oZipPackage.createInstanceWithArguments(aArgs())
  oSimpleFileAccess = createUNOService("com.sun.star.ucb.SimpleFileAccess")
  oInputStream = oSimpleFileAccess.OpenFileRead(sPath & sFile)
  oZipPackageStream.setInputStream(oInputStream)
  'insert the file
  oZipPackageFolder = oZipPackage.getByHierarchicalName("")
  oZipPackageFolder.insertByName(sFile,oZipPackageStream)
  'save
  oZipPackage.commitChanges()
  oInputStream.closeInput()
End Sub

User input

Finally we need to define the variables. They are the name of the exported palette, the filename of the extension, an identifier in order to allow more than one exported palette, and a version number for updates. Not to forget the publisher info. For most flexibility we ask the user in a simple dialog.

Open Tools > Macros > Organize Dialogs…, select the current library at the dialog tab and add a dialog per New. With Edit (or double clicking the node) you go into the edit mode. Controls can be placed freely on the new dialog using the dropdown at the toolbar. Some controls have properties that are set here, like the caption of label fields (“label” in the properties editor).

But we can also do that from the code. Assuming we named the library Test and the dialog MyDialog and we have a label with the name lbTest the code could be

'Load dialog
DialogLibraries.LoadLibrary("Test")
'assign it to a variable
oDlg = CreateUnoDialog(DialogLibraries.Test.MyDialog)
'insert some values
oDlg.getControl("lbTest").setText("Hello World")
'show the dialog
oDlg.Execute

The command to gather the user input would be oDlg.getControl(<name>).getText(), which of course makes sense only for input controls.

Another property of controls are events that execute a procedure on focus change, modification of values, execution, and key or mouse events – available depending on the type of control. Create a new procedure in your code, e.g.

Sub OnExecuteFile()
  'create file dialog
  oFileDialog = CreateUnoService("com.sun.star.ui.dialogs.FilePicker")
  'show dialog
  If oFileDialog.Execute() Then
    aFileName = oFileDialog.Files(0)
  End If
  oFileDialog.Dispose()
End Sub

…and assign the execute method of a button with this procedure to show a file picker when the button is pressed.

Create an extension

Once the code is fully functional you may want to share it with others. LibreOffice has the option to export the code as an (rudimental) extension via Tools > Macro > Organize dialogs… > Libraries > Export… as extension. The file is a zip packed container of the code, the dialog, and a manifest. You need to add the description.xml and some other files such as icons (side note: you need to use magenta for transparent pixels), license etc. If the extension should be assigned automatically to a menu entry and/or a toolbar button you may also want to add the file Addons.xcu, an xml file where at the menu and/or toolbar the new item is placed, for what modules it should be available, and what procedure should be executed on click.

<!-- Place it at the Tools menu under Macros -- >
<prop oor:name="MergePoint">
  <value>.uno:ToolsMenu\.uno:MacrosMenu</value>
</prop>
<!-- Available for Writer, Calc, Draw, and Impress -- >
<prop oor:name="MergeContext" oor:type="xs:string">
  <value>com.sun.star.text.TextDocument, com.sun.star.sheet.SpreadsheetDocument, com.sun.star.drawing.DrawingDocument,com.sun.star.presentation.PresentationDocument</value>
</prop>
<node oor:name="MenuItems">
  <node oor:name="M1" oor:op="replace">
  <!-- Label of the menu entry -- >
    <prop oor:name="Title" oor:type="xs:string">
      <value>Export Custom Palette...</value>
    </prop>
    <!-- Run Sub Main() in Module1 from the CustomPaletteExport macro -- >
    <prop oor:name="URL" oor:type="xs:string">
      <value>vnd.sun.star.script:CustomPaletteExport.Module1.Main?language=Basic&amp;location=application</value>
    </prop>
… (content shortened)

Extensions are shared at https://extensions.libreoffice.org/. Just follow the more or less obvious steps on this platform.

Custom Palette Export

You can download the extension at https://extensions.libreoffice.org/ and add it via extension manager (ctrl+alt+E / Tools > Extension Manager…). The Custom Palette Exporter adds a menu item to the Tools menu under Macros. On click you are presented with a dialog where you have some choices to made:

User dialog to export the custom palette

Figure 1: User dialog to export the custom palette.

  • Name: The name of the palette that will be used in the color picker and at the extension manager
  • Version: Extensions have a version number.
  • Identifier: Every extension is identified by this value and not the name. Enter a unique string here to install more than one color palette on the target system. Of course, if the extension is updated with a new version number you should keep the same name.
  • Publisher and publisher link: These information will be shown in the extension manager.
  • Export to: Select path and filename of the extension itself. Click the button right of the input field to modify the default value. Existing files are overridden.

When started without any custom color a message box pops-up that nothing is to export. Keep in mind that you have to restart LibreOffice after modifying the custom palette to actually store the changes in the registry.

Many thanks to Niklas Johansson for his contributions to the code.

Comments
  1. 3 years ago
  2. 3 years ago
  3. 3 years ago
    • 3 years ago
      • 3 years ago
        • 3 years ago
        • 1 year ago
  4. 2 years ago
    • 2 years ago