Chapter 16. Making Slides

The Make Slides example creates a deck of five slides, illustrating different aspects of slide generation:

  • Slide 1. A slide combining a title and subtitle (see Fig. 142);

  • Slide 2. A slide with a title, bullet points, and an image (see Fig. 143);

  • Slide 3. A slide with a title, and an embedded video which plays automatically when that slide appears during a slide show (see Fig. 145);

  • Slide 4. A slide with an ellipse and a rounded rectangle acting as buttons. During a slide show, clicking on the ellipse starts a video playing in an external viewer. Clicking on the rounded rectangle causes the slide show to jump to the first slide in the deck (see Fig. 146);

  • Slide 5. This slide contains eight shapes generated using dispatches, including special symbols, block arrows, 3D shapes, flowchart elements, callouts, and stars (see Fig. 151).

make_slides.py creates a slide deck, adds the five slides to it, and finishes by asking if you want to close the document.:

# in make_slides.py
def main(self) -> None:
    loader = Lo.load_office(Lo.ConnectPipe())

    try:
        doc = ImpressDoc(Draw.create_impress_doc(loader))
        curr_slide = doc.get_slide(idx=0)

        doc.set_visible()
        Lo.delay(1_000)  # delay to make sure zoom takes
        doc.zoom(ZoomKind.ENTIRE_PAGE)

        curr_slide.title_slide(
            title="Python-Generated Slides",
            sub_title="Using LibreOffice",
        )

        # second slide
        curr_slide = doc.add_slide()
        self._do_bullets(curr_slide=curr_slide)

        # third slide: title and video
        curr_slide = doc.add_slide()
        curr_slide.title_only_slide("Clock Video")
        curr_slide.draw_media(fnm=self._fnm_clock, x=20, y=70, width=50, height=50)

        # fourth slide
        curr_slide = doc.add_slide()
        self._button_shapes(curr_slide=curr_slide)

        # fifth slide
        if DrawDispatcher:
            # windows only
            # a bit slow due to gui interaction but a good demo
            self._dispatch_shapes(doc)

        Lo.print(f"Total no. of slides: {doc.get_slides_count()}")

        Lo.delay(2000)
        msg_result = MsgBox.msgbox(
            "Do you wish to close document?",
            "All done",
            boxtype=MessageBoxType.QUERYBOX,
            buttons=MessageBoxButtonsEnum.BUTTONS_YES_NO,
        )
        if msg_result == MessageBoxResultsEnum.YES:
            doc.close_doc()
            Lo.close_office()
        else:
            print("Keeping document open")

    except Exception:
        Lo.close_office()
        raise

The five slides are explained in the following sections.

16.1 The First Slide (Title and Subtitle)

Draw.create_impress_doc() calls Lo.create_doc(), supplying it with the Impress document string type:

# in Draw class
@staticmethod
def create_impress_doc(loader: XComponentLoader) -> XComponent:
    return Lo.create_doc(doc_type=Lo.DocTypeStr.IMPRESS, loader=loader)

This creates a new slide deck with one slide whose layout depends on Impress’ default settings. Fig. 140 shows the usual layout when a user starts Impress.

The Default New Slide in Impress

Fig. 140 :The Default New Slide in Impress.

The slide contains two empty presentation shapes – the text rectangle at the top is a TitleTextShape, and the larger rectangle below is a SubTitleShape.

This first slide, which is at index position 0 in the deck, can be referred to by calling Draw.get_slide():

doc = ImpressDoc(Draw.create_impress_doc(loader))
curr_slide = doc.get_slide(idx=0)

This is the same method used to get the first page in a Draw document, so we won’t go through it again. The XDrawPage object can be examined by calling Draw.show_shapes_info() which lists all the shapes (both draw and presentation ones) on the slide:

# in Draw class (simplified)
@classmethod
def show_shapes_info(cls, slide: XDrawPage) -> None:
    print("Draw Page shapes:")
    shapes = cls.get_shapes(slide)
    for shape in shapes:
        cls.show_shape_info(shape)

