2021-06-13

62: Create Any Global UNO Service in Java

<The previous article in this series | The table of contents of this series | The next article in this series>

In order to instantiate the UNO component from a remote programming language environment or to create an artifact like a spread sheet cell function.

Topics


About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: the Java programming language

The table of contents of this article


Starting Context



Target Context


  • The reader will know how to create his or her own global UNO service in Java.

Orientation


There is an article on how to create and register any UNO Interface and generate the mapping images.

There is an article on how to build any sample program of this series.

There is an article on how to build an environment for developing UNO programs in Linux or in Windows.


Main Body

Stage Direction
Here are Special-Student-7, Douglas (a Java programmer), and Renee (a Java programmer) in front of a computer.


0: What We Mean by "UNO Service" Here


Special-Student-7
What we mean by "UNO service" here is what is explained in an article.

To state succinctly, UNO service is an item in an UNO objects factory, which is called 'UNO services manager'.

In fact, "service" is a very confusedly and confusingly used term in the official terminology, and we do not mean the 2nd meaning or the 3rd meaning in the official terminology by "UNO service".

If you are wondering how you can create a "service" in the 2nd meaning in the official terminology, I am pretty sure that it is not something you need to create.

Some people may deem "service" in the 3rd meaning in the official terminology handy, but also it is not something you particularly need to create, and as I choose not to create any, I do not explain about it.


1: Why Is a UNO Service Desired?


Special-Student-7
The purpose of creating a UNO service is to instantiate the UNO component from a remote programming language environment or to create an artifact that requires the definition of a UNO service, like a spread sheet cell function.

Douglas
"the UNO component"? What UNO component?

Special-Student-7
Sir, as any UNO service is a UNO component registered into a UNO services manager, you should already have the UNO component in your mind when you have the intention of creating the UNO service.

Douglas
Should I? I just thought "Maybe, I should have a UNO service, for a change."

Special-Student-7
. . . You have to have a UNO component.

Douglas
Would you give me one?

Special-Student-7
Just an arbitrary "one" will not do; it has to be one that satisfies your specific demands.

Douglas
You are unkind.

Special-Student-7
It is not about kindness; it is meaningless.

Renee
What do you mean by "remote programming language environment"?

Special-Student-7
Madam, any remote programming language environment is any programming language environment that is not the programming language environment in which the UNO component is to be instantiated.

Douglas
Is that a Zen koan?

Special-Student-7
Usually, the UNO component is instantiated in the LibreOffice or Apache OpenOffice JVM; for example, a Python macro is the remote programming language environment.

Douglas
You should have said just 'from another programming language'.

Special-Student-7
No, I should not have: the remote programming language environment may be a Java UNO client.

Douglas
. . . Why does it have to be "remote"?

Special-Student-7
It does not particularly "have to" be remote, but if it is local, you will not particularly need the UNO service, because you will be able to instantiate the UNO component just with the 'new' operator.

Douglas
So, do I have to always create a UNO service when a UNO component is remote?

Special-Student-7
Not always: if the UNO component is instantiated by a method of another normal UNO object, the UNO service will not be required.

Douglas
What do you mean by "normal UNO object"? I mean, what is an 'abnormal' UNO object?

Special-Student-7
I mean any UNO object that is not any UNO services manager, which is not particularly "abnormal" though.

Douglas
Then, if I let my UNO components be instantiated from normal UNO objects, I do not need to create any UNO service, ever?

Special-Student-7
But where did those normal UNO objects come from? Usually, the 1st of your UNO objects is instantiated from a UNO services manager, and then, the 1st UNO object could instantiate some of your UNO components, and so on.

Renee
What is that "artifact that requires the definition of a UNO service" about?

Special-Student-7
Such an artifact is instantiated by the LibreOffice or Apache OpenOffice main body as a UNO service, so you need to create the UNO service.


2: A Note: We Are Creating a Global UNO Service


Special-Student-7
Although not every UNO service is a global UNO service, when I talk about creating any UNO service, I am talking about creating a global UNO service.

Douglas
Why? I want a non-global UNO service.

Special-Student-7
Really, sir?

Douglas
At least, I can insist so.

Special-Student-7
For what purpose, do you want such a non-global UNO service, if I may ask?

