Writer-text multiselection with starbasic, a possible solution

It is known that, unlike calc, in writer it is not possible to construct a textranges starting from an array of textRange, because the textranges object does not support an addtextrange method.
This is possible solution
It’s not particularly fast solution on large textdocuments and several textranges to add
Also, this solution currently does not support textrange belonging to both the document and the contained textFrames and does not support textrange oTR with oTR.string=""
For the rest it seems to me that it works, so I propose it as a possible solution

Attached file includes
a) the macros needed both to obtain the textranges and to run the necessary tests, all contained in the “multisel” module
b) a user toolbar (“multisel”) to run the test
c) a minimum of documentation both at the beginning of the text to be tested and in the code attached to the file

Edit: sorry, replaced the attached file: due to a final transcription error, right at the end of the test, once a test was finished, another was started, losing the results of the first (now correct)
Made small fixes to intermediate views in the test

First version: ask_lo_multisel.odt (54.1 KB)
Second version 26 february 2023 ask_lo_multisel_01.odt (56.0 KB)

I don’t know whether this is “safe improvement”, but it is about 3× faster on my laptop.

function wr_msel_start_of_TR_all(t, a, vc)   ''wr_msel_start_of_TR  ex TR_VPage

	dim i&, oTr2, oCur2, p(), s$
	oCur2=ThisComponent.Text.createTextCursor
	
	for i=0 to ubound(a)
		oTR=a(i)
		if oTR.string<>"" then
			vc.goRight(len(oTR.string), true) 'select current range
			if i<>ubound(a) then
				oTR2=a(i+1) 'next selection
				oCur2.goToRange(oTR.End, False) 'put text cursor to the end of 1st selection
				oCur2.goToRange(oTR2.Start, true) 'move text cursor to the start of 2nd selection
				s=oCur2.String 'the string from end of 1st selection to start of 2nd selection
				p=split(s, chr(13)) 'the count of Enters in text cursor
				vc.goRight(len(s)-ubound(p), false) 'move visible cursor to the start of 2nd selection
			end if
		end if
	next i

	vc.goleft(0,false):	vc.goright(0,true) 	' dummy selection for set all multiselected textRange from index=1 in oD.currentSelection
											' with this dummy selection, at index 0 is located dummy selection and not last text range selected
end function
1 Like

@KamilLanda your idea is excellent. :+1:
From a first test just performed on a very large file (39 pages, 156 selected items) I confirm the tripled speed.
However, the shift is always relative to the previous textrange. So if for any reason (and I find it in my test after several correct recognitions) the viewcursor finds itself in an incorrect position (e.g. a few characters after the beginning of the correct textRange), from this moment the error propagates to all subsequent
In other words, your code is excellent for approaching in a single solution near the next textrange, but a check is still required for any fixes.
And this can be done with a call to the attached wr_msel_start_of_TR function or by using the compareRegionStarts function directly in the loop to test the coincidence of the starting points of the viewcursor and the textrange to be selected, eventually deciding to move with characters (and no more lines)
In my test about halfway through the document, suddenly (I don’t know why) the selection seems to extend to the start of the next paragraph, and from that point all subsequent acknowledgments are progressively more and more garbled.
With the control I just proposed this should be avoided
I add that if in the input array the textranges to be selected are NOT arranged one behind the other, therefore the starting positions are not in ascending order, the entity of the shift obtained with len(s) - -ubound(p) can be negative (thus move with goLeft), not positive (thus move with goRight)
In other words, your proposal greatly improves response times, but still needs to be integrated with checks and any correctives that I already used in my solution
The solution I proposed proved to be the most stable one. Previously I had tried solutions with a kind of binary search that allows the view cursor to get closer to the final target for subsequent adjustments
Both in terms of lines and character, for example, I first moved 20 lines at a time, then passed the target back 10, then forward 5, etc. until I reached a line away. And from that point I repeated the same process with the characters
But in an inexplicable way in certain situations we entered infinite loops and related blockage
I don’t know if this stability problem is also part of the problems that occasionally occur in the snap version, the one I’m using for now
The important thing is that the (known) limitation of writers in not allowing the merge of individual textranges into a single textranges has a possible solution, to be tested to know any application limits

I’m probably just too stupid, but I can’t find the problem this solution fits?

just to sort the paragraphs with ParaStyle Heading 1:

from collections import deque

def sort_heading_1(*_):
    doc = XSCRIPTCONTEXT.getDocument()
    headings = []
    for para in doc.Text:
        try:
            if para.ParaStyleName == "Heading 1":
                headings.append(para.String)
        except AttributeError:
            pass
    headings = deque(sorted(headings))
    for para in doc.Text:
        try:
            if para.ParaStyleName == "Heading 1":
                para.String = headings.popleft()
        except AttributeError:
            pass

KamilLanda : I figured out why at some point in my test file the sync between the start of the viewCursor and the start of the textrange was lost
In the textrange where the synchrony was skipped, this arrow character (🠖) appeared which is encoded in U+1F816 (ascii >64k, 3byte)
This character for Libreoffice counts as two “normal” characters, so len(“🠖”) for libreoffice equals 2 non 1 as thought…
In my solution it wasn’t noticeable because the viewcursor.start was constantly monitored as it was moved, line by line, character by character to the next textrange to select.
However, I now notice that where these special characters with ascii > 64k existed, the paragraph placeholder was also included in the selection, which is obviously incorrect.
The problem can be corrected, in the next few days I will try to integrate my solution with that of KamilLanda

So it seems the question is how to count quickly the long characters in oCur.String and not only Enters. I tried the split() twice, maybe the com.sun.star.util.TextSearch could be faster, I don’t know now.

function wr_msel_start_of_TR_all(t, a, vc)   ''wr_msel_start_of_TR  ex TR_VPage
	dim i&, oTr2, oCur2, p(), s$, iLong&
	oCur2=ThisComponent.Text.createTextCursor
	
	for i=0 to ubound(a)
		oTR=a(i)
		if oTR.string<>"" then
			vc.goRight(len(oTR.string), true) 'extend multiselect viewCursor to end of current TextRange and select it	
			if i<>ubound(a) then
				oTR2=a(i+1)
				oCur2.goToRange(oTR.End, False)
				oCur2.goToRange(oTR2.Start, true)
				s=oCur2.String 'the string from end of 1st selection to start pf 2nd selection
				
				p=split(s, chr(13)) : iLong=ubound(p) 'the count of Enters
				p=split(s, chr(55348)) : iLong=iLong+ubound(p) 'the count of long characters (for example U+1D160 'Musical symbol eighth note' that is taken like 2 chars U+D834 & U+DD60; dec: 55348 & 56672)
				
				vc.goRight(len(s)-iLong, false) 'move visible cursor to the start of 2nd selection
			end if
		end if
	next i
	vc.goleft(0,false):	vc.goright(0,true) 	' dummy selection
end function

To identify characters with ascii >64k I think it is preferable to use a regex with a pattern like replace [\u10000-\u1FFFF] with a single unique character and then split on this character, obviously on a copy of the string
But as a first approximation, I have to work on it in the next few days, I think it’s faster to use your technique limiting it to carriage returns and then still having to check for safety with compareRegionStarts, move very few characters, one at a time, using a while instruction
Also remember that the textranges in the array may not be in positional order, and therefore important shifts to the left must also be managed, not only to the right (although these are the most frequent)
I work with very large test files (one of 350 pages) with which to verify the correctness of the solution in very different contexts.
I’d say it’s only a matter of days to be sure of a perfectly working code.
Anyway, I never thought that the len function gives different values ​​if the string contains graphic characters with ascii >64k

I din’t discover how to count the characters [\u10000-\u1FFFF] in string.
But split() is OK and it is faster than “com.sun.star.util.TextSearch”I tested it with Enter \u000D.
Enter is also taken like long character in .goRight(1, true) → chr(13) & chr(10).