@classmethod
def show_shape_info(cls, shape: XShape) -> None:
    print(f"  Shape service: {shape.getShapeType()}; z-order: {cls.get_zorder(shape)}")

@staticmethod
def get_zorder(shape: XShape) -> int:
    return int(Props.get(shape, "ZOrder"))

Draw.show_shapes_info() output for the first slide is:

Draw Page shapes:
  Shape service: com.sun.star.presentation.TitleTextShape; z-order: 0
  Shape service: com.sun.star.presentation.SubtitleShape; z-order: 1

Obviously, the default layout sometimes isn’t the one we want. One solution would be to delete the unnecessary shapes on the slide, then add the shapes that we do want. A better approach is the programming equivalent of selecting a different slide layout.

This is implemented as several Draw methods, called Draw.title_slide(), Draw.bullets_slide(), Draw.title_only_slide(), and Draw.blank_slide(), which change the slide’s layout to those shown in Fig. 141.

Slide Layout Methods

Fig. 141 :Slide Layout Methods.

A title/subtitle layout is used for the first slide by calling:

The Title and Subtitle Slide.

Fig. 142 :The Title and Subtitle Slide.

Having a Draw.title_slide() method may seem a bit silly since we’ve seen that the first slide already uses this layout (e.g. in Fig. 140). That’s true for the Impress setup, but may not be the case for other installations with different configurations.

The other layouts shown on the right of Fig. 140 could also be implemented as Draw methods, but the four in Fig. 141 seem most useful. They set the Layout property in the DrawPage service in the com.sun.star.presentation module (not the one in the drawing module).

The documentation for DrawPage (use lodoc DrawPage presentation service) only says that Layout stores a short; it doesn’t list the possible values or how they correspond to layouts.

For this reason OooDev has PresentationLayoutKind which is used as the basis of the layout constants in the Draw class.

Draw.title_slide() starts by setting the slide’s Layout property to PresentationLayoutKind.TITLE_SUB:

# in Draw class (simplified)
@classmethod
def title_slide(cls, slide: XDrawPage, title: str, sub_title: str = "") -> None:

    Props.set(slide, Layout=PresentationLayoutKind.TITLE_SUB.value)

    xs = cls.find_shape_by_type(slide=slide, shape_type=DrawingNameSpaceKind.TITLE_TEXT)
    txt_field = Lo.qi(XText, xs, True)
    txt_field.setString(title)

    if sub_title:
        xs = cls.find_shape_by_type(slide=slide, shape_type=DrawingNameSpaceKind.SUBTITLE_TEXT)
        txt_field = Lo.qi(XText, xs, True)
        txt_field.setString(sub_title)

This changes the slide’s layout to an empty TitleTextShape and SubtitleShape. The functions adds title and subtitle strings to these shapes, and returns. The tricky part is obtaining a reference to a particular shape so it can be modified.

One (bad) solution is to use the index ordering of the shapes on the slide, which is displayed by Draw.show_shapes_info(). It turns out that TitleTextShape is first (i.e. at index 0), and SubtitleShape second. This can be used to write the following code:

x_shapes = curr_slide.qi(XShapes, True)

title_shape = Lo.qi(XShape, x_shapes.getByIndex(0))
sub_title_shape = Lo.qi(XShape, x_shapes.getByIndex(1))

This is a bit hacky, so Draw.find_shape_by_type() is coded instead, which searches for a shape based on its type:

# in Draw class (simplified)
@classmethod
def find_shape_by_type(cls, slide: XDrawPage, shape_type: DrawingNameSpaceKind | str) -> XShape:

    shapes = cls.get_shapes(slide)
    if not shapes:
        raise ShapeMissingError("No shapes were found in the draw page")

    st = str(shape_type)

    for shape in shapes:
        if st == shape.getShapeType():
            return shape
    raise ShapeMissingError(f'No shape found for "{st}"')

OooDev has DrawingNameSpaceKind to lookup shape type names.

This allows for finding the title shape by calling:

xs = Draw.find_shape_by_type(curr_slide, DrawingNameSpaceKind.TITLE_TEXT)

16.2 The Second Slide (Title, Bullets, and Image)

