Chapter 18. Slide Shows

This chapter focuses on ways to programmatically control slide shows. If you’re unfamiliar with what Impress offers in this regard, then chapter 9 of the Impress user guide gives a good overview.

Creating and controlling slide shows employs properties in the Presentation service, and methods in the XSlideShowController interface (see Fig. 161).

The Slide Show Presentation Services

Fig. 161 :The Slide Show Presentation Services.

Two elements of slide shows not shown in Fig. 161 are slide transition effects (i.e. have the next slide fade into view, replacing the current one), and shape animation effects (i.e. have some text whoosh in from the bottom of the screen). These effects are mostly controlled by setting properties – transition properties are in the com.sun.star.presentation.DrawPage service, animations properties in com.sun.star.presentation.Shape.

18.1 Starting a Slide Show

The basic_show.py example shows how a program can start a slide show, and then let the user progress through the presentation by clicking on a slide, pressing the space bar, or using the arrow keys.

While the slide show is running, basic_show.py suspends, but wakes up when the user exits the show. This can occur when he presses the ESC key, or clicks on the slide show’s “click to exit” screen. basic_show.py then closes the document and shuts down Office.

The basic_show.py module:

# basic_show.py module.
from __future__ import annotations

import uno
from ooodev.draw import Draw, ImpressDoc
from ooodev.utils.dispatch.draw_view_dispatch import DrawViewDispatch
from ooodev.utils.file_io import FileIO
from ooodev.loader.lo import Lo
from ooodev.utils.props import Props
from ooodev.utils.type_var import PathOrStr


class BasicShow:
    def __init__(self, fnm: PathOrStr) -> None:
        _ = FileIO.is_exist_file(fnm=fnm, raise_err=True)
        self._fnm = FileIO.get_absolute_path(fnm)

    def main(self) -> None:
        with Lo.Loader(Lo.ConnectPipe()) as loader:
            doc = ImpressDoc(Lo.open_doc(fnm=self._fnm, loader=loader))
            try:
                # slideshow start() crashes if the doc is not visible
                doc.set_visible()

                show = doc.get_show()
                Props.show_obj_props("Slide show", show)

                Lo.delay(500)
                Lo.dispatch_cmd(DrawViewDispatch.PRESENTATION)
                # show.start() starts slideshow but not necessarily in 100% full screen
                # show.start()

                sc = doc.get_show_controller()
                Draw.wait_ended(sc)

            finally:
                doc.close_doc()

The document is opened in the normal way and a slide show object created by calling doc.get_show() that invokes Draw.get_show(), which is defined as:

# in the Draw class
@staticmethod
def get_show(doc: XComponent) -> XPresentation2:
    try:
        ps = Lo.qi(XPresentationSupplier, doc, True)
        return Lo.qi(XPresentation2, ps.getPresentation(), True)
    except Exception as e:
        raise DrawError("Unable to get Presentation") from e

The call to Props.show_obj_props() in main() prints the properties associated with the slide show, most of which are defined in the Presentation service (see Fig. 161):

Output:
Slide show Properties
  AllowAnimations: True
  CustomShow:
  Display: 0
  FirstPage:
  IsAlwaysOnTop: False
  IsAutomatic: False
  IsEndless: False
  IsFullScreen: True
  IsMouseVisible: False
  IsShowAll: True
  IsShowLogo: False
  IsTransitionOnClick: True
  Pause: 0
  StartWithNavigator: False
  UsePen: False

The default values for these properties are sufficient for most presentations.

The slide show can be started by calling XPresentation.show(), However; this can start the presentation with the toolbars still showing. For this reason starting with dispatch command (Lo.dispatch_cmd(DrawViewDispatch.PRESENTATION)) seemed the best option. Although the call returns immediately, it may be a few 100 milliseconds before the presentation appears on screen. If you have more than one monitor, one of them will be allocated a “Presenter Console” window.

This short period while the slide show initializes can cause a problem if the XSlideShowController instance is requested too quickly – None will be returned if the slide show hasn’t finished being created. Draw.get_show_controller() handles this issue by waiting:

