Is it possible to have automatic capitalization of defined variables/constants and keywords in Basic?

Say I have created a number of modules with Basic code in my Calc document.
One module is named VarConstDefinitions.

It includes some definitions, e.g.

Option Compatible
Option Explicit

[...Stuff...]
Public Const myUserName as string = "John"

Now my question:
Whenever I write myusername (lowercase!) in some other module of the same library and then hit space/enter/tab afterwards, is it possible to have the Basic editor capitalize the U and N automatically? This works in Excel’s VBA editor.
Also, could this work so that string would automatically capitalized as well? → String

I know this does not matter regarding Basic, but I would really prefer if it worked.

So I have been to hell and back on Google for the past hour or so.

Found this: Basic IDE

In short, the answer is: yes it is available, but it is arguably buggy and does not really work in my experience. For example, defining a variable needs to happen through Dim, the markers Public or Const are not used by the IDE for that recognition of already existing variables/keywords, meaning the auto-capitalization does not work then. And aforementioned recognition does not include defined markers from other modules in the same library/workbook.

Regardless, from the link above:

Warning: This feature is experimental and may produce errors or behave unexpectedly.

  1. To enable it anyway, go to: Tools - Options - LibreOffice - Advanced and select Enable experimental features checkbox.

  2. Restart LO.

  3. Go to: Tools - Options - LibreOffice - Basic IDE (this is only visible after restarting)

  4. Set the checkmarks for all the experimental features you wish to choose.

  5. Start coding.

  6. Prepare to be somewhat disappointed. :confused:

It is more, than I ever expected to happen. For a modern environment my suggestion would be to use python like in this thread:
https://forum.openoffice.org/en/forum/viewtopic.php?t=110454

2 Likes

It is possible to add the listener on keys in Basic Editor to improve the ability of editor. But I suppose it will need really to have strong nerves for real use :-)!

Run IDE_listener_ON and then every pressing the Tab will transform whole line to UpperCases.

Esc cancels the listener.

Experiment with doKeyCode in ListIDE_keyReleased to make your required changes with the string, good luck :-).

Maybe also wait 1 could be increased.

global IDE_SUBWINDOW as object
global IDE_LISTENER as object
global IDE_DOC as object
global CLIPBOARD$

Sub IDE_listener_ON
		dim oComponents as object, oComponent as object, o as object, oListener as object
		oComponents=StarDesktop.Components.CreateEnumeration
		while oComponents.hasMoreElements 'try all Libre windows
			oComponent=oComponents.NextElement
			if oComponent.Identifier="com.sun.star.script.BasicIDE" then 'Basic Editor is active
				IDE_DOC=oComponent
				IDE_SUBWINDOW=oComponent.CurrentController.ComponentWindow.AccessibleContext.getAccessibleChild(0).AccessibleContext.getAccessibleChild(1).AccessibleContext.getAccessibleChild(4).AccessibleContext.getAccessibleChild(0).AccessibleContext.getAccessibleChild(2)
				IDE_LISTENER=CreateUnoListener("ListIDE_","com.sun.star.awt.XKeyListener")
				IDE_SUBWINDOW.addKeyListener(IDE_LISTENER)	
				exit sub
			end if
		wend
End Sub

Sub ListIDE_disposing
End Sub

Sub ListIDE_keyPressed(optional oEvt)
End Sub
	
Sub ListIDE_keyReleased(optional oEvt)
	select case oEvt.KeyCode 'codes: com.sun.star.awt.Key
	case 1281 'Escape
		IDE_SUBWINDOW.removeKeyListener(IDE_LISTENER) 'deactivate IDE listener
	case 1282 'Tab
		doKeyCode(1558) 'select to start of paragraph
		wait 1
		doUno("Cut") 'cut selection to clipboard
		wait 1
		dim s$
		s=getTextFromClip() 'get string from clipboard
		s=UCase(s) ' ---> change the string <---
		copyToClip(s) 'copy string to clipboard
		wait 1
		doUno("Paste")
		copyToClip("") 'delete clipboard
	end select