The second slide uses a title and bullet points layout, with an image added at the bottom right corner. The relevant lines in make_slides.py are:

# in main() in make_slides.py
# second slide
curr_slide = doc.add_slide()
self._do_bullets(curr_slide=curr_slide)

The result shown in Fig. 143.

A Slide with a Title, Bullet Points, and an Image.

Fig. 143 :A Slide with a Title, Bullet Points, and an Image.

Fig. 143 slide is created by _do_bullets() in make_slides.py:

# in main() in make_slides.py
def _do_bullets(self, curr_slide: ImpressPage[ImpressDoc]) -> None:
    # second slide: bullets and image
    body = curr_slide.bullets_slide(title="What is an Algorithm?")

    # bullet levels are 0, 1, 2,...
    body.add_bullet(
        level=0,
        text="An algorithm is a finite set of unambiguous instructions for solving a problem.",
    )

    body.add_bullet(
        level=1,
        text="An algorithm is correct if on all legitimate inputs, it outputs the right answer in a finite amount of time",
    )

    body.add_bullet(level=0, text="Can be expressed as")
    body.add_bullet(level=1, text="pseudocode")
    body.add_bullet(level=0, text="flow charts")
    body.add_bullet(
        level=1,
        text="text in a natural language (e.g. English)",
    )
    body.add_bullet(level=1, text="computer code")
    # add the image in bottom right corner, and scaled if necessary
    im = curr_slide.draw_image_offset(
        fnm=self._fnm_img,
        xoffset=ImageOffset(0.6),
        yoffset=ImageOffset(0.5),
    )
    # move below the slide text
    im.move_to_bottom()

Draw.bullets_slide() works in a similar way to Draw.title_slide() – first the slide’s layout is set, then the presentation shapes are found and modified:

# in Draw class (simplified)
@classmethod
def bullets_slide(cls, slide: XDrawPage, title: str) -> XText:

    Props.set(slide, Layout=PresentationLayoutKind.TITLE_BULLETS.value)

    xs = cls.find_shape_by_type(slide=slide, shape_type=DrawingNameSpaceKind.TITLE_TEXT)
    txt_field = Lo.qi(XText, xs, True)
    txt_field.setString(title)

    xs = cls.find_shape_by_type(slide=slide, shape_type=DrawingNameSpaceKind.BULLETS_TEXT)
    return Lo.qi(XText, xs, True)

The PresentationLayoutKind.TITLE_BULLETS enum changes the slide’s layout to contain two presentation shapes – a TitleTextShape at the top, and an OutlinerShape beneath it (as in the second picture in Fig. 141). Draw.bullets_slide() calls Draw.find_shape_by_type() twice to find these shapes, but it does nothing to the OutlinerShape itself, returning it as an XText reference. This allows text to be inserted into the shape by other code (i.e. by Draw.add_bullet()).

16.2.1 Adding Bullets to a Text Area

Draw.add_bullet() converts the shape’s XText reference into an XTextRange, which offers a setString() method:

# in Draw class (simplified)
@staticmethod
def add_bullet(bulls_txt: XText, level: int, text: str) -> None:

    bulls_txt_end = Lo.qi(XTextRange, bulls_txt, True).getEnd()
    Props.set(bulls_txt_end, NumberingLevel=level)
    bulls_txt_end.setString(f"{text}\n")

As explained Chapter 5. Text API Overview, XTextRange is part of the TextRange service which inherits both paragraph and character property classes, as indicated by Fig. 144.

The Text Range Service.

Fig. 144 :The TextRange Service.

A look through the ParagraphProperties documentation reveals a NumberingLevel property which affects the displayed bullet level.

Another way of finding out about the properties associated with XTextRange is to use Props.show_obj_props() to list all of them:

Props.show_obj_props("TextRange in OutlinerShape", tr)

The bullet text is added with XTextRange.setString(). A newline is added to the text before the set, to ensure that the string is treated as a complete paragraph. The drawback is that the newline causes an extra bullet symbol to be drawn after the real bullet points. This can be seen in Fig. 143, at the bottom of the slide. (Principal Skinner is pointing at it.)

