Duplicate a Drawpage object with Macro

Hello,
I am trying to figure out how to clone/duplicate a Calc Drawpage object either within the same sheet or to another sheet. This object could randomly be a Graphic object or Draw object or Control object. I tried using the dispatcher to copy and paste the object but that wasn’t reliable and it required select a cell which meant it need to switch sheets in some instances.

I tried to creating a new Drawpage object and copying all the properties from the original and then doing a Drawpage.add() to add the new object to the preferred drawpage, but that didn’t work reliably.

Does anyone know of a way to programmatically clone the object and set the position and sheet without having to switch sheets?

Please always mention your operating system and LibreOffice version. It will help anyone who wants to help you.

Why oh why?

Every spreadsheet has exactly one DrawPage.
If you are talking of a duplicate in the same document: Duplicate the sheet and remove the non-graphical content.
Graphical objects in sheets are mostly anchored to cells. If the anchor is kept during the duplication, the geometric arrangement can change due to different cell sizes …
The DrawPage isn’t an object you can handle as one thing independent of the hosting sheet.

1 Like

Sorry, I am using LO version 5.2.33 on a Linux OS

I am not talking of duplicating the Drawpage, only an object within the drawpage.

For example, I want it to duplicate a specific image that is anchored to a cell and anchor the new image to a different cell in same sheet or a different sheet.

Like I said, I am currently using the dispatcher to copy & paste the image, but that requires selecting a cell in the sheet before pasting and it makes it very slow when copying a lot of object across various sheets. I really need a way to quickly clone any object in a Drawpage without the dispatcher.

Who wrote this?

In any case you need to choose the object you want to work with, and the target where to place (and anchor?) the copy you want to create. Can you accept that the needed steps to accomplish the task may depend on what was done in advance? There will not be a Sub rightfully named putAcopyOfTheThingIhaveInMindToThePlaceIhaveInMind() (no parameters needed).
If you used a .uno:Command you surely had selected the object in advance. Will this still be the case? Will you do it manually (UI/mouse) or by code using the CurrentController. In what way will you call the “macro”.
We should finally get to business.

So this macro is part of a larger LO project that I can not share.

Currently, a macro loops through all the objects in the drawpage and selects them and uses the dispatcher to copy the object. Then it selects a cell in the other sheet and pastes it. This works most of the time but is not always reliable and it is very slow because of switching sheets for each object.

Attached is an example file that I came up with. The goal is to copy all the objects in Sheet1 to Sheet2 without having to switch sheets. I’m not sure why the current example doesn’t work without throwing an error.
Copy Sheet Objects Test v1.0.ods (127.4 KB)

Now you have a sheet with one graphical object anchored “To Page” (you may read “To Sheet”). And again you don’t tell where (anchor, position) the copy (clone) shall be inserted.
So far; See example
disask87302example.ods (67.0 KB)

Ok, I will check out your file.

I don’t need help positioning and anchoring the copied image. I already have code to do that. I only need help duplicating the objects in a fast and reliable manner.

I checked out your example and it works, but it still requires switching sheets so it isn’t faster than the dispatcher was.

That’s too unclear. Your example had 1 (one) shape. You didn’t cleartly tell if you wanted to have many (howe many) clones from the same collection. You didn’t tell what actually is “too slow” or “fast enough” for you.
So I may have wasted my time reworking my example a bit. It’s now bloated to 16 shapes (source) and has still a control shape that must be excluded. There can be taken one transferable object from the original, and it can be used to create many clones in many newly created sheets.
This way the routine needs (on my old system having some more applications loaded) about 190 ms (milliseconds) for the first clone, and about 30 ms for each additional one (including the creation of a new sheet).
This seems fast enough to me. After all there is something to do.

Ok, just to clear things up a bit, I will try to explain the process better. This file might have up to 300-500 objects (images & custom shapes) in a sheet and the code needs to clone all the objects to a different sheet that have a specific identifier in the object.name property. That means I have to loop through the drawpage and determine which objects need to be cloned. I was normally running the “clone” code for each object individually which made it very slow if it needed to switch sheets for each object.

Is it possible to loop through the drawpage and filter all the objects, that need to be cloned, into an array and then use the get/insert transferable to clone them? This would mean it would only have to switch sheets once.

Of course.
My first example demonstrated how to do this, The filter functionality was based there on the services supported for the shape object. This may have diverted your attention from the main point. Of course it is possible - and easier - to use other properties (subtype, name) for filtering. The main thing is to start from the whole DrawPage, gather the desired objects in a ShapeCollection object, and leave out the rest. This can’t be done with an ordinary array (sequence), because the result has to be made available for getTransferable via CurrentController.select(collection). A ShapeCollection is selectable, an array of shapes is not.
You then can use the same transferable object, say trans, as often as you need after having changed the CurrentSelection to a different sheet with CurrentController.insertTransferable(trans). After every insertion you can also filter again, now probably with filter criteria depending on the target.
I hardly can believe that this should be too slow, In my experiments it was rather fast, and for additional acceleration you should probably disable temporarily AutoCalc, UndoManager, an visibility.