End Sub

Sub doKeyCode(iCode%) 
	dim oKeyEvent as new com.sun.star.awt.KeyEvent
	oKeyEvent.Modifiers=0
	oKeyEvent.KeyCode=iCode 'codes from com.sun.star.awt.Key
	simulateKeyPress(oKeyEvent)
End Sub

Sub simulateKeyPress(oKeyEvent as com.sun.star.awt.KeyEvent)
	if NOT IsNull(oKeyEvent) then
		dim oWindow as object, oToolkit as object
		oWindow=IDE_DOC.CurrentController.Frame.ContainerWindow
		oKeyEvent.Source=oWindow
		oToolkit=oWindow.getToolkit()
		oToolkit.keyPress(oKeyEvent)	
		oToolkit.keyRelease(oKeyEvent)
	End if
End Sub

Sub doUno(s$)
	s=".uno:" & s
	createUnoService("com.sun.star.frame.DispatchHelper").executeDispatch(IDE_DOC.CurrentController.Frame, s, "", 0, array())
End Sub

Function getTextFromClip() as string 'get text from system clipboard
	dim oClip, oConv, oCont, oTyps, i%
	oClip=CreateUNOService("com.sun.star.datatransfer.clipboard.SystemClipboard")
	oConv=CreateUNOService("com.sun.star.script.Converter")
	oCont=oClip.getContents()
	oTyps=oCont.getTransferDataFlavors()
	for i=lbound(oTyps) to ubound(oTyps)
		if oTyps(i).MimeType="text/plain;charset=utf-16" then
			getTextFromClip()=oConv.convertToSimpleType(oCont.getTransferData(oTyps(i)), com.sun.star.uno.TypeClass.STRING)
			exit for
		end if
	next
End Function

Function LsystClip_getTransferData( aFlavor As com.sun.star.datatransfer.DataFlavor)
	If (aFlavor.MimeType="text/plain;charset=utf-16") Then
		LsystClip_getTransferData()=CLIPBOARD
	End If
End Function

Function LsystClip_getTransferDataFlavors()
	Dim aFlavor As New com.sun.star.datatransfer.DataFlavor
	aFlavor.MimeType="text/plain;charset=utf-16"
	aFlavor.HumanPresentableName="Unicode-Text"
	LsystClip_getTransferDataFlavors()=array(aFlavor)
End Function

Function LsystClip_isDataFlavorSupported( aFlavor As com.sun.star.datatransfer.DataFlavor) As Boolean
	If aFlavor.MimeType="text/plain;charset=utf-16" Then
		LsystClip_isDataFlavorSupported=true
	Else
		LsystClip_isDataFlavorSupported=false
	End If
End Function

Sub copyToClip(s$)
	Dim oClip As Object, oTR As Object
	oClip=createUNOService( "com.sun.star.datatransfer.clipboard.SystemClipboard")
	oTR=createUNOListener("LsystClip_", "com.sun.star.datatransfer.XTransferable")
	oClip.setContents(oTR,Null)
	CLIPBOARD=s
End Sub

Tested: Libre 7.6.1.2 Win10

Hello @KamilLanda !
The path through AccessibleContext is indeed very long.

  1. You need find in the hierarchy an object with AccessibleRole TEXT_FRAME (61).
  2. Its child objects have AccessibleRole PARAGRAPH (41) - macro lines that are displayed on the BasicIDE screen.
  3. You have a rich palette of methods for working with paragraphs through the AccessibleEdit. interfaces.

@sokol92 Thanks a lot for testing, there was my bug in one .getAccessibleChild(), I fixed it. Now it detects TEXT_FRAME 61, and it is possible to control the actual lines with code in Basic window, but I don’t know how to detect line where the Visible Cursor is.

