Speed up macro insertion of many text frames

I have a macro that inserts a lot of text frames and adds text in each frame as it goes along and applies styles to the text and whatnot (see code mockup below). The problem is, it runs rather slowly. It takes about 5 seconds to insert 15-20 frames, and I figure that is because it is manipulating the Writer document as it is going along inserting each frame.

Is there a way to build all the frames and manipulate their contents and then insert them all at once into the document? And would that actually speed things up? I tried moving the insertion command to the end, but then the macro crashed at “oFrame.Text.String =”.

(This happens inside a for-loop.)

oFrame = ThisComponent.createInstance( "com.sun.star.text.TextFrame" )
oFrame.widthType = 0
oFrame.FrameStyleName = "My Special Frame"
oFrame.Text.AnchorType = com.sun.star.text.TextContentAnchorType.AS_CHARACTER
oFrame.Text.VertOrient = 7
...
...
...
ThisComponent.Text.insertTextContent( viewCursor.Start, oFrame, false )
oFrame.Text.String = "[important stuff]"
[do stuff to the text in the frame]

UPDATE: This question doesn’t really matter so much anymore, because it seems like LibreOffice was just being slow for no reason. After restarting, my existing code runs much faster.

UPDATE2: The macro does slow down a lot when the document gets larger.

I don’t feel sure if I got everything right.
-1- Why do you use the ViewCursor? If you want to make lots of insertions in one run based on the selections you made in the text, you should use the CurrentSelction, imo.
-2- A frame not yet inserted as a TextContent obviously cannot take a string. That’s a result of your (and my) experiments. As so often I do not know a place where this is specified explicitly. We should take it as a fact nonetheless. If it actually is a bug you would have to report it and wait about 20 years for the fixing.
-3- With the code posted below I inserted 20 frames into a dummy text within about 1 second. Of course insertions into a very long text may need longer. After all every single insertion should cause a new page-wrap process, shouldn’t it? When I still had to use MS Word in the 1990es, it often needed minutes for the page wrap.

Code:

REM  *****  BASIC  *****
' NOT from profound knowledge about specifications
' BUT from my experiences and "research" (reverse engineering?)
' A single selection counts for ONE TextRange.
' With a multiple selection the (next term invented!) 'BlinkCursorPosition'
' counts for an extra TextRange and is returned as the 
' ZEROth range of the CurrentSelection object.
Sub insertManyFramesAtMultiSelectedPositions()
theText = ThisComponent.Text
theSel  = ThisComponent.CurrentSelection
u = theSel.Count - 1
If (u>0) Then
 blinkRg = theSel(0)
 lastRg  = theSel(u)
 h1 = theText.CompareRegionStarts(blinkRg, lastRg)
 h2 = theText.CompareRegionEnds  (blinkRg, lastRg)
 If (h1*h2=0) Then
  u = u-1
 End If
End If
Dim theFrames(0 To u) As Object
ThisComponent.UndoManager.EnterUndoContext("manyFrames")
For j = 0 To u
 oneFr = ThisComponent.createInstance( "com.sun.star.text.TextFrame" )
 oneFr.widthType = 0
 oneFr.FrameStyleName = "MySpecialFrameStyle"
 oneFr.AnchorType = com.sun.star.text.TextContentAnchorType.AS_CHARACTER
 theText.InsertTextContent(theSel(j).Start, oneFr, False )
 theFrames(j) = oneFr
Next j
For j = 0 To u
 theFrames(j).Text.String = "" & (j+1) &". Important Stuff"
Next j
ThisComponent.UndoManager.LeaveUndoContext
End Sub
' "MySpecialFrameStyle" was defined in the document used for testing.  

You may use this example containing the code.
An attempt to runl the Sub for a multiselection NOT only containing regions within ThisComponent.Text will fail. The Sub does not contain code to test for this.

It doesn’t look like this does anything much different than what I already had. What is the difference between CurrentSelection and getViewCursor?

What did I miss? As far as I can see you insert one frame at a time giving the position as viewCursor.Start. How would you iterate through the many positions of a muiltiselection this way?
The CurrentSelection is a collection of the single TextRanges selected at the same time and you can create a frame for each position and insert it while the macro iterates through the selected TextRanges.
This was what I roughly understood as the meaning of “combine” you used in the subject.

From the API documentation: "A TextViewCursor is a TextRange which can travel within a view of a Text object. "
I did a bit of considering and experimenting meanwhile. Results:
The above quotet statement is obviously not correct. The CurrentController can Select a ViewCursor saved after multiselection e.g. and the selection will not be “a TextRange”, but the multiselection again. You also can access the single TextRanges by index as if the ViewCursor was a textRanges object.

(Continuation)
However, the Count property of a TextRanges object is not accessible from a ViewCursor, The ViewCursor also cannot CreateEnumeration and there is no hint under SupportedServiceNames.
It seems to be another mess.
Well, for the current purpose the CurrentSelection is the much simpler object anyway.

This answer on another question changed my life! how to disable screen updating while running a macro in Calc?

Disabling the screen update while running the macro was the key!

ThisComponent.lockControllers()

All it takes is that command to freeze the document. Of course it is crucial when the macro is finished to unlock the document, otherwise you will be forced to close the document, and, in my experience, when you do close the document LO will crash.

ThisComponent.unlockControllers()

My macro runs soooo much faster on large documents now!

This makes things more complicated in the code though. Every time you abort a macro due to some user error you will have to make sure to unlock. So I ended up creating a little function so all my error messages actually call this function which displays the message and unlocks the doc before exiting the sub. (I also set an undo point at the beginning of the macro and close it here.)

'Prints an error message and then unlocks the document if it was locked
'so that the calling sub can safely exit
Sub SafeError (msg)
 MsgBox msg, 16, "Oh noes!"
 ThisComponent.unlockControllers()
 ThisComponent.UndoManager.LeaveUndoContext
End Sub

BUT DON’T FORGET you need to unlock the document even if there are uncaught errors in your code.

Sub myMacro
  On Error GoTo Unlock

  REM macro code here

Exit Sub

  Unlock:
	SafeError ("ERROR: " & Error$ & chr(13) & chr(13) & "line:" & Erl)

End Sub

If your do you macro like this any uncaught errors will jump to the Unlock section and generate a “safe error” (thus unlocking the document), displaying the error message and the line where it occurred.

It might be easier to create a wrapper function instead:

Sub SafeCallMyOtherFunction
  On Error Resume Next
  Dim oDoc As Object
  oDoc = ThisComponent
  oDoc.lockControllers() ' do this on a saved variable, since ThisComponent could change during a long operation '
  CallMyOtherFunction ' Might not care about proper locking/unlocking, since it would happen anyway (unless you terminate the macro from IDE) '
  oDoc.unlockControllers() 
End Sub