How to programmatically get/set a form control's events

Howdy, was wondering how I could get / set the macro assigned to a particular form control event, say, mouse press.

Am trying to do this without the gui, thus avoiding going into design mode, right-clicking on the control, clicking “Control Properties” then “Events”, then scrolling through the event entry to see what it’s running.

Using XRay I’ve looked at the object reference for Buttons (com.sun.star.form.OButtonControl) and Button Models (com.sun.star.form.OButtonModel) but don’t seem to see anything relevant there.

Did find something in the Document object (ScModelObj has a getEvents method as well as an Events property property object containing getByName and replaceByName methods), so was thinking there would be something similar for form controls.

As to why, sheer laziness. When developing I have a “reminder” routine which displays a message box showing the index, type, name and label of all controls in a form. Wanted to add the routine they ran as well, and also got to thinking it would nice to be able to change it without having to tediously navigate the GUI as shown above.

Any pointers would be most appreciated. Thanks!

You need used getScriptEvents for get, and registerScriptEvent for set in form control, look: LibreOffice: XEventAttacherManager Interface Reference

With Python it’s possible simplify this.

2 Likes

OK, thanks to Mauricio for pointing me in the right direction to get this figured out.

The confusing part was that the events assigned to a form control object are not (as one would presume) contained within the control itself, rather its parent, the form object.

Perhaps in the future the control object will have additional properties and methods for viewing, getting and setting events associated with it.

While Python is a very popular way to run macros in Libre Office, this discussion uses Basic. If Python’s your bag, it shouldn’t be that hard to refactor.

Assuming you have a form object “oForm” with 5 controls, and a push button control whose index in the list of form controls is “3” to which you wish to add or update its “mousePressed” event:

  • oControl = oForm.getByIndex(3) gives you the button object.

  • oControl.Name is the name of the button (useful for determining which control to work with)

  • aControlEvents = oForm.getScriptEvents(3) will give you an array of all the events associated with the button, one element of which may or may not have an .EventMethod property set to “mousePressed”

So, like Mauricio said, getScriptEvents is how we can get a control’s events. But what about setting?

It can vary depending on which type of control you’re working with, as they support different events.

Here we’ll again assume a push button, and use a couple of iterators. “i” is the index to the form’s control, and “j” is the index to the array of the control’s event(s).

  • oControl = oForm.getByIndex(i) ’ The control
  • aControlEvents = oForm.getScriptEvents(i) ’ The array of events associated with the control

An array entry in aControlEvents is a ScriptEventDescriptor type, containing five string entries:

  • .ListenerType
  • .EventMethod
  • .AddListenerParam
  • .ScriptType
  • .ScriptCode

Create the descriptor: descriptor = New com.sun.star.script.ScriptEventDescriptor

To add an event, you call oForm.registerScriptEvent(i, descriptor)

So far as how to populate the descriptor fields:

For .EventMethod and .ListenerType, a pushbutton has 15 possible events. In design mode, examine a pushbutton’s control properties, and look at the events tab. Match the label of the event you wish to affect to the .EventMethod shown below set and populate the two fields accordingly:

  • .EventMethod → .ListenerType
  • mousePressed → XMouseListener
  • resetted → XResetListener
  • approveReset → XResetListener
  • approveAction → XApproveActionListener
  • mouseMoved → XMouseMotionListener
  • mouseDragged → XMouseMotionListener
  • mouseEntered → XMouseListener
  • mouseReleased → XMouseListener
  • keyReleased → XKeyListener
  • focusLost → XFocusListener
  • keyPressed → XKeyListener
  • mouseExited → XMouseListener
  • itemStateChanged → XItemListener
  • focusGained → XFocusListener
  • actionPerformed → XActionListener

(Alternatively, examine xmloff/source/forms/formevents.cxx in the Libre source to see the available methods and listeners.)

Set .ScriptCode to the subroutine or method in your code you want to run, i.e.,
‘vnd.sun.star.script:Standard.Module1.YOUR_ROUTINE_NAME_HERE?language=Basic&location=document’

(If the routine is in a different module, or you’ve renamed “Module1”, adjust as needed.)