# in the Draw class
@staticmethod
def get_show_controller(show: XPresentation2) -> XSlideShowController:
    try:
        sc = show.getController()
        # may return None if executed too quickly after start of show
        if sc is not None:
            return sc
        timeout = 5.0  # wait time in seconds
        try_sleep = 0.5
        end_time = time.time() + timeout
        while end_time > time.time():
            time.sleep(try_sleep)  # give slide show time to start
            sc = show.getController()
            if sc is not None:
                break
    except Exception as e:
        raise DrawError("Error getting slide show controller") from e
    if sc is None:
        raise DrawError(f"Could obtain slide show controller after {timeout:.1f} seconds")
    return sc

Draw.get_show_controller() tries to obtain the controller for 5 seconds before giving up and raising DrawError.

The XSlideShowController interface gives the programmer much greater control over the slide show, including the ability to change the slide being displayed, and monitor and control the slide show state. Two topics that are not covered here are how XSlideShowController can assign listeners to the slide show (of type XSlideShowListener), and how to utilize the XSlideShow interface.

Back in basic_show.py, the main() function suspends by calling Draw.wait_ended(); the idea is that the program will sleep until the human presenter ends the slide show. wait_ended() is implemented using XSlideShowController:

# in the Draw Class
@staticmethod
def wait_ended(sc: XSlideShowController) -> None:
    while True:
        curr_index = sc.getCurrentSlideIndex()
        if curr_index == -1:
            break
        Lo.delay(500)

    Lo.print("End of presentation detected")

XSlideShowController.getCurrentSlideIndex() normally returns a slide index (i.e. 0 or greater), but when the slide show has finished it returns -1. wait_ended() keeps polling for this value, sleeping for half a second between each test.

18.2 Play and End a Slide Show Automatically

The auto_show.py example removes the need for a presenter to click on a slide to progress to the next one, and terminates the show itself after the last slide had been displayed:

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

    try:
        doc = ImpressDoc(Lo.open_doc(self._fnm, loader))

        # slideshow start() crashes if the doc is not visible
        doc.set_visible()

        # set up a fast automatic change between all the slides
        slides = doc.get_slides_list()
        for slide in slides:
            slide.set_transition(
                fade_effect=self._fade_effect,
                speed=AnimationSpeed.FAST,
                change=DrawingSlideShowKind.AUTO_CHANGE,
                duration=self._duration,
            )

        show = doc.get_show()
        Props.show_obj_props("Slide Show", show)
        self._set_show_prop(show)
        # Props.set(show, IsEndless=True, Pause=0)

        Lo.delay(500)
        Lo.dispatch_cmd(DrawViewDispatch.PRESENTATION)
        # show.start() starts slideshow but not necessarily in 100% full screen

        sc = doc.get_show_controller()
        Draw.wait_last(sc=sc, delay=self._end_delay)
        Lo.dispatch_cmd(DrawViewDispatch.PRESENTATION_END)
        Lo.delay(500)

        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:
            print("Ending the slide show")
            doc.close_doc()
            Lo.close_office()
        else:
            print("Keeping document open")
    except Exception:
        Lo.close_office()
        raise

Automatic Slide Transitioning

The automated transition between slides is configured by calling slide.set_transition() that invokes Draw.set_transition() on every slide in the deck:

# in AutoShow.main() of auto_show.py
slide.set_transition(
    fade_effect=self._fade_effect,
    speed=AnimationSpeed.FAST,
    change=DrawingSlideShowKind.AUTO_CHANGE,
    duration=self._duration,
)

Draw.set_transition() combines the setting of four slide properties: Effect, Speed, Change, and Duration:

# in Draw class
@staticmethod
def set_transition(
    slide: XDrawPage,
    fade_effect: FadeEffect,
    speed: AnimationSpeed,
    change: DrawingSlideShowKind,
    duration: int,
) -> None:
    try:
        ps = Lo.qi(XPropertySet, slide, True)
        ps.setPropertyValue("Effect", fade_effect)
        ps.setPropertyValue("Speed", speed)
        ps.setPropertyValue("Change", int(change))
        # if change is SlideShowKind.AUTO_CHANGE
        # then Duration is used
        ps.setPropertyValue("Duration", abs(duration))  # in seconds
    except Exception as e:
        raise DrawPageError("Could not set slide transition") from e