Sub test 'show the Ascii codes of characters in document
	dim oDoc as object, oSel as object, oVCur as object, s$, i&, sOut$, a&, x$, sreg$, sLong$, sOut2$
	oDoc=StarDesktop.LoadComponentFromUrl("private:factory/swriter", "_blank", 0, array())
	oVCur=oDoc.CurrentController.VIewCursor
	s="a" & chr(13) & "b" & getLongString(119070) & getLongString("U+1D15E") & getLongString("U+1D15F") & getLongString("U+1D160") & getLongString("U+1D161") & getLongString("U+1D162")
	oDoc.Text.String=s

	oVCur.goToStart(false)
	do while oVCur.goRight(1, true)
		s=oVCur.String
		if len(s)=1 then
			a=Asc(s)
			sOut=sOut & s & ": " & CLng(a) & ", U+" & Hex(a) & chr(13)
		else
			sOut2=""
			for i=1 to Len(s) 'long char
				x=Mid(s, i, 1) 'character
				a=Asc(x) 'Ascii code
				sOut2=sOut2 & x & ": " & CLng(a) & ", U+" & Hex(a) & iif(i>1, chr(13), " & ")
			next i
			sOut=sOut & sOut2
		end if
		oVCur.collapseToEnd()
	loop
	msgbox(sOut, 0, "string traverse via goRight()") 'Enter in goRight() is written as chr(13) & chr(10)

	oVCur.goToStart(false)
	oVCur.goToEnd(true)
	
	s=oDoc.CurrentController.Selection.getByIndex(0).String
	
	sreg="\u000D" 'count of Enters
	msgbox("Count of Enters: " & getCountRegex(s, sreg),, sreg)
	
	sreg="\uD834" 'count of 1st char =chr(55348)
	msgbox("Count of chr(55348): " & getCountRegex(s, sreg),, sreg)
	
	sreg="[\u10000-\u10FFFF]" 'count of long characters
	msgbox("count of [\u10000-\u10FFFF]: " & getCountRegex(s, sreg)& chr(13) & _
		"correct via split(): " & ubound(split(s, chr(&HD834&))),, sreg) 'split() is OK
	
	'oDoc.close(true)
End Sub

Function getLongString(s$) as string 'get string from value from U+10000 to U+10FFFF
	const c=55348 'const for 1st character of long character
	if Left(s, 2)="U+" then s="&H" & Mid(s, 3, len(s)) & "&" 'transform: U+...  ->  &H...&
	getLongString=chr(c) & chr(CLng(s)-c-7116)
End Function

Function getCountRegex(s$, sreg$) as long
	dim pole(2), oSearch as object, oParam as object, oFound as object, oStart as object, oEnd as object, i&
	oSearch=CreateUnoService("com.sun.star.util.TextSearch")
	oParam=CreateUnoStruct("com.sun.star.util.SearchOptions")
	with oParam
	  .algorithmType=com.sun.star.util.SearchAlgorithms.REGEXP
	  .searchString=sreg
	end with
	oSearch.setOptions(oParam)
	oFound=oSearch.searchForward(s, 0, Len(s))
	do while oFound.subRegExpressions()>0
		oStart=oFound.startOffset()
		oEnd=oFound.endOffset()
		i=i+1
		oFound=oSearch.searchForward(s, oEnd(0), Len(s))
	loop
	getCountRegex=i
End Function
' the first solution implement split with different markers recognizable with regex pattern


sub splitTest01()   ' test solution one, with split
  dim s,c
                          '  chr in string with   &H0100 < ascii <&HFFFF    -->  ‹›➁➞➞➞
  s="‹Simboli grafici ascii› utili (AscSym) ➁➞➞➞"  
  c=splitRegex_01(s,"[\u01ff-\uffff]")

end sub

sub splitTest02()   ' test solution two, remove character not in range  &H0100 < ascii <&HFFFF 
  dim s,c
                                             
  s="‹Simboli grafici ascii› utili (AscSym) ➁➞➞➞"   
  c=splitRegex_02(s,"[^\u01ff-\uffff]")    ' u lowercase
end sub

sub splitTest02a()     '  test solution two, remove character not in range  &H00010000 < ascii <&H0001FFFF 
  dim s,c
                                          '  chr in string with   &H00010000 < ascii <&H0001FFFF    -->  🠖🠖🠖 🠖🠖🠖
                                          ' ascci("🠖") = &H1F816
  s="‹Simboli grafici ascii› 🠖🠖🠖utili (AscSym) ➁🠖🠖🠖"  '🠖🠖🠖
  c=splitRegex_02(s,"[^\U00010000-\U0001FFFF]")  ' ' U uppercase
end sub

' -------------------------------------------------------------------------------------------------------

function splitRegex_01(byval s,sPatt) 							
	dim calc_fnAccess,sOut, a
	const sSep="•"   ' ascii &H2022 
    calc_fnAccess = createunoservice("com.sun.star.sheet.FunctionAccess")
    sOut = calc_fnAccess.callFunction("REGEX", array(s,sPatt,sSep,"g"))  
    a=split(sOut+" ",sSep)
    splitRegex_01=ubound(a)   'Simboli grafici ascii 🠖🠖🠖utili (AscSym) ➁🠖🠖🠖