16.2.2 Offsetting an Image

The Animate Bike example in Chapter 14. Animation employed a version of Draw.draw_image() based around specifying an (x, y) position on the page and a width and height for the image frame. Draw.draw_image_offset() used here is a variant which specifies its position in terms of fractional offsets from the top-left corner of the slide.

from ooodev.office.draw import Draw, ImageOffset

im = curr_slide.draw_image_offset(
    fnm=self._fnm_img,
    xoffset=ImageOffset(0.6),
    yoffset=ImageOffset(0.5),
)

The last two arguments mean that the image’s top-left corner will be placed at a point that is 0.6 of the slide’s width across and 0.5 of its height down. draw_image_offset() also scales the image so that it doesn’t extend beyond the right and bottom edges of the slide. The scaling is the same along both dimensions so the picture isn’t distorted.

ImageOffset ensure that offsets are not out of range.

The code for Draw.draw_image_offset():

# in Draw class (simplified)
@classmethod
def draw_image_offset(
    cls, slide: XDrawPage, fnm: PathOrStr, xoffset: ImageOffset, yoffset: ImageOffset
) -> XShape:

    slide_size = cls.get_slide_size(slide)
    x = round(slide_size.Width * xoffset.Value)  # in mm units
    y = round(slide_size.Height * yoffset.Value)

    max_width = slide_size.Width - x
    max_height = slide_size.Height - y

    im_size = ImagesLo.calc_scale(fnm=fnm, max_width=max_width, max_height=max_height)
    if im_size is None:
        Lo.print(f'Unable to calc image size for "{fnm}"')
        return None
    return cls.draw_image(
        slide=slide, fnm=fnm, x=x, y=y, width=im_size.Width, height=im_size.Height
    )

draw_image_offset() uses the slide’s size to determine an (x, y) position for the image, and its width and height. ImagesLo.calc_scale() calculates the best width and height for the image frame such that the image will be drawn entirely on the slide:

# in ImagesLo class
@classmethod
def calc_scale(cls, fnm: PathOrStr, max_width: int, max_height: int) -> Size | None:
    im_size = cls.get_size_100mm(fnm)  # in 1/100 mm units
    if im_size is None:
        return None

    width_scale = (max_width * 100) / im_size.Width
    height_scale = (max_height * 100) / im_size.Height

    scale_factor = min(width_scale, height_scale)

    w = round(im_size.Width * scale_factor / 100)
    h = round(im_size.Height * scale_factor / 100)
    return Size(w, h)

calc_scale() uses ImagesLo.get_size100mm() to retrieve the size of the image in 1/100 mm units, and then a scale factor is calculated for both the width and height. This is used to set the image frame’s dimensions when the graphic is loaded by draw_image().

16.3 The Third Slide (Title and Video)

The third slide consists of a title shape and a video frame, which looks like Fig. 145.

A Slide Containing a Video Frame.

Fig. 145 :A Slide Containing a Video Frame.

When this slide appears in a slide show, the video will automatically start playing.

The code for generating this slide is:

# in MakeSlide.main() of make_slides.py
# third slide: title and video
curr_slide = doc.add_slide()
curr_slide.title_only_slide("Clock Video")
curr_slide.draw_media(
    fnm=self._fnm_clock, x=20, y=70, width=50, height=50
)

Draw.title_only_slide() works in a similar way to Draw.title_slide() and Draw.bullets_slide():

# in Draw class (simplified)
@classmethod
def title_only_slide(cls, slide: XDrawPage, header: str) -> None:

    Props.set(slide, Layout=PresentationLayoutKind.TITLE_ONLY.value)

    xs = cls.find_shape_by_type(slide=slide, shape_type=DrawingNameSpaceKind.TITLE_TEXT)
    txt_field = Lo.qi(XText, xs, True)
    txt_field.setString(header)

The MediaShape service doesn’t appear in the Office documentation. Perhaps one reason for its absence is that the shape behaves a little ‘erratically’. Although make_slides.py successfully builds a slide deck containing the video. When the deck is run as a slide show, the video frame is sometimes incorrectly placed, although the video plays correctly.