.Script is just the string “Script”, and .AddListenerParam is usually an empty string (unless you need to pass parameters to the routine, beyond the scope of this discussion).

Unfortunately, the form object references event routines by index, not name, and for some reason index is not a property for a form control, so you can’t use oForm.getByName(“Your Control Name”) and expect to be able to work with the events for that control.

Instead, you have to iterate all the controls with oForm.getByIndex and then look for the one whose Name property matches the one you’re looking for, giving you the desired index, or value for “i”.

You also have to make sure the event method isn’t already declared, so once you have “i”, you use oForm.getScriptEvents(i) to get an array of all events defined for that control, with each array element being a descriptor as described above.

If the upper bounds of the array returned by oForm.getScriptEvents(i) is less than zero, there are currently no events associated with the control, so you can simply use oForm.registerScriptEvent(i, descriptor).

If there are events, though, you need to iterate through them (using “j” from above), looking for the one whose .EventMethod property matches whatever action you’re trying to affect (mousePressed, keyPressed, etc.)

If you do find a match, you can’t just update the .ScriptCode property. You need to remove, then re-add the event.

Removal is a little silly. You’d think you could just reference i and j, but no, the removal method needs a lot more: form.revokeScriptEvent(i, events(j).ListenerType, events(j).EventMethod, events(j).AddListenerParam)

Not sure why it needs all that. Seems “i” and “j” should have been enough, unless, again, different controls act differently, but I can’t quite see how you could have multiple routines assigned to the same .EventMethod

Once it’s gone, just add it back in with oForm.registerScriptEvent(i, descriptor), then exit the iterator as there’s no need to examine the other events.

After leaving the events iterator, you should still check to make sure you actually updated one. Inside your “j” iterator, use a boolean that gets set to true if you found an event to update and done the revoke / register. After the iterator, if the boolean is still false, that means there was no event associated to whatever .EventMethod you were trying to assign, so you simply do oForm.registerScriptEvent(i, descriptor)

Whew! Now you have programmatically assigned a routine to a control based on its event method.

It would be super nice if this was a little more streamlined, first by giving the control itself access to its events rather than having to go through the form object. The register method is pretty straightforward, but revoking seems to require unneccesary parameters. You should also be able to update an event, rather than having to destroy and then re-add it.

Lastly, the descriptor seems to have redundant information as well. One would think you could get away with just having .EventMethod rather than also needing to set .ListenerType. Since the combinations are all unique, the register method should be able to figure out which listener type to use based on the event method. (Perhaps with other controls this is not the case. Again, we’re only talking about pushbuttons here.)

So, adding some methods to a form control object, such as:

  • oControl.getEvent([in] string descriptor.EventMethod)
  • oControl.getEventMethods
  • oControl.getEvents
  • oControl.SetEvent([in] struct descriptor)
  • oControl.SetEvents([in] array[struct] aDescriptorArray)
  • oControl.RemoveEvent([in] struct descriptor)
  • oControl.RemoveEvents

would IMHO make working with them a lot easier. SetEvent and SetEvents would do double-duty, first ensuring there was not already an event matching the descriptor’s .EventMethod, and either update or insert as needed. All the info it would need would be in the descriptor.

Thanks again for the clue, Mauricio!

1 Like

Hello,

First thank you for your research effort and post.

It appears, based upon the original question, you may be better off simply creating your own listeners (ie: events) with code rather than using setting/modifying in the properties. See:

CreateUnoListener Function [Runtime]

How to properly code a broadcaster and listener in LO Basic

and Pitonyak’s book Open Office Macros Explained → OOME PDF

1 Like

Yuck, thought that’s what I was doing, but belatedly realized just running oForm.registerScriptEvent does nothing.

The part in Interface XEventAttacherManager that was throwing me off was the section in the method’s description, saying “If any object is attached under this index, then this event is attached automatically.”

I took that to mean the result of oForm.getByIndex was the object so it would just take care of it, but looks like that’s not the case.

Sure, the item shows up in getScriptEvents, but it’s not attached to anything so doesn’t fire when you click the button. Am trying to abstract this so it can work with any control and event, but starting with push buttons. Looks like there’s a little more work to be done. Thanks for the comment!

1 Like