end function  



' NOTE: 
' calc_fnAccess variable should be sized as global, testing each time whether it is empty or not,
' in the first case it initializes. This allows you to avoid redefinition every time

function splitRegex_02(byval s,sPatt) 	 ' solution two, remove character not in range defined in sPatt
	dim calc_fnAccess,sOut
	const sSep="•"   ' ascii &H2022 
    calc_fnAccess = createunoservice("com.sun.star.sheet.FunctionAccess")
    sOut = calc_fnAccess.callFunction("REGEX", array(s,sPatt,"","g")) 
    splitRegex_02=len(sOut)/2  ' string contain only single character with length  2
end function

KamilLanda
I put together my solution and yours, adding a series of checks to take into account the special cases I mentioned in my previous replies to your posts
It seems to work well, the speed is double that currently present in the file attached to the first post due to the decisive contribution of your intuition.
Before publishing it, I hope tomorrow (Italian time) I’ll wait to test it completely.
This new version

  1. use the paragraph separator according to the operating system (chr(13)+chr(10) is used under windows, under linux it is only chr(10))
  2. check that the initial and final extremes of viewcursor and current oTR coincide: (if the current oTR contains graphic characters with ascii >64k goright(len(oTR.string, true) does not work correctly
  3. Correctly determines the start and end position of the viewcursor by superimposing it on the current textrange even when the next textrange is placed in the text before the current one textrange

Regarding characters with ascii >64k, their presence in a document is statistically very low.
I discovered the problem in the testing phase only because by chance while using many characters with ascii <64k, I forgot to delete some with ascii >64k
I don’t know if it is beneficial to introduce a test on each oTR to test for the presence of any character with ascii>64k, when the problem will almost never occur, it can become unnecessary overhead
However, having to check the coincidence of the extremes of viewcursor and oTR, the minimum correction required (a few shift characters) still solves the problem of the presence of these characters with minimum processing times and greater simplicity of the code

My bug was [\u10000-\u1FFFF], of course properly is like in your example → [\U00010000-\U0010FFFF] :-).

So I tested and com.sun.star.util.TextSearch is mostly faster than com.sun.star.sheet.FunctionAccess.

sub testA
	dim s, c, d, i
	d=GetSystemTicks()
	for i=1 to 10000
		s="‹Simboli grafici ascii› utili (AscSym) ➁" & chr(55348) & chr(56753) & chr(55348) & chr(56754) & chr(55348) & chr(56672) & chr(55348) & chr(56674)
		c=splitRegex_02(s,"[^\U00010000-\U0001FFFF]") '11,344s
		'c=getCountRegex(s, "[\U00010000-\U0010FFFF]") '6,674s
	next i
	d=GetSystemTicks()-d
	msgbox(s & chr(13) & d/1000 & " seconds")
end sub


It is posisble to use inStr() to speed up the detection of >64k chars

sub testB
	dim s, c, d, i	
	d=GetSystemTicks()
	rem string with normal characters
	for i=1 to 5000
		s="‹Simboli grafici ascii› utili (AscSym) " 
		if inStr(s, chr(55348))>0 then
			'c=splitRegex_02(s,"[^\U00010000-\U0001FFFF]")
			c=getCountRegex(s, "[\U00010000-\U0010FFFF]")
		end if
	next i
	rem string with >64k chars
	for i=1 to 5000
		s="‹Simboli grafici ascii› utili (AscSym) " & chr(55348) & chr(56753) & chr(55348) & chr(56754) & chr(55348) & chr(56672) & chr(55348) & chr(56674)
		'c=splitRegex_02(s,"[^\U00010000-\U0001FFFF]") 
		c=getCountRegex(s, "[\U00010000-\U0010FFFF]")
	next i
	d=GetSystemTicks()-d
	msgbox(d/1000 & " seconds")
	rem splitRegex_02 '5,518s
	rem getCountRegex '3,392s
end sub


But the fastest is simple ubound(split(s, chr(55348))) to detect 64k+ chars characters

sub testC
	dim s, c, d, i
'	d=GetSystemTicks()
'	for i=1 to 10000
'		s="‹Simboli grafici ascii› utili (AscSym) " & chr(55348) & chr(56753) & chr(55348) & chr(56754) & chr(55348) & chr(56672) & chr(55348) & chr(56674)
'		'c=splitRegex_02(s,"[^\U00010000-\U0001FFFF]") '11,292s
'		c=getCountRegex(s, "[\U00010000-\U0010FFFF]") '11,158s
'	next i
'	d=GetSystemTicks()-d
'	msgbox(d/1000 & " seconds")


'	d=GetSystemTicks()
'	for i=1 to 10000
'		s="‹Simboli grafici ascii› utili (AscSym) " & chr(55348) & chr(56753) & chr(55348) & chr(56754) & chr(55348) & chr(56672) & chr(55348) & chr(56674)
'		if inStr(s, chr(55348))>0 then
'			'c=splitRegex_02(s,"[^\U00010000-\U0001FFFF]") '11,254s
'			c=getCountRegex(s, "[\U00010000-\U0010FFFF]") '6,845s
'		end if
'	next i
'	d=GetSystemTicks()-d
'	msgbox(d/1000 & " seconds")

rem with split(), the count for loop is increased because split() is fast

'	d=GetSystemTicks()
'	for i=1 to 200000
'		s="‹Simboli grafici ascii› utili (AscSym) " & chr(55348) & chr(56753) & chr(55348) & chr(56754) & chr(55348) & chr(56672) & chr(55348) & chr(56674)
'		if inStr(s, chr(55348))>0 then
'			c=ubound(split(s, chr(55348))) '7,285
'		end if
'	next i
'	d=GetSystemTicks()-d
'	msgbox(d/1000 & " seconds")


	d=GetSystemTicks()
	for i=1 to 200000
		s="‹Simboli grafici ascii› utili (AscSym) ➁" & chr(55348) & chr(56753) & chr(55348) & chr(56754) & chr(55348) & chr(56672) & chr(55348) & chr(56674)
		c=ubound(split(s, chr(55348))) '5,623s
	next i
	d=GetSystemTicks()-d
	msgbox(d/1000 & " seconds")
end sub

I read one article about some encodings before few days and there was written for example China or Korean characters must be in UTF-16 because there isn’t enough of space in UTF-8 for ones.
So what to add the optional parameter to the function? And set the detection of 64k+ as default, but have the possibility to turn off the detection if user is sure he hasn’t any long character?

And there must be also the detection for 64k+ chars also for vc.goRight(len(oTR.string), true) 'extend multiselect viewCursor to end of current TextRange and select it, because try to add some long chars to the Headings and the problem will occur :-(.

I used functionAccess only to demonstrate that using regex it is possible to implement the oRegex.split function which currently is not yet available on LO (but it seems will be in the next versions), using first a substitution on pattern and then splitting
I didn’t use com.sun.star.util.TextSearch because unlike the searchdescriptors I didn’t use it enough
However, the problem with split is that it works well if you have a single character with asc >64k to search for, and this is not my case, for example: as much as you try not to use chr with asc>64k, if I decide to use them, I use more than one like and like me other users

The solution that we try to propose in this thread must be the most general and reliable possible, otherwise it’s useless, and in the tests I discover particular cases that I didn’t know
For example, I discovered today that the len function has strange behaviors even when the text to be selected includes lists of paragraphs, while it seems to have none with tables (cells only with text without anything else nested).
Unfortunately it is not just a problem of characters with asc >64k, the len function at least for now is very useful for speeding up the approach to the next textrange but it is not yet completely reliable in getting there with precision, hence the need for corrections

It can be done but I don’t know how and, above all, I don’t know the effects of the character extension with asc >64k on processing times
We should remember that with the final control and eventually corrections (moving a few characters to the right or to the left), all these problems can be solved with a single solution, i.e. moving a few further characters following a trivial check

I redid the code compared to the previous version introducing and extending the contribution of @KamilLanda, whom I thank again, greatly improving the performance and the reliability of the code losing for now, however, the support of the textcontent selection in the textTable supported in the first version
Both versions remain available as attachments in the first two posts at the start of the thread.
The instructions for use are all contained in the attached file
I am interested in any comments, suggestions and bug detection

I discovered the long characters aren’t compose only from chr(55348), but also from other characters:
longChars

So it seems there is only one safe way for detection ones → [\U00010000-\U00010FFFF].
But I think the correction with goRight(iMove, false)/goLeft(iMove, left) is good solution in 2nd prototype :-).