Douglas
. . . You may not ask.

Special-Student-7
I see, well, as each non-global UNO services manager is of a specific purpose and a specific implementation, usually, registering your UNO service there is not useful at all unless the UNO services manager has such a customization in its mind.

Douglas
If it has a customization in its mind?

Special-Student-7
Then, it should have somehow disclosed the instruction.

Douglas
Where is the instruction?

Special-Student-7
Nowhere, I guess, because there is no such a UNO services manager, I guess.


3: Create the UNO Service



3-1: Prepare the UNO Component


Special-Student-7
The UNO component has to satisfy a certain prerequisite for it to be registered as a UNO service.

Namely, it has to implement 2 UNO interfaces, 'com.sun.star.lang.XServiceInfo' and 'com.sun.star.lang.XInitialization', like this.

@Java Source Code
// # Change the package name
package theBiasPlanet.tests.models;

import java.util.Set;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.Map;
import com.sun.star.lib.uno.helper.WeakBase;
import com.sun.star.lang.XServiceInfo;
import com.sun.star.lang.XInitialization;
import com.sun.star.uno.XComponentContext;
// # Add the implemented UNO interface
import theBiasPlanet.unoDatumTypes.tests.XTest1;
// # Add the other necessary classes and interfaces Start
import com.sun.star.lang.XServiceInfo;
import com.sun.star.lib.uno.helper.WeakBase;
// # Add the other necessary classes and interfaces End

// # Change the class name and the implemented UNO interface name
public class JavaTest1UnoComponent extends WeakBase implements XServiceInfo, XInitialization, XTest1 {
	private static final Class c_thisClass = new Object () { }.getClass ().getEnclosingClass ();
	private static final Set <String> c_unoServiceNames = new HashSet <String> ();
	private static final Set <String> c_unoCompoundInterfaceNames = new HashSet <String> ();
	private XComponentContext i_underlyingRemoteUnoObjectsContextInXComponentContext;
	// # Add the other member variables Start
	private String i_message = null;
	// # Add the other member variables End
	
	static {
		// # Add UNO service names Start
		c_unoServiceNames.add ("theBiasPlanet.tests.JavaTest1UnoComponent");
		// # Add UNO service names End
		// # Add UNO compound interface names Start
		// # Add UNO compound interface names End
	}
	
	// # Change the class name
	public JavaTest1UnoComponent (XComponentContext a_underlyingUnoObjectsContextInXComponentContext) throws IllegalArgumentException {
		try {
			i_underlyingRemoteUnoObjectsContextInXComponentContext = a_underlyingUnoObjectsContextInXComponentContext;
		}
		catch (Exception l_exception) {
		}
	}
	
	public static void setThisClassToGlobalUnoServicesProvider (Map <String, Map <Class <?>, Set <String>>> a_implementationClassNameToImplementationClassToUnoServiceNamesMapMap) {
		LinkedHashMap <Class <?>, Set <String>> l_implementationClassToUnoServiceNamesMap = new LinkedHashMap <Class <?>, Set <String>> ();
		l_implementationClassToUnoServiceNamesMap.put (c_thisClass, c_unoServiceNames);
		
		a_implementationClassNameToImplementationClassToUnoServiceNamesMapMap.put (c_thisClass.getName (), l_implementationClassToUnoServiceNamesMap);
	}
	
@Override
	public final void initialize (java.lang.Object [] a_arguments) throws com.sun.star.uno.Exception {
		// # Write the initialization Start
		if (a_arguments != null && a_arguments.length == 1) {
			if (a_arguments [0] instanceof String) {
				i_message = (String) a_arguments [0];
				if (i_message == null) {
					throw new IllegalArgumentException ("The first argument can't be null.");
				}
			}
			else {
				throw new IllegalArgumentException ("The first argument must be a String instance.");
			}
		}
		else {
			throw new IllegalArgumentException ("The number of arguments must be 1.");
		}
		// # Write the initialization End
	}
	
	@Override
	public String getImplementationName () {
		return c_thisClass.getName ();
	}
	
	@Override
	public final boolean supportsService (String a_unoCompoundInterfaceName) {
		return c_unoCompoundInterfaceNames.contains (a_unoCompoundInterfaceName);
	}
	