Argh, I am an idiot. In the event descriptor I had .ListenerType set to XActionListener instead of xMouseListener.

So, while oForm.registerScriptEvent did indeed produce an array entry that one could see with oForm.getScriptEvents, the listener type being incorrect prevented clicking the button from doing anything.

It’s working now.

Thanks for all the comments. Take care!

1 Like

As a follow-up, it’s easy to get and set a sheet or document’s events, too, though the procedure is just a hair different.

You need:

A sheet (or document) object, oObj

A string, strEvent, representing the name of the event (“onFocus”, “onDoubleClick”, etc.)

A two-element variant array of type PropertyValue (com.sun.star.beans.PropertyValue), arrElements

Sheet and document objects have a getEvents method, in which there is a getByName method.

See oObj.getEvents.getElementNames for the element names.

See oObj.getEvents.getByName(strEvent) for any events assigned. It will either be empty, or have the two-element array.

To update it, use oObj.getEvents.replaceByName(strEvent, arrTabEvents)

Make element 1’s “.Value” string empty to remove an event, or re-assign it to any other routine you want to use.

1 Like

Format for the arrElements array:

With arrElements(0)

.Name = "EventType"

.Handle = -1

.Value = "Script"

.State = 0

End With

With arrElements(1)

.Name = "Script"

.Handle = -1

.Value = "vnd.sun.star.script:Standard.Module1.<YOUR_ROUTINE_NAME_HERE>?language=Basic&location=document"

.State = 0

End With

1 Like

Couple of other posts (there are more):

https://forum.openoffice.org/en/forum/viewtopic.php?t=27424#p125518

Thanks, Ratslinger, I used getElementNames from the sheet / document’s getElements method to acquire the event names.

Turns out that with a document object, the two arrTabEvent elements are reversed, so you’d put your routine in element zero, not one. Other than that, the procedure seems identical.

Except for knowing how to do all this, and the one post (first link I posted) using it when adding a new control via code, haven’t really figured out any use for the procedures.

Can’t say there isn’t any, just haven’t come up a use for any of this. All seems simpler with just another line or two of code.

For Python developers, I’m develop a library for LibreOffice that help us to simplify the API UNO. I added support for the question in this thread. Now, you can.

IMPORTANT: Not try this code only copy/paste, you need read the wiki first: https://gitlab.com/mauriciobaeza/zaz/wikis/easymacro.py

import uno
import easymacro as app

def main(event=None):

    doc = app.get_document()
    sheet = doc.active
    form = sheet.forms[0]

    button = form['cmd_test']
    # ~ Get events
    events = button.events
    for event in events:
        app.debug(event)

    # ~ Set events
    # ~ Automatic call CONTROLNAME_EVENTNAME: cmd_test_action
    macro = {'library': 'mymacros'}
    button.remove_event()
    button.add_event('action', macro)

    # ~ if you want save changes
    doc.save()
    return


def cmd_test_action(event):
    app.msgbox('Ok Action')
    return

You can see how: https://gitlab.com/mauriciobaeza/zaz/blob/master/source/easymacro.py#L777

@mauricio,

Decided to try your library. Have stayed away from it for some time since it seems to lack documentation. Had multiple problems just getting the “Test” from here https://gitlab.com/mauriciobaeza/zaz/wikis/easymacro.py to work. For example line 4 of Test script is missing :. There were other problems.

After all that, set a Calc file up with the button for script in your answer. Had a number of other problems. Finally got it all straightened out and the script worked.

Will attempt to duplicate some of the original problems such as one message received was something about ‘This system is not supported’.

Initial reaction is that people with little Python exposure will have difficulty with this.

It does seem to have potential though.

Thanks, this library it’s continuous developer. Yes, the start is little hard, but more late, it’s delicious work with this.

Please, it’s better report any error in the tickets system the repository, if not is possible, a new thread in this site it’s ok.

No problem with your request for error reporting but some regard this post. For example, you should include:

import uno
import easymacro as app

in the script. Now I realized it was missing import's but had to figure out which ones. Also that to test as is it should be named as mymacros.py. Had to examine the code to figure this out.

Just simple items like that will make some people just give up saying “it doesn’t work” and abandoning it.

Thanks, add note for this.