It is possible to use the detection of OS via function GetGuiType or Calc function INFO(“system”)

	dim sEndOfPar$ 'Enter as per OS
	select case GetGuiType
	case 3 'mac
		sEndOfPar=chr(13)
	 case 4 'unix/linux
		sEndOfPar=chr(10)
	case 1 'win
		sEndOfPar=chr(13) & chr(10)
	case else 'unknown?
		sEndOfPar=chr(10)
	end select

so I suppose the split() for long Enter chr(13)&chr(10) in function wr_multisel is needful only under Windows:

	iMove=len(oTC.string)- iif(GetGuiType=1, ubound(split(oTC.string, sEndOfPar)), 0)

Yes, @KamilLanda, that was what I was trying to tell you in previous posts, there are so many interesting special characters with ascii code >64k, even if I try not to use them limiting myself to those with asc <64k

However, with one of the techniques I explained about splits (the second one in particular), not only can you detect them, in real number and length, but above all you can perform the split with separators of various types detectable by regex patterns, just precede the split by a replacement of the chr in the copy of the string (to not lose the original) with a unique separator and then split.

However, in the new solution I proposed I strengthened the use of your technique by including it in a loop.

By inserting some counters (which I eliminated in the version I posted), I saw that on n texrange that cycle is repeated less than 2n times in all and that the corrective that followed the cycle (goleft/goright) is also executed very few times (minus 2-3n at most) and this on really important files