Slide transition properties (such as Effect, Speed, Change, and Duration) are defined in the com.sun.star.presentation.DrawPage service. However, the possible values for Effect are stored in an enumeration listed at the end of the com.sun.star.presentation module Fig. 162 shows the FadeEffect enum.

The FadeEffect Enum

Fig. 162 :The FadeEffect Enum.

The Speed property of AnimationSpeed is used to set the speed of a slide transition. There are three possible settings: SLOW, MEDIUM, and FAST.

The Change property specifies how a transition is triggered. The property can take one of three integer values, which aren’t defined by LibreOffice as an enum so OooDev defines them as DrawingSlideShowKind.

The default behavior is represented by 0 (DrawingSlideShowKind.CLICK_ALL_CHANGE) which requires the presenter to click on a slide to change it, and a click is also need to trigger any shape animations on the page. A value of 2 (DrawingSlideShowKind.CLICK_PAGE_CHANGE) relieves the presenter from clicking to trigger shape animations, but he still needs to activate a slide transition manually. auto_show.py a passes DrawingSlideShowKind.AUTO_CHANGE to Draw.set_transition() which causes all the animations and transitions to execute automatically.

The Duration property is specified in seconds and refers to how long the current slide stays on display before the transition effect begins. This is different from the Speed property which refers to how quickly a transition is performed.

Finishing Automatically

The other aspect of this automated slide show is making it stop when the last slide has been displayed. This is implemented by Draw.wait_last():

# in Draw class
@staticmethod
def wait_last(sc: XSlideShowController, delay: int) -> None:
    wait = int(delay)
    num_slides = sc.getSlideCount()
    while True:
        curr_index = sc.getCurrentSlideIndex()
        if curr_index == -1:
            break
        if curr_index >= num_slides - 1:
            break
        Lo.delay(500)

    if wait > 0:
        Lo.delay(wait)

wait_last() keeps checking the current slide index and sleeps until the last slide in the deck is reached. It then goes to sleep one last time, to give the final slide time to be seen by the user.

18.3 Play a Slide Show Repeatedly

Another common kind of automated slide show is one that plays the show repeatedly, only terminating when the presenter steps in and presses the ESC key. This only requires a few lines to be changed in auto_show.py, shown in below:

# in auto_show.py
# ...
show = doc.get_show()
Props.show_obj_props("Slide Show", show)
self._set_show_prop(show)
show.start()

sc = doc.get_show_controller()
Draw.wait_ended(sc)
# ...

def _set_show_prop(self, show: XPresentation2) -> None:
    Props.set(show, IsEndless=self._is_endless, Pause=self._pause)

The IsEndless property turns on slide show cycling, and Pause indicates how long the black “Click to exit” screen is displayed before the show restarts.

Draw.wait_ended() is the same as before – it makes auto_show.py suspend until the user clicks on the exit screen or presses the ESC key.

18.4 Play a Custom Slide Show

A custom slide show is a display sequence other than the usual one that starts with the first slide and moves linearly through to the last. A named ‘play list’ of pages must be created, consisting of references to slides in the deck. The list can point to the slides in any order, and may reference a slide more than once.

Draw.build_play_list() creates the named play list using three arguments: the slide document, an array of slide indices which represents the intended playing sequence, and a name for the list. For example:

play_list = Draw.build_play_list(doc, "ShortPlay", 5, 6, 7, 8)  # XNameContainer

This creates a play list called “ShortPlay” which will show the slides with indices 5, 6, 7, and 8 (note: the first slide has index 0). Draw.build_play_list() is used in the custom_show.py example:

# custom_show.py module
from __future__ import annotations