Draw.draw_media() is defined as:

# in Draw class (simplified)
@classmethod
def draw_media(
    cls, slide: XDrawPage, fnm: PathOrStr, x: int, y: int, width: int, height: int
) -> XShape:

    shape = cls.add_shape(
        slide=slide, shape_type=DrawingShapeKind.MEDIA_SHAPE, x=x, y=y, width=width, height=height
    )

    Lo.print(f'Loading media: "{fnm}"')
    cls.set_shape_props(shape, Loop=True, MediaURL=mFileIO.FileIO.fnm_to_url(fnm))

In the absence of documentation, Props.show_obj_props() can be used to list the properties for the MediaShape:

Props.show_obj_props("Shape", shape)

The MediaURL property requires a file in URL format, and Loop is a boolean for making the animation play repeatedly.

16.4 The Fourth Slide (Title and Buttons)

The fourth slide has two ‘buttons’ – an ellipse which starts a video playing in an external application, and a rounded rectangle which makes the presentation jump to the first slide. These actions are both implemented using the OnClick property for presentation shapes. Fig. 146 shows how the slide looks.

A Slide with Two Buttons

Fig. 146 :A Slide with Two ‘Buttons’.

The relevant code in main() of make_slides.py is:

curr_slide = doc.add_slide()
self._button_shapes(curr_slide=curr_slide)

This button approach to playing a video doesn’t suffer from the strange behavior when using MediaShape on the third slide.

The _button_shapes() method in make_slides.py creates the slide:

def _button_shapes(self, curr_slide: ImpressPage[ImpressDoc]) -> None:
    # fourth slide: title and rectangle (button) for playing a video
    # and a rounded button back to start
    curr_slide.title_only_slide("Wildlife Video Via Button")

    # button in the center of the slide
    sz = curr_slide.get_size_mm()
    width = 80
    height = 40

    ellipse = curr_slide.draw_ellipse(
        x=round((sz.Width - width) / 2),
        y=round((sz.Height - height) / 2),
        width=width,
        height=height,
    )

    ellipse.add_text(msg="Start Video", font_size=30)
    ellipse.set_property(
        OnClick=ClickAction.DOCUMENT, Bookmark=FileIO.fnm_to_url(self._fnm_wildlife)
    )
    # set Animation
    ellipse.set_property(
        Effect=AnimationEffect.MOVE_FROM_LEFT, Speed=AnimationSpeed.FAST
    )

    # draw a rounded rectangle with text
    button = curr_slide.draw_rectangle(
        x=sz.Width - width - 4,
        y=sz.Height - height - 5,
        width=width,
        height=height,
    )
    button.add_text(msg="Click to go\nto slide 1")
    button.set_gradient_color(name=DrawingGradientKind.SUNSHINE)
    # clicking makes the presentation jump to first slide
    button.set_property(CornerRadius=300, OnClick=ClickAction.FIRSTPAGE)

A minor point of interest is that a rounded rectangle is a RectangleShape, but with its CornerRadius property set.

The more important part of the method is the two uses of the OnClick property from the presentation Shape class.

Clicking on the ellipse executes the video file that was passed into the constructor of MakeSlides in make_slides.py. This requires OnClick to be assigned the ClickAction.DOCUMENT constant, and Bookmark to refer to the file as an URL.

Clicking on the rounded rectangle causes the slide show to jump back to the first page. This needs OnClick to be set to ClickAction.FIRSTPAGE.

Several other forms of click action are listed in Table 6.

Table 6 ClickAction Effects.

ClickAction

Name Effect

NONE

No action is performed on the click. Animation and fade effects are also switched off.

PREVPAGE

The presentation jumps to the previous page.

NEXTPAGE

The presentation jumps to the next page.

FIRSTPAGE

The presentation continues with the first page.

LASTPAGE

The presentation continues with the last page.

BOOKMARK

The presentation jumps to a bookmark.

DOCUMENT

The presentation jumps to another document.

INVISIBLE

The object renders itself invisible after a click.

SOUND