	@Override
	public final String [] getSupportedServiceNames () {
		int l_numberOfItems = c_unoCompoundInterfaceNames.size ();
		String [] l_unoCompoundInterfaceNamesArray = new String [l_numberOfItems];
		int l_itemIndex = 0;
		for (String l_unoCompoundInterfaceName: c_unoCompoundInterfaceNames) {
			l_unoCompoundInterfaceNamesArray [l_itemIndex] = l_unoCompoundInterfaceName;
			l_itemIndex ++;
		}
		
		return l_unoCompoundInterfaceNamesArray;
	}
	
	// # Add the methods of the implemented UNO interface Start
	@Override
	public String test1 (String a_name) throws IllegalArgumentException {
		if (a_name == null) {
			throw new IllegalArgumentException ("The first argument can't be null.");
		}
		return String.format ("%s, %s!", i_message, a_name);
	}
	// # Add the methods of the implemented UNO interface End
	
	// # Add other methods Start
	// # Add other methods End
}

Douglas
. . . Um? Hmm . . .

Special-Student-7
Of course, you do not need to implement it exactly like that: you need to implement the constructor and those "@Override"-annotated methods somehow appropriately.

Douglas
I don't understand what is "somehow appropriate".

Special-Student-7
You need to create the constructor in that signature; any constructor of any different signature will not be called.

The initialization arguments are passed into the "initialize" method, not into the constructor.

"service" for "supportsService" and "getSupportedServiceNames" are basically not UNO service, but "service" in the 2nd meaning in the official terminology.

Renee
Well, so, the UNO component needs to support what "services"?

Special-Student-7
If the UNO component is of a certain kind of artifacts, the kind requires the UNO component to support certain "services"; otherwise, the UNO component does not need to support any "service".

Renee
You said "basically" . . .

Special-Student-7
As "service" is so jumbled up as a concept, sometimes, something that is basically a "service" in the 2nd meaning in the official document is opportunistically abused also in the meaning of UNO service and vice versa. I have not noticed any problem in not supporting the UNO service name in those 2 methods, but I said "basically" just in case. In fact, there will be no practical problem in supporting the UNO service name there, which you may choose.

Renee
Ah.

Special-Student-7
Managing the UNO service information in like "c_unoServiceNames" is my contrivance to not scatter such information around methods, which you do not particularly follow; and the "setThisClassToGlobalUnoServicesProvider" method is for injecting the information into the global UNO services provider, which will be created next, in order to avoid doubly writing the information in the global UNO services provider, so you do not particularly need to have the method if you want to doubly write.


3-2: Create a Global UNO Services Provider


Special-Student-7
We need to create a global UNO services provider.

Renee
What is that?

Special-Student-7
That is a class that has 2 required static methods, '__writeRegistryServiceInfo' and '__getComponentFactory', which are necessary in order for UNO services to be registered and instantiated.

Douglas
Can't I just put those 2 methods into the UNO component?

Special-Student-7
Ah, a good question. You could, but as there may be multiple UNO components, which UNO component should have the 2 methods? Of course, you can choose an arbitrary one, but the structure will become unreasonably asymmetric.

Douglas
But I have only one UNO component.

Special-Student-7
The point is that those 2 methods do not belong to your one UNO component information-structure-wise. So, the structure would be skewed anyway, even if there happens to be only one UNO component.

Douglas
I don't mind.

Special-Student-7
Of course, the choice is yours, but I really disapprove skewing things for fleeting labor saving.

Anyway, this is a global UNO services provider, followed by my utility class.

@Java Source Code
// # Change the package name
package theBiasPlanet.tests.globalUnoServicesProvider;

import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import com.sun.star.lang.XSingleComponentFactory;
import com.sun.star.registry.XRegistryKey;
import theBiasPlanet.unoUtilities.servicesHandling.GlobalUnoServicesProviderUtility;
import theBiasPlanet.tests.models.JavaTest1UnoComponent;

// # Change the class name
public class TestsGlobalUnoServicesProvider {
	private static final Map <String, Map <Class <?>, Set <String>>> c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap = new HashMap <String, Map <Class <?>, Set <String>>> ();
	static {
		// # Add implementation classes Start
		JavaTest1UnoComponent.setThisClassToGlobalUnoServicesProvider (c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap);
		// # Add implementation classes End
	}
	
