How to monitor keyboard input in a Libreoffice document using a python macro?

Tags: ,



Please note, this is a self answered question for reference. I haven’t found it documented for python, in spite of determined searching.

Rather than create a listener for a specific element in a dialog, I want to “listen” to keyboard input for a text document. The object being to perform an action if certain keys or combinations are seen.
Having created the following code using an Uno com.sun.star.awt XKeyListener, I had expected to see at least some indictation that it was functioning.

import unohelper
from com.sun.star.awt import XKeyListener

def fs_listen(*args):
    doc = XSCRIPTCONTEXT.getDocument()
    desktop = XSCRIPTCONTEXT.getDesktop()
    model = desktop.getCurrentComponent()
    contr = model.getCurrentController()
    url_current = doc.getLocation()
    oEventListener = KeyListen(doc)
    contr.addEventListener(oEventListener)

class KeyListen(unohelper.Base, XKeyListener):
    def __init__(self, parent):
        self.parent = parent
        print("listener added")

    def keyPressed( self,  event ):
        """ is invoked when a key has been pressed."""
        print("event",event)

    def keyReleased( self, event ):
        """ is invoked when a key has been released."""
        print("release",event)
    

The code will activate and run but produces no output for keyboard input, after “listener added”.
Where am I going wrong?

Answer

Counter-intuitively, it appears that adding a KeyListener to the CurrentController doesn’t do what one would think. It would appear to prime the document to receive input, which you’d think, it would be doing anyway.
The function that will produce a response to keyboard input, is an XKeyHandler.
It isn’t added as an EventListener though, it’s added as a KeyHandler.

This function has it’s own quirk, in that to cancel it, you need to remove the handler with exactly the same instance of oEventListener as was used to start it. Not an issue with oobasic, where the instance can be stored as “Global” and I assume, because it’s integral, it is maintained or stored in some way.

The problem for python is it can’t be stored in this way, unless someone reading this, knows of a way. It will not “pickle” and I ended up in a pickle, as to how to store it. I may well have missed something obvious.

The solution I ended up using, was to make the function self-cancelling, based on keyboard input.

Caveat: This has been tested on Linux only

The following can tested by starting LibreOffice from the command line lowriter and then run as any other macro, terminate with Shift+Alt+Ctrl+k

#!/usr/bin/python
import unohelper
from com.sun.star.awt import XKeyHandler
from com.sun.star.awt import Key
from com.sun.star.awt.MessageBoxButtons import BUTTONS_OK
from com.sun.star.awt.MessageBoxType import INFOBOX

fs_fkeys={} # dictionary of keys to identify each key
for key in dir(Key):
    fs_fkeys[getattr(Key, key)] = key

#Idiosyncrasies
# Shift_L, Ctrl_L, Alt_L are not reported as separate keys but are reported as modifiers
# 1,2 and 4 respectively. Shift_R and Ctrl_R are identical to their Left twins
# Alt_R (AltGr) is not reported and not a modifier.
# Super_R (Right Windows) is not reported but is a modifier, even though it doesn't modify any keys.
# It reports as modifier 8
# Super_L (Left Windows) is not reported and not a modifier.
# Caps_Lock and Num_Lock report as unidentified keys but not as modifiers.

# track key input with option of consuming the input (return True)
def fs_Tracker(*args):
    doc = XSCRIPTCONTEXT.getDocument()
    desktop = XSCRIPTCONTEXT.getDesktop()
    global contr, oEventHandler

    contr = desktop.getCurrentComponent().getCurrentController()
    oEventHandler = KeyHandler(doc)
    contr.addKeyHandler(oEventHandler)
    mess = "Key tracker activenTo deactivate close document or Shift+Alt+Ctrl K"
    heading = "Key Tracker"
    MessageBox(None, mess, heading, INFOBOX, BUTTONS_OK)

class KeyHandler( unohelper.Base, XKeyHandler ):
    def __init__(self, parent):
        self.parent = parent
        return None

    def Terminate ( self, event ):
        mess = "Key tracker deactivated!"
        heading = "Key Tracker"
        MessageBox(None, mess, heading, INFOBOX, BUTTONS_OK)

        contr.removeKeyHandler(oEventHandler)

    def keyPressed( self,  event ):
        k = event.KeyCode
        c = event.KeyChar.value 
        mods = event.Modifiers
        # mods are additive
        # 0 - None
        # 1 - Shift
        # 2 - Ctrl
        # 4 - Alt
        # 8 - Super_R   
        if c == "K" and mods == 7: #Shift+Ctrl+Alt+k
            self.Terminate(None)
            return True # Returning True consumes the key
                        # Thus assigning this macro to the same keyboard shortcut means that
                        # the macro is toggled On/Off by Shift+Alt+Ctrl+k
        if k in fs_fkeys:
            name = fs_fkeys[k]
        else:
            name = "Undefined"
        print(name, k, c, mods)
        return False

    def keyReleased( self, event ):
        return False

def MessageBox(ParentWindow, MsgText, MsgTitle, MsgType, MsgButtons):
    ctx = XSCRIPTCONTEXT.getComponentContext()
    sm = ctx.ServiceManager
    si = sm.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
    mBox = si.createMessageBox(ParentWindow, MsgType, MsgButtons, MsgTitle, MsgText)
    mBox.execute()

#List components that are accessible
g_exportedScripts = fs_Tracker,   


Source: stackoverflow