Clearly workarounds avoiding the use of getTransferable / insertTransferable are complicated and unsafe due to lacking knowledge of internals. The developers who created the XTransferable interface surely knew better.

Thanks @Lupp

Attached is an example that filters the objects into a ShapeCollection and then copies the ShapeCollection to Sheet2.

This example works when you run the macro from the IDE or when you click the button. However, I had to set the button’s “Take focus on Click” to false in order to get it working. As you can see, the macro is linked to the Listbox > Changed event and it always fails when you trigger it from the Listbox. It appears that the Controller cannot properly select the ShapeCollection if any ControlShape has focus.

Can someone tell me how to get it to work when it gets called from a control object that has focus?
Copy Sheet Objects - Transferable Example.ods (128.9 KB)

I’m not sure I understand the purpose of these lines:


	Dim origSelec as Object
	origSelec = cCtrl.getSelection()

	'GetTransferable doesn't always seem to work so we
	'allow up to 5 tries to copy the shapes
    For e = 1 To 5

  		If ICnt = 8 Then ' only copy if trans is valid, it fails if anything other than 8

Especially the last line. If there are not 8, but 16 - what will this change?

I tried to remove the focus from Listbox, but I got only not so clever idea :frowning:

    For e = 1 To 5
    	doc.CurrentController.ActiveSheet=sheet2
    	wait 10
    	doc.CurrentController.ActiveSheet=sheet1
    	cCtrl.select(oShapes)

So the secondary For loop was really a failed attempt to only switch sheets when the GetTransferable actually had a valid copy of the objects.

Attached is another example that works when called from a control shape that has focus. It switches sheets to remove focus from the control shape, then it copies the objects to the other sheet. Thank You @KamilLanda
Copy Sheet Objects - Transferable Example-2.0.ods (128.8 KB)

Sorry, I don’t know how to do it with Basic, but this Python code clone all images in source sheet to target sheet.

import uno
import io
import unohelper
from com.sun.star.io import IOException, XOutputStream
from com.sun.star.beans import PropertyValue


CTX = uno.getComponentContext()
SM = CTX.getServiceManager()


class LOShape():

    def __init__(self, shape):
        self._shape = shape

    class OutputStream(unohelper.Base, XOutputStream):

        def __init__(self):
            self._buffer = b''
            self.closed = 0

        @property
        def buffer(self):
            return self._buffer

        def closeOutput(self):
            self.closed = 1

        def writeBytes(self, seq):
            if seq.value:
                self._buffer = seq.value

        def flush(self):
            pass

    def clone_image(self, doc, to_sheet):
        stream = self._shape.GraphicStream
        buffer = self.OutputStream()
        size, data = stream.readBytes(buffer, stream.available())

        stream = SM.createInstanceWithContext('com.sun.star.io.SequenceInputStream', CTX)
        stream.initialize((data,))

        image = doc.createInstance('com.sun.star.drawing.GraphicObjectShape')
        gp = SM.createInstance('com.sun.star.graphic.GraphicProvider')
        properties = (PropertyValue(Name='InputStream', Value=stream),)
        image.Graphic = gp.queryGraphic(properties)
        to_sheet.DrawPage.add(image)
        image.Size = self._shape.Size
        image.Position = self._shape.Position
        return

    def clone_shape(self, doc, to_sheet):
        for p in self._shape.CustomShapeGeometry:
            if p.Name == 'Type':
                type_shape = p.Value.title()

        service = f'com.sun.star.drawing.{type_shape}Shape'
        shape = doc.createInstance(service)
        shape.Size = self._shape.Size
        shape.Position = self._shape.Position
        to_sheet.DrawPage.add(shape)
        return


def main():
    IMAGE = 'com.sun.star.drawing.GraphicObjectShape'
    SHAPE = 'com.sun.star.drawing.CustomShape'

    doc = XSCRIPTCONTEXT.getDocument()
    source = doc.CurrentController.ActiveSheet
    target = doc.Sheets[1]

    for shape in source.DrawPage:
        s = LOShape(shape)
        if shape.ShapeType == IMAGE:
            s.clone_image(doc, target)
        elif shape.ShapeType == SHAPE:
            s.clone_shape(doc, target)
    return

salida

Thank you for this python example. It works perfectly for Images. Now I just need to figure out how to do the same thing for CustomShape objects as well.