	public static XSingleComponentFactory __getComponentFactory (String a_implementationName) {
		return GlobalUnoServicesProviderUtility.getUnoServiceInstancesFactory (c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap, a_implementationName);
	}
	
	public static boolean __writeRegistryServiceInfo (XRegistryKey a_registryKey) {
		return GlobalUnoServicesProviderUtility.writeGlobalUnoServicesInformationToRegistry (c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap, a_registryKey);
	}
}


@Java Source Code
package theBiasPlanet.unoUtilities.servicesHandling;

import java.util.Map;
import java.util.Set;
import com.sun.star.lang.XSingleComponentFactory;
import com.sun.star.registry.XRegistryKey;
import com.sun.star.lib.uno.helper.Factory;
import theBiasPlanet.coreUtilities.constantsGroups.*;
import theBiasPlanet.coreUtilities.collectionsHandling.MapHandler;
import theBiasPlanet.coreUtilities.collectionsHandling.ArraysFactory;

public class GlobalUnoServicesProviderUtility {
	public static XSingleComponentFactory getUnoServiceInstancesFactory (Map <String, Map <Class <?>, Set <String>>> a_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMap, String a_unoComponentClassName) {
		XSingleComponentFactory l_globalUnoServiceInstancesFactory = null;
		Map <Class <?>, Set <String>> l_unoComponentClassToUnoServiceNamesMap = a_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMap.get (a_unoComponentClassName);
		if (l_unoComponentClassToUnoServiceNamesMap != null) {
			@SuppressWarnings (WarningNamesConstantsGroup.c_notChecked)
			Map.Entry <Class <?>, Set <String>> l_unoComponentClassToUnoServiceNamesMapEntry = MapHandler. <Class <?>, Set <String>>getFirstEntry (l_unoComponentClassToUnoServiceNamesMap);
			Set <String> l_unoServiceNames =  l_unoComponentClassToUnoServiceNamesMapEntry.getValue ();
			l_globalUnoServiceInstancesFactory = Factory.createComponentFactory (l_unoComponentClassToUnoServiceNamesMapEntry.getKey (), ArraysFactory. <String>createArray (String.class, l_unoServiceNames));
		}
		return l_globalUnoServiceInstancesFactory;
	}
	
	public static boolean writeGlobalUnoServicesInformationToRegistry (Map <String, Map <Class <?>, Set <String>>> a_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMap, XRegistryKey a_registryKeyInXRegistryKey) {
		boolean l_returnStatus = false;
		for (Map.Entry <String,  Map <Class <?>, Set <String>>> l_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMapEntry: a_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMap.entrySet ()) {
			@SuppressWarnings (WarningNamesConstantsGroup.c_notChecked)
			Map.Entry <Class <?>, Set <String>> l_unoComponentClassToUnoServiceNamesMapEntry = MapHandler. <Class <?>, Set <String>>getFirstEntry (l_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMapEntry.getValue ());
			Set <String> l_unoServiceNames = l_unoComponentClassToUnoServiceNamesMapEntry.getValue ();
			l_returnStatus = Factory.writeRegistryServiceInfo (l_unoComponentClassNameToUnoComponentClassToUnoServiceNamesMapMapEntry.getKey (), ArraysFactory. <String>createArray (String.class, l_unoServiceNames), a_registryKeyInXRegistryKey);
			if (!l_returnStatus) {
				break;
			}
		}
		return l_returnStatus;
	}
}

I have prepared that utility class, "GlobalUnoServicesProviderUtility", because it would be uneconomical to write that code into each global UNO services provider, but if you will, of course, you can be uneconomical.

Douglas
Having a utility class is good, but can't you do it better? I mean, that way, each global UNO services provider has to define the 2 methods in the exactly same code, which seems foolish.

Special-Student-7
I understand the feeling, but the hurdle is that the methods are static methods, which do not work well with inheritance.

Douglas
What do you mean by "not work well"?

Special-Student-7
Although the code of the methods is exactly the same for each global UNO services provider, the information handled there is different, which cannot be realized by letting the utility class have the methods which are inherited by each global UNO services provider.