Sub IDE_listener_ON
	const bTest=True 'change to False to activate Listener
	dim oComponents as object, oComponent as object, oListener as object, o1 as object, i&, s$
	oComponents=StarDesktop.Components.CreateEnumeration
	while oComponents.hasMoreElements 'try all Libre windows
		oComponent=oComponents.NextElement
		if oComponent.Identifier="com.sun.star.script.BasicIDE" then 'Basic Editor is active
			IDE_DOC=oComponent
			o1=oComponent.CurrentController.ComponentWindow.AccessibleContext.getAccessibleChild(0).AccessibleContext.getAccessibleChild(1).AccessibleContext 'for shorter line
			IDE_SUBWINDOW=o1.getAccessibleChild(6).AccessibleContext.getAccessibleChild(0).AccessibleContext.getAccessibleChild(2)

			if bTest then

				mri IDE_SUBWINDOW.AccessibleContext 'window with Basic code -> TEXT_FRAME=61 (com.sun.star.accessibility.AccessibleRole)

				rem get actual lines of Basic code
'				for i=0 to IDE_SUBWINDOW.AccessibleContext.AccessibleChildCount-1
'					s=s & IDE_SUBWINDOW.AccessibleContext.getAccessibleChild(i).Text & chr(13)
'					mri IDE_SUBWINDOW.AccessibleContext.getAccessibleChild(i)
'				next i
'				msgbox s
				
			else 'add listener to Basic IDE
				IDE_LISTENER=CreateUnoListener("ListIDE_","com.sun.star.awt.XKeyListener")
				IDE_SUBWINDOW.addKeyListener(IDE_LISTENER)
			end if
			
			exit sub
		end if
	wend
End Sub

When I copied macro to library Standard, it accepts the .getAccessibleChild(4) instead .getAccessibleChild(6), but in other library it accepts .getAccessibleChild(6) :frowning:

CaretPosition is for current line

rem get actual lines of Basic code
dim o2 as object
for i=0 to IDE_SUBWINDOW.AccessibleContext.AccessibleChildCount-1
	o2=IDE_SUBWINDOW.AccessibleContext.getAccessibleChild(i) 'line
	if o2.CaretPosition<>-1 then 'line has Visible Cursor
		msgbox o2.Text
	end if
next i

I once did this:

Option Explicit

' Find TEXT_Frame In BasicVBE
Sub FindTextFrame
  Dim oComp As Object, oVBE as Object, oWin as Object, oTextFrame As Object, oAcc As Object, oAccChild As Object
  Dim i As Long, s As String, arr
  For Each oComp In StarDesktop.Components
    If HasUnoInterfaces(oComp, "com.sun.star.lang.XServiceInfo") Then
      If oComp.supportsService("com.sun.star.script.BasicIDE") Then
        oVBE=oComp
        Exit For
      End If
    End If      
  Next oComp
  
  If Not (oVBE Is Nothing) Then
    arr=FindAccessibleRole(oVBE.CurrentController.Frame.ComponentWindow.AccessibleContext, 61)  ' 61 TEXT_FRAME
    If Ubound(arr)>0 Then
      oWin=arr(1)                ' XWindow
      oTextFrame=arr(0)          ' TEXT_FRAME
    End If
  End If
  
  If Not (oTextFrame Is Nothing) Then                                             
     For i=0 To oTextFrame.AccessibleChildCount-1    
       s=s & oTextFrame.getAccessibleChild(i).Text & Chr(10)
     Next i
     
     Msgbox s
  End If     
End Sub