A sound is played after a click.

VERB

An OLE verb is performed on this object.

VANISH

The object vanishes with its effect.

PROGRAM

Another program is executed after a click.

MACRO

An Office macro is executed after the click.

Table 6 shows that it’s possible to jump to various places in a slide show, and also execute macros and external programs. In both cases, the Bookmark property is used to specify the URL of the macro or program. For example, the following will invoke Windows’ calculator when the button is pressed:

Props.set(
    button,
    OnClick=ClickAction.PROGRAM,
    Bookmark=FileIO.fnm_to_url(f'(System.getenv("SystemRoot")}\\System32\\calc.exe')
    )

Bookmark requires an absolute path to the application, converted to URL form.

Clicking on the ClickAction takes you to a table very like the one in Table 6.

16.5 Shape Animation

Shape animations are performed during a slide show, and are regulated through three presentation Shape properties: Effect, Speed and TextEffect.

Effect can be assigned a large range of animation effects, which are defined as constants in the AnimationEffect enumeration.

Details can be found in the com.sun.star.presentation module. Another nice summary, in the form of a large table, is in the Developer’s Guide. Fig. 147 shows part of that table.

Animation Effect Constants Table in the Developer's Guide.

Fig. 147 :AnimationEffect Constants Table in the Developer’s Guide.

There are two broad groups of effects: those that move a shape onto the slide when the page appears, and fade effects that make a shape gradually appear in a given spot.

The following code fragment makes the ellipse on the fourth slide slide into view, starting from the left of the slide:

# in _button_shapes() in make_slides.py
ellipse.set_property(
    Effect=AnimationEffect.MOVE_FROM_LEFT, Speed=AnimationSpeed.FAST
)

The animation speed takes a AnimationSpeed value and can be set to AnimationSpeed.SLOW, AnimationSpeed.MEDIUM, or AnimationSpeed.FAST.

Unfortunately, there seems to be an issue with some of the Animation Effects as shown in Fig. 148, Fig. 149, and Fig. 150. When some of the effects are set they actually work in reverse. At least this is the case on Windows 10 and LibreOffice 7.3 There seemed to be issues with most of the fade effects. Not all effects were tested due to the volume of effects. There may be more effects of different types not working correctly.

:Animation Effect FADE FROM LOWER RIGHT workS in reverse

Fig. 148 :AnimationEffect.FADE_FROM_LOWERRIGHT reversed

:Animation Effect FADE FROM BOTTOM workS in reverse

Fig. 149 :AnimationEffect.FADE_FROM_BOTTOM reversed

The developer tools of LibreOffice can be used to confirm that Effect property is actually being set correctly as shown in Fig. 150. Developer tools are available in LibreOffice 7.3 +.

:Animation Effect FADE FROM TOP workS in reverse, developer tools view

Fig. 150 :AnimationEffect.FADE_TOP_BOTTOM reversed developer tool view.

More Complex Shape Animations

If you browse chapter 9 of the Impress user’s guide on slide shows, its animation capabilities extend well beyond the constants in AnimationEffect. These features are available through the XAnimationNode interface, which is obtained like so:

from com.sun.star.animations import XAnimationNode
from ooodev.loader.lo import Lo

node_supp = Lo.qi(XAnimationNodeSupplier, slide)
slide_node = node_supp.getAnimationNode()  # XAnimationNode

XAnimationNode allows a programmer much finer control over animation timings and animation paths for shapes. XAnimationNode is part of the large com.sun.star.animations package.

16.6 The Fifth Slide (Various Dispatch Shapes)

The fifth slide is a hacky, slow solution for generating the numerous shapes in Impress’ GUI which have no corresponding classes in the API. The approach uses dispatch commands, OooDev GUI Automation for windows, and Class RobotKeys (first described back in 4.6 Robot Keys).

The resulting slide is shown in Fig. 151.

Shapes Created by Dispatch Commands.

Fig. 151 :Shapes Created by Dispatch Commands.

The shapes in Fig. 151 are just a few of the many available via Impress’ “Drawing Toolbar”, shown in Fig. 152. The relevant menus are labeled and their sub-menus are shown beneath the toolbar.