Douglas
Huh? What happen if I let the utility class have the methods?

Special-Student-7
Even if the methods are invoked via the global UNO services provider class, they look up the variables in the utility class, not the variables in the global UNO services provider that "shadow" the variables in the utility class.

Douglas
Really?

Special-Student-7
Really; that is how static methods work.

Douglas
Well . . .

Special-Student-7
As I said in the previous sub section, the information of the UNO service is injected into the global UNO services provider by the "setThisClassToGlobalUnoServicesProvider" method.


3-3: Create the Jar Manifest


Special-Student-7
We will archive the necessary Java class files into a Jar file, which requires the Jar manifest.

Renee
A Jar file doesn't particularly require any Jar manifest, right?

Special-Student-7
Any global-UNO-services-provider-contained Jar file requires it.

Renee
Hmm.

Special-Student-7
This is the Jar manifest.

@MANIFEST.MF Source Code
Comment01: # Change the class name
RegistrationClassName: theBiasPlanet.tests.globalUnoServicesProvider.TestsGlobalUnoServicesProvider
UNO-Type-Path: <>


Renee
I see. That is how the global UNO services provider is recognized by LibreOffice.


3-4: Create the Extension Configuration Files


Special-Student-7
We are going to register the UNO service via a LibreOffice or Apache OpenOffice extension, and we need some configuration files for that.

Douglas
"extension"? Do I need to create an "extension", whatever that is?

Special-Student-7
Sir, it is as easy as creating some configuration files and archiving the ingredients in a ZIP file.

Douglas
Oh?

Special-Student-7
We are creating that "UnoServiceComponents.xml" file, like this.

@XML Source Code
<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://openoffice.org/2010/uno-components">
	<!-- # Change the jar file uri -->
	<component loader="com.sun.star.loader.Java2" uri="theBiasPlanet.tests.unoExtension.jar">
		<!-- # Add UNO component class names -->
		<implementation name="theBiasPlanet.tests.models.JavaTest1UnoComponent">
			<!-- # Add UNO service names Start -->
			<service name="theBiasPlanet.tests.JavaTest1UnoComponent"/>
			<!-- # Add UNO service names End -->
			<!-- # Add UNO compound interface names Start -->
			<!-- <service name="???"/> -->
			<!-- # Add UNO compound interface names End -->
		</implementation>
	</component>
</components>

Renee
The tag name is "component", but it isn't about UNO component, right?

Special-Student-7
Yes, another case of "component"'s being abused in the lax official terminology: "component" could mean anything, now a Jar file.

Note that the UNO service name should be specified there as well as "service" in the 2nd meaning names should.

Douglas
Confusing. Those 2 methods do not need to support the UNO service name, but now, the configuration file needs the UNO service name . . .

Special-Student-7
It is really confusing: "service" is really jumbled up in the code, not just in the documentation.

Anyway, we need to create also "manifest.xml" of course, but I do not show it here, because it is exactly as in the article.


3-5: An Aside: What Are Not Necessary


Renee
You seem to have forgotten creating the service IDL file.

Special-Student-7
Actually, we do not need to create any.

Renee
Huh? We need it, right?

Special-Student-7
No, any service IDL file is for creating a "service" in the 2nd meaning or "service" in the 3rd meaning in the lax official terminology, not for creating a UNO service.

Douglas
But isn't is safer to create one, just in case?

Special-Student-7
No, it is not particularly safer.


3-6: Create the Extension File


Special-Student-7
Now, we are going to create the extension file.

I do not need to explain how, because it has been already detailed in a previous article.


3-7: Register the Extension


Special-Student-7
We are going to register the extension.

It can be done via the GUI menu item "Tools" -> "Extension Manager...".

Or you can use the 'unopkg' command, if your build script wants to register the extension, like this.

@bash or cmd Source Code
unopkg add -f -v %the extension file path%

Note that LibreOffice or Apache OpenOffice instance is supposed to have been shut down before the command.


3-8: Setting the Additional Classes Paths into the Office Product


Special-Student-7
Supposing that your UNO service uses some libraries made by you or otherwise, those libraries can be put into the UNO service Jar file, but there is another option: those libraries can be located somewhere probably in the local file system and the paths can be set into the office product.