' Hierarchically search AccessibleContext for role value.
' - oAccContext Object (supports AccessibleContext service).
' - role        role value.
'
' Returns an array with the following indexes:
' 0 Found object with a given role.
' 1 The last object in the chain that supports the XWindow interface.
' 2 Number of iterations to search.
' 
' If not found, an empty array is returned. 
' If the object has more than 500 child objects, then the child objects are not processed.
Function FindAccessibleRole(ByVal oAccContext, ByVal role As Long, Optional Byref arr)
  Dim oAccChild As Object, i As Long, n as Long, retval()
  FindAccessibleRole=Array()
  If IsMissing(arr) Then
    ReDim arr(2)
    arr(2)=0
  End If  
  n=oAccContext.AccessibleChildCount
  If n=0 Or n>500 Then Exit function
  For i=0 To oAccContext.AccessibleChildCount-1
    oAccChild=oAccContext.getAccessibleChild(i)
    If Not (oAccChild Is Nothing) Then
      If HasUnoInterfaces(oAccChild, "com.sun.star.awt.XWindow") Then
        arr(1)=oAccChild
      End If  
      If HasUnoInterfaces(oAccChild, "com.sun.star.accessibility.XAccessible") Then
        oAccChild=oAccChild.AccessibleContext
      End If  
      arr(2)=arr(2)+1
      If oAccChild.AccessibleRole=role Then
        arr(0)=oAccChild
        FindAccessibleRole=arr
        Exit Function
      End If
        
      retval=FindAccessibleRole(oAccChild, role, arr)
      If Not IsEmpty(arr(0)) Then
        FindAccessibleRole=arr
        Exit Function
      End If
    End If
  Next i
End Function
1 Like

@sokol92 I tried simpler recursion.

global IDE as object 'window with Basic Code
private IDEb as boolean 'faster exit from recursion findIDE()

Sub changeIDE
	dim oComp as object, o as object, oParent as object, oIDE as object
	for each oComp in StarDesktop.Components
		if HasUnoInterfaces(oComp,  "com.sun.star.lang.XServiceInfo") then
			if oComp.supportsService("com.sun.star.script.BasicIDE") then
				findIDE(oComp.CurrentController.ComponentWindow.AccessibleContext) 'set IDE
				oIDE=IDE.AccessibleParent.AccessibleContext.getAccessibleChild(IDE.AccessibleIndexInParent) 'oIDE.AccessibleContext = IDE
				rem change Properties, add Listeners etc.
				oIDE.Background=RGB(255, 255, 235)
				exit for
			end if
		end if
	next oComp
End Sub

Sub findIDE(o as object) 'find object with the Code of Basic IDE
	dim i&, n&
	on local error resume next 'for sure
	if IDEb then exit sub 'IDE already exists
	n=o.AccessibleChildCount
	if o.AccessibleRole=61 then 'window with Basic Code found
		IDE=o
		IDEb=true
		exit sub
	end if
	if n>0 then 'child is
		if IDEb then exit sub 'IDE already exists
		for i=0 to n-1
			findIDE(o.getAccessibleChild(i).AccessibleContext)
		next i
	end if
End Sub

But it is not good idea to put listeners in Basic to Basic IDE. If some mistakes will occur, then the variables are canceled and listener starts to show bug :frowning:. Maybe the solution could be to create Python extension with service to control the listeners of Basic IDE. But I don’t know now if I will try it.

Yes, this is true, especially when the macro text is changed in BasicIDE, since this resets the values of all global variables.

I tried it in Python and new problem is if user change the Module. Key-press selects the current line, but if user will switch to other Module (in same or other library), Libre changes the frame with Basic code to some “new” frame and I didn’t discover how to add new listener to “new” frame.

I run py code from external editor. Run LibreOffice with socket for Python, run Basic editor, and then this code. It will color the background of Basic IDE and key-press will select the current line. If you change the Module (for example in Object Catalog), then listeners from “old” frame are removed (event WindowHidden in ListenerWindow), but I don’t know how to detect “new” frame with Basic code from “new” Module.

import uno, unohelper

from com.sun.star.awt import XWindowListener
from com.sun.star.awt import WindowEvent

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

from com.sun.star.lang import EventObject

IDE=None # frame with Basic Code in Basic Editor
IDEb=False # for faster recursion in findIDE()

IDE1=None # object of IDE with writable properties
KEYlistener=None # listener for IDE

IDEparent=None # parent of IDE
WINDOWlistener=None # listener for IDEparent


def connectLibre():
    """start connection with LibreOffice"""
    try:
        localContext=uno.getComponentContext()
        resolver=localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
        ctx=resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
    except:
        ctx=None
    if ctx==None:
        print("!!! Failed to connect LibreOffice !!!")
        import sys
        sys.exit()
    else:
        return ctx