import uno
from ooodev.dialog.msgbox import (
    MsgBox,
    MessageBoxType,
    MessageBoxButtonsEnum,
    MessageBoxResultsEnum,
)
from ooodev.draw import Draw, ImpressDoc
from ooodev.utils.dispatch.draw_view_dispatch import DrawViewDispatch
from ooodev.utils.file_io import FileIO
from ooodev.loader.lo import Lo
from ooodev.utils.props import Props
from ooodev.utils.type_var import PathOrStr


class CustomShow:
    def __init__(self, fnm: PathOrStr, *slide_idx: int) -> None:
        FileIO.is_exist_file(fnm=fnm, raise_err=True)
        self._fnm = FileIO.get_absolute_path(fnm)
        for idx in slide_idx:
            if idx < 0:
                raise IndexError("Index cannot be negative")
        self._idxs = slide_idx

    def main(self) -> None:
        loader = Lo.load_office(Lo.ConnectPipe())

        try:
            doc = ImpressDoc(Lo.open_doc(fnm=self._fnm, loader=loader))
            # slideshow start() crashes if the doc is not visible
            doc.set_visible()

            if len(self._idxs) > 0:
                _ = doc.build_play_list("ShortPlay", *self._idxs)
                show = doc.get_show()
                Props.set(show, CustomShow="ShortPlay")
                Props.show_obj_props("Slide show", show)
                Lo.delay(500)
                Lo.dispatch_cmd(DrawViewDispatch.PRESENTATION)
                # show.start() starts slideshow but not necessarily in 100% full screen
                # show.start()
                sc = doc.get_show_controller()
                Draw.wait_ended(sc)

                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")
            else:
                MsgBox.msgbox(
                    "There were no slides indexes to create a slide show.",
                    "No Slide Indexes",
                    boxtype=MessageBoxType.WARNINGBOX,
                )

        except Exception:
            Lo.close_office()
            raise

The play list is installed by setting the CustomShow property in the slide show. The rest of the code in custom_show.py is similar to the basic_show.py example.

Creating a Play List Using Containers

The most confusing part of Draw.build_play_list() is its use of two containers to hold the play list:

# part of the build_play_list in draw class
# ...
# get name container for the slide show
# doc is an XComponent``
play_list = cls.get_play_list(doc)

# get factory from the container
xfactory = Lo.qi(XSingleServiceFactory, play_list, True)

# use factory to make an index container
slides_con = Lo.qi(XIndexContainer, xfactory.createInstance(), True)
# ...

An index container is created by XSingleServiceFactory.createInstance(), which requires a factory instance. This factory is most conveniently obtained from an existing container, namely the one for the slide show. That’s obtained by Draw.get_play_list():

# in the Draw class
@staticmethod
def get_play_list(doc: XComponent) -> XNameContainer:
    try:
        cp_supp = Lo.qi(XCustomPresentationSupplier, doc, True)
        return cp_supp.getCustomPresentations()
    except Exception as e:
        raise DrawError("Error getting play list") from e

Draw.build_play_list() fills the index container with references to the slides, and then places it inside the name container:

# in the Draw class
@classmethod
def build_play_list(cls, doc: XComponent, custom_name: str, *slide_idxs: int) -> XNameContainer:
    play_list = cls.get_play_list(doc)
    try:
        xfactory = Lo.qi(XSingleServiceFactory, play_list, True)
        slides_con = Lo.qi(XIndexContainer, xfactory.createInstance(), True)

        Lo.print("Building play list using:")
        j = 0
        for i in slide_idxs:
            try:
                slide = cls._get_slide_doc(doc, i)
            except IndexError as ex:
                Lo.print(f"  Error getting slide for playlist. Skipping index {i}")
                Lo.print(f"    {ex}")
                continue
            slides_con.insertByIndex(j, slide)
            j += 1
            Lo.print(f"  Slide No. {i+1}, index: {i}")

        play_list.insertByName(custom_name, slides_con)
        Lo.print(f'Play list stored under the name: "{custom_name}"')
        return play_list
    except Exception as e:
        raise DrawError("Unable to build play list.") from e

The for-loop employs the tuple of indices to get references to the slides via Draw.get_slide(). Each reference is added to the index container.