Renee
Usually, one will not want to put a common library into all the extensions; if the common library is modified, the Jar files of all the extensions would have to be re-created and re-registered.

Special-Student-7
That is one's choice, but knowing an option is a good thing either way.

Douglas
Can't I create a library extension that is for registering the library into Office?

Special-Student-7
Ah, a good point, but it will not work, because each extension is loaded via its own class loader.

Douglas
Huh? What does that mean?

Special-Student-7
The classes in such a library extension would not be loaded for your UNO service that uses the library.

Douglas
Is that so . . .

Special-Student-7
But you can locate those libraries somewhere probably in the local file system and set the paths into the office product by configuring the '%the office product directory%/program/fundamentalrc' or .'%the office product directory%\program\fundamental.ini' file, like this, where 'theBiasPlanet.unoAdditionalDatumTypes.jar' and 'theBiasPlanet.unoUtilities.jar' are the libraries being set.

@XML Source Code
~
URE_MORE_JAVA_TYPES=${BRAND_BASE_DIR}/program/classes/unoil.jar ${BRAND_BASE_DIR}/program/classes/ScriptFramework.jar ${${$ORIGIN/lounorc:PKG_UserUnoFile}:UNO_JAVA_CLASSPATH} ${${$ORIGIN/lounorc:PKG_SharedUnoFile}:UNO_JAVA_CLASSPATH} ${${$ORIGIN/lounorc:PKG_BundledUnoFile}:UNO_JAVA_CLASSPATH} ${BRAND_BASE_DIR}/program/classes/theBiasPlanet.unoAdditionalDatumTypes.jar ${BRAND_BASE_DIR}/program/classes/theBiasPlanet.unoUtilities.jar
~


4: Instantiate the UNO Service


Special-Student-7
The UNO service can be instantiated, as any built-in global UNO service can.

Douglas
. . . How, exactly?

Special-Student-7
For example, this is a Python macro.

@Python Source Code
from typing import Any
from typing import cast
import uno
~
from com.sun.star.script.provider import XScriptContext
from com.sun.star.uno import XComponentContext
XTest1: Any = uno.getClass ("theBiasPlanet.unoDatumTypes.tests.XTest1")

XSCRIPTCONTEXT: XScriptContext
c_underlyingRemoteUnoObjectsContextInXComponentContext: XComponentContext = XSCRIPTCONTEXT.getComponentContext ()
c_javaTest1UnoComponent: "XTest1" = cast (XTest1, c_underlyingRemoteUnoObjectsContextInXComponentContext.getServiceManager ().createInstanceWithArgumentsAndContext ("theBiasPlanet.tests.JavaTest1UnoComponent", ["Hi"], c_underlyingRemoteUnoObjectsContextInXComponentContext))
~

def test1 () ->None:
	~
	c_javaTest1UnoComponent.test1 ("bro")
	~


Renee
So, the Java class can be instantiated from a remote programming language environment.


5: Here Is a Workable Sample


Special-Student-7
Here is a workable sample extension.

How to build the sample project is explained in an article.

The main project is 'hiUnoExtensionsUnoExtension', which uses the additional UNO datum types project and some utility projects.

The UNO datum types merged file has to be registered into the office product according to the way 1) stated in a previous article.

And the paths of the Jar files ('theBiasPlanet.coreUtilities.jar', 'theBiasPlanet.unoAdditionalDatumTypes.jar', and 'theBiasPlanet.unoUtilities.jar') have to be set into the office product according to the way explained in a previous section.

Douglas
Can't we do without such preparations?

Special-Student-7
We can, if the UNO datum types merged file is registered into the office product according to the way 2) stated in a previous article and the depended Jar files are put into the UNO service Jar file as stated in the previous section.

Douglas
Then, why aren't we doing without them?

Special-Student-7
Because I prefer not putting the same artifacts into multiple UNO extensions.

Douglas
. . .

Special-Student-7
It is a sample, which you can tweak as you like, as the instruction for tweaking has been disclosed.

Douglas
I see . . .

Special-Student-7
Anyway, the extension should have registered a tool buttons group, from which the testing macro can be called.


References


<The previous article in this series | The table of contents of this series | The next article in this series>