The Shapes Available from the Drawing Toolbar

Fig. 152 :The Shapes Available from the Drawing Toolbar.

Each sub-menu shape has a name which appears in a tooltip when the cursor is placed over the shape’s icon. This text turns out to be very useful when writing the dispatch commands.

There’s also a “3D-Objects” toolbar which offers the shapes in Fig. 153.

The 3D Objects Toolbar

Fig. 153 :The 3D-Objects Toolbar.

Some of these 3D shapes are available in the API as undocumented Shape subclasses, but it was unable to programmatically resize the shapes to make them visible. The only way possible to get them to appear at a reasonable size was by creating them with dispatch commands.

Although there’s no mention of these custom and 3D shapes in the Developer’s Guide, their dispatch commands do appear in the UICommands.ods spreadsheet (available from https://arielch.fedorapeople.org/devel/ooo/UICommands.ods). They’re also mentioned, in less detail, in the online documentation for Impress dispatches at https://wiki.documentfoundation.org/Development/DispatchCommands#Impress_slots_.28sdslots.29

It’s quite easy to match up the tooltip names in the GUI with the dispatch names. For example, the smiley face in the Symbol shapes menu is called “Smiley Face” in the GUI and .uno:SymbolShapes.smiley in the UICommands spreadsheet.

make_slides.py generates the eight shapes shown in Fig. 151 by calling _dispatch_shapes():

# in make_slides.py
def _dispatch_shapes(self, doc: ImpressDoc) -> None:
    curr_slide = doc.add_slide()
    curr_slide.title_only_slide("Dispatched Shapes")

    doc.set_visible()
    Lo.delay(1_000)

    doc.goto_page(page=curr_slide.component)
    Lo.print(
        f"Viewing Slide number: {Draw.get_slide_number(Draw.get_viewed_page(doc.component))}"
    )

    # first row
    y = 38
    _ = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.BASIC_SHAPES_DIAMOND,
        x=20,
        y=y,
        width=50,
        height=30,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    _ = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.THREE_D_HALF_SPHERE,
        x=80,
        y=y,
        width=50,
        height=30,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.CALLOUT_SHAPES_CLOUD_CALLOUT,
        x=140,
        y=y,
        width=50,
        height=30,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape.set_bitmap_color(name=DrawingBitmapKind.LITTLE_CLOUDS)

    dispatch_shape = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.FLOW_CHART_SHAPES_FLOWCHART_CARD,
        x=200,
        y=y,
        width=50,
        height=30,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape.set_hatch_color(name=DrawingHatchingKind.BLUE_NEG_45_DEGREES)
    # convert blue to black manually
    dispatch_hatch = cast(Hatch, dispatch_shape.get_property("FillHatch"))
    dispatch_hatch.Color = CommonColor.BLACK
    dispatch_shape.set_property(
        LineColor=CommonColor.BLACK, FillHatch=dispatch_hatch
    )
    # Props.show_obj_props("Hatch Shape", dispatch_shape)

    # second row
    y = 100
    dispatch_shape = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.STAR_SHAPES_STAR_12,
        x=20,
        y=y,
        width=40,
        height=40,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape.set_gradient_color(name=DrawingGradientKind.SUNSHINE)
    dispatch_shape.set_property(LineStyle=LineStyle.NONE)

    dispatch_shape = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.SYMBOL_SHAPES_HEART,
        x=80,
        y=y,
        width=40,
        height=40,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape.set_property(FillColor=CommonColor.RED)

    _ = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.ARROW_SHAPES_LEFT_RIGHT_ARROW,
        x=140,
        y=y,
        width=50,
        height=30,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape = curr_slide.add_dispatch_shape(
        shape_dispatch=ShapeDispatchKind.THREE_D_CYRAMID,
        x=200,
        y=y - 20,
        width=50,
        height=50,
        fn=DrawDispatcher.create_dispatch_shape,
    )
    dispatch_shape.set_bitmap_color(name=DrawingBitmapKind.STONE)

    Draw.show_shapes_info(curr_slide.component)

A title-only slide is created, followed by eight calls to Draw.add_dispatch_shape() to create two rows of four shapes in Fig. 151.

Note that Draw.add_dispatch_shape() take a fn parameter. This is basically a call back function. fn is expected to be a function that takes a XDrawPage and str as input parameters and returns XShape or None.

The reason for this is OooDev is not responsible for automating Windows GUI however, OooDev GUI Automation for windows is. OooDev GUI Automation for windows provides odevgui_win.draw_dispatcher.DrawDispatcher.create_dispatch_shape() that handles automating mouse movements and returns the shape. So, add_dispatch_shape() is passed as call back function.

16.6.1 Viewing the Fifth Slide

Draw.add_dispatch_shape() requires the fifth slide to be the active, visible window on- screen. This necessitates a call to GUI.set_visible() to make the document visible, but that isn’t quite enough. Making the document visible causes the first slide to be displayed, not the fifth one.

Impress offers many ways of viewing slides, which are implemented in the API as view classes that inherit the Controller service. The inheritance structure is shown in Fig. 154.

Impress View Classes.

Fig. 154 :Impress View Classes.

When a Draw or Impress document is being edited, the view is DrawingDocumentDrawView, which supports a number of useful properties, such as ZoomType and VisibleArea. Its XDrawView interface is employed for getting and setting the current page displayed in this view.

Draw.goto_page() gets the XController interface for the document, and converts it to XDrawView so the visible page can be set:

# in Draw class (simplified)
@classmethod
def goto_page(cls, doc: XComponent, page: XDrawPage) -> None:
    try:
        ctl = GUI.get_current_controller(doc)
        cls.goto_page(ctl, page)
    except DrawError:
        raise
    except Exception as e:
        raise DrawError("Error while trying to go to page") from e

@staticmethod
def goto_page(ctl: XController, page: XDrawPage) -> None:
    try:
        xdraw_view = Lo.qi(XDrawView, ctl)
        xdraw_view.setCurrentPage(page)
    except Exception as e:
        raise DrawError("Error while trying to go to page") from e

See also

After the call to Draw.goto_page(), the specified draw page will be visible on-screen, and so receive any dispatch commands.

Draw.get_viewed_page() returns a reference to the currently viewed page by calling XDrawView.getCurrentPage():

# in Draw class
@staticmethod
def get_viewed_page(doc: XComponent) -> XDrawPage:
    try:
        ctl = GUI.get_current_controller(doc)
        xdraw_view = Lo.qi(XDrawView, ctl, True)
        return xdraw_view.getCurrentPage()
    except Exception as e:
        raise DrawPageError("Error geting Viewed page") from e

16.6.2 Adding a Dispatch Shape to the Visible Page

If you try adding a smiley face to a slide inside Impress, it’s a two-step process. It isn’t enough only to click on the icon, it’s also necessary to drag the cursor over the page in order for the shape to appear and be resized.

These steps are necessary for all the Drawing toolbar and 3D-Objects shapes, and are emulated by my code. The programming equivalent of clicking on the icon is done by calling Lo.dispatch_cmd(), while implementing a mouse drag utilizes OooDev GUI Automation for windows and Class RobotKeys.

Draw.add_dispatch_shape() uses Draw.create_dispatch_shape() to create the shape, and then positions and resizes it:

# in Draw class
@classmethod
def add_dispatch_shape(
    cls, slide: XDrawPage, shape_dispatch: ShapeDispatchKind | str,
    x: int, y: int, width: int, height: int, fn: DispatchShape
) -> XShape:
    cls.warns_position(slide, x, y)
    try:
        shape = fn(slide, str(shape_dispatch))
        if shape is None:
            raise NoneError(f'Failed to add shape for dispatch command "{shape_dispatch}"')
        cls.set_position(shape=shape, x=x, y=y)
        cls.set_size(shape=shape, width=width, height=height)
        return shape
    except NoneError:
        raise
    except Exception as e:
        raise ShapeError(
            f'Error occured adding dispatch shape for dispatch command "{shape_dispatch}"'
        ) from e