def mri(obj):
    """MRI"""
    m=createUnoService("mytools.Mri")
    m.inspect(obj)


def createUnoService(sName):
    """Basic: CreateUnoService()"""
    localContext=uno.getComponentContext()
    resolver=localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
    ctx=resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
    return ctx.ServiceManager.createInstanceWithContext(sName, ctx)


def hasUnoInterfaces(obj, *interfaces):
    """Basic: HasUnoInterfaces()"""
    return set(interfaces).issubset(t.typeName for t in obj.Types)


def rgb(r, g, b):
    """Basic: RGB()"""
    return 256*256*r+256*g+b


def findIDE(obj):
    """find frame with Basic Code"""
    global IDE, IDEb
    n=obj.AccessibleChildCount
    if IDEb: # IDE is already found
        return
    if obj.AccessibleRole==61: # frame with Basic Code found
        IDE=obj
        IDEb=True
        return
    if n>0: # has child
        i=0
        while i<n: # test children
            if IDEb: # IDE is already found
                return
            o=obj.getAccessibleChild(i)
            if not isinstance(o, type(None)):
                findIDE(o.AccessibleContext)
            i+=1


def setBackground(color):
    """set background color of frame with Basic Code"""
    global IDE1
    IDE1.Background=color


def IDEjob():
    """set keyboard listener to frame with Basic Code"""
    global IDE, IDEb, IDE1, IDEparent, WINDOWlistener, KEYlistener
    ctx=connectLibre()
    StarDesktop=ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
    for oComp in StarDesktop.Components:
        if hasUnoInterfaces(oComp, "com.sun.star.lang.XServiceInfo"):
            if oComp.supportsService("com.sun.star.script.BasicIDE"):

                findIDE(oComp.CurrentController.ComponentWindow.AccessibleContext) # set global variable IDE, that is frame with Basic Code

                if not isinstance(IDE, type(None)): # Basic Editor is open
                    # set key listener
                    IDE1=IDE.AccessibleParent.AccessibleContext.getAccessibleChild(IDE.AccessibleIndexInParent)
                    KEYlistener=ListenerKey()
                    IDE1.addKeyListener(KEYlistener)

                    # set window listener
                    IDEparent=IDE.AccessibleParent
                    WINDOWlistener=ListenerWindow()
                    IDEparent.addWindowListener(WINDOWlistener)
                    setBackground(rgb(255, 255, 235)) # change background that means keyboard listener is active


class ListenerWindow(unohelper.Base, XWindowListener):
    """window listener"""
    def __init__(self):
        pass

    def windowResized(self, evt: WindowEvent):
        pass

    def windowMoved(self, evt: WindowEvent):
        pass

    def windowShown(self, evt: EventObject):
        setBackground(rgb(255, 215, 215)) # change background of IDE if WINDOWlistener isn't removed
        return

    def windowHidden(self, evt: EventObject):
        global IDE1, IDEparent, WINDOWlistener, KEYlistener
        # remove listeners
        IDE1.removeKeyListener(KEYlistener)
        IDEparent.removeWindowListener(WINDOWlistener)
        setBackground(rgb(255, 255, 255))
        return

    def disposing(self, evt: EventObject):
        pass


class ListenerKey(unohelper.Base, XKeyListener):
    """keyboard listener"""
    def __init__(self):
        pass

    def keyPressed(self, evt: KeyEvent):
        pass

    def keyReleased(self, evt: KeyEvent):
        o=evt.Source.AccessibleContext
        iLines=o.AccessibleChildCount # count of lines with Basic code
        i=0
        while i<iLines:
            oLine=o.getAccessibleChild(i)
            if oLine.CaretPosition!=-1: # line with cursor
                oLine.setSelection(0, oLine.CaretPosition) # select current line
                break
            i+=1
        return

    def disposing(self, evt: EventObject):
        pass


IDEjob()

I don’t know how to end the communication with LibreOffice and Python editor, when I close LibreOffice, it stays run but it is invisible and I must end it via Ctrl+Alt+Del.
Tested in Libre 7.6.1.2 Win10