Also, most importantly, I really think I’ve also found a way to make the second version work within tables as well, even with nested tables, I still need to check a couple of things before I’m sure.
But I wasted a lot of time making these first two versions, so I’ll take a few days to make it

Thanks again for the contribution, I hope there is some idea, contribution from others too, for example in the testing phase under windows, for example, which I don’t use.

PS: I remembered that there was a function like GetGuiType, but I was looking for it under writer, so I solved it in another way.
The split is also useful under linux, if the number of paragraph terminators must be subtracted to obtain the correct shift.

Good explanation, I understand well now :-). And I’m also like I discovered the concrete characters with different codes, I think I can use these concrete data for testing in other task :-).

The applications are remarkable.
Imagine, for example, inserting a hidden section of text at the end of specific paragraphs (heading, for example, but not only) to make it appear/disappear, preceded by a special character (ascii >&HFF and < &HFFFF) to signal its presence and avoid involuntary cancellations.
With these characters you can quickly define entire sections of text at both hierarchical and functional level, facilitating search and filtering
Some of these characters inserted as drop caps (and therefore with different styles from the paragraph) to signal and graphically highlight the function of a block of text allow graphically to replace the images (which weigh much more in terms of space, for intensive use) being for more usable in research with patten regex.
The advanced use of regex patterns also thanks to these characters allows really fast searches even on documents with more than a million characters and with several hundred pages (something already tested)
Their importance would only be reduced if it were possible to search by character/paragraph attribute by working on ranges of values ​​and not, as has been possible so far, only for single values.
But this possibility is not supported for now and therefore other solutions must be used, obviously only if the need is felt (it is my case, but I use writer and calc in a very anomalous way)

It is known that, unlike calc, in writer it is not possible to construct a textranges starting from an array of textRange, because the textranges object does not support an addtextrange method.
This is possible solution
It’s not particularly fast solution on large textdocuments and several textranges to add
Also, this solution currently does not support textrange belonging to both the document and the contained textFrames and does not support textrange oTR with oTR.string=""
For the rest it seems to me that it works, so I propose it as a possible solution

Attached file includes
a) the macros needed both to obtain the textranges and to run the necessary tests, all contained in the “multisel” module
b) a user toolbar (“multisel”) to run the test
c) a minimum of documentation both at the beginning of the text to be tested and in the code attached to the file

Edit: sorry, replaced the attached file: due to a final transcription error, right at the end of the test, once a test was finished, another was started, losing the results of the first (now correct)
Made small fixes to intermediate views in the test

LO 7.4.5.1 snap ubuntu 16.04

First version: ask_lo_multisel.odt (54.1 KB)
Second version 26 february 2023 ask_lo_multisel_01.odt (56.0 KB)

1 Like