Create a slideshow outline in Impress from 'divider slide' text

hi all
with a bit of help from ChatGPT, I created this script to insert an Outline slide in my presentation, using the text from my ‘Divider’ slides as the outline sections. This is for longer slideshows (eg lecture slides) where you have lots of slides in each section, and you don’t want headings in your outline for every single slide.
for this to work, you need to have a master slide called ‘Content’ and another master slide called ‘Divider’. The outline slide has hyperlinks to the corresponding Divider slides. (A further refinement would be to add ‘back to outline’ links on the divider slides!)
hope this is useful for others. I note that the debug output gets sent to a temp file (/tmp/outline_debug.log). If anyone has a better way of getting debug output from a python uno script, please let me know! I tested this in LO 24.2 on Ubuntu 24.04.
cheers
JP

# ~/.config/libreoffice/4/user/Scripts/python/OutlineFromDividers.py
import uno, os
from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK

LOG = "/tmp/outline_debug.log"

def log(line: str) -> None:
    with open(LOG, "a", encoding="utf‑8") as f:
        f.write(line + "\n")

def tag_and_outline_dividers():
    # fresh log
    try:
        os.remove(LOG)
    except FileNotFoundError:
        pass
    log("—— Outline macro run ——")

    doc      = XSCRIPTCONTEXT.getDocument()
    pages    = doc.getDrawPages()
    masters  = doc.getMasterPages()
    base_url = doc.URL or ""

    # 1. look for a master page called "Divider"
    divider = next((m for m in masters if m.Name == "Divider"), None)
    if not divider:
        log("❌  No “Divider” master – aborting.")
        return
    log("✅  Found “Divider” master.")

    # 2. collect every slide that already uses the Divider master
    entries = []
    for i in range(pages.getCount()):
        slide = pages.getByIndex(i)
        try:
            if slide.getMasterPage().Name == "Divider":
                title = next(
                    (shp.String for shp in slide
                     if shp.supportsService("com.sun.star.presentation.TitleTextShape")),
                    None)
                if title:
                    title = " ".join(title.splitlines())
                    log(f"   ▸ Divider title: {title} (currently slide {i+1})")
                    entries.append((i + 1, title))
        except Exception:
            pass
    log(f"🔍  Found {len(entries)} divider slide(s).")
    if not entries:
        log("Nothing to outline – aborting.")
        return

    # 3. choose a layout for the outline slide
    preferred = ["Title, Content", "Content", "Title"]
    tc = None
    for name in preferred:
        tc = next((m for m in masters if m.Name == name), None)
        if tc:
            log(f"✅  Using “{name}” layout for the outline slide.")
            break
    if not tc:
        log("⚠️  No suitable named layout – outline will be a blank slide.")

    # 4. create the outline slide (insert at index 0 so it becomes slide 1)
    outline = pages.insertNewByIndex(0)

    # attach the “Content” master page found earlier
    if tc:
        outline.setMasterPage(tc)

    # ── find a numeric layout ID that already gives Title+Content ──────────
    def first_title_content_layout():
        for i in range(1, pages.getCount()):           # skip the new outline itself
            s = pages.getByIndex(i)
            has_title   = any(
                shp.supportsService("com.sun.star.presentation.PlaceholderShape")
                and shp.PlaceholderType == 1
                for shp in s)
            has_content = any(
                shp.supportsService("com.sun.star.presentation.PlaceholderShape")
                and shp.PlaceholderType == 2
                for shp in s)
            if has_title and has_content:
                return s.Layout                        # <- short integer
        return None

    layout_id = 2 #first_title_content_layout()
    if layout_id is not None:
        try:
            outline.Layout = layout_id
            log(f"✅  Applied existing Title+Content layout id {layout_id}.")
        except Exception as e:
            log(f"⚠️  Could not apply layout id {layout_id}: {e}")
    else:
        log("⚠️  No existing Title+Content slide found – will create placeholders.")

    # 5. TITLE placeholder – look for TitleTextShape
    title_shape = next(
        (shp for shp in outline
         if shp.supportsService("com.sun.star.presentation.TitleTextShape")),
        None)
    if title_shape is None:
        # (extremely unlikely once Layout=2 is applied, but keep a fallback)
        title_shape = doc.createInstance("com.sun.star.drawing.TextShape")
        title_shape.setSize(uno.createUnoStruct("com.sun.star.awt.Size", 15000, 1500))
        title_shape.setPosition(uno.createUnoStruct("com.sun.star.awt.Point", 1000, 200))
        outline.add(title_shape)
    title_shape.Text.setString("Outline")

    # 6. CONTENT placeholder – look for OutlinerShape
    content = next(
        (shp for shp in outline
         if shp.supportsService("com.sun.star.presentation.OutlinerShape")),
        None)
    if content is None:
        log("ℹ️  No OutlinerShape found – creating a TextShape.")
        content = doc.createInstance("com.sun.star.drawing.TextShape")
        content.setSize(uno.createUnoStruct("com.sun.star.awt.Size", 15000, 10000))
        content.setPosition(uno.createUnoStruct("com.sun.star.awt.Point", 1000, 1800))
        outline.add(content)

    # 7. bump every stored slide number by +1 (outline slide sits at the front)
    entries = [(num + 1, title) for num, title in entries]

    # 8. populate bullets + internal links
    text, cursor = content.Text, content.Text.createTextCursor()
    for num, title in entries:
        url_field = doc.createInstance("com.sun.star.text.TextField.URL")
        url_field.URL            = f"#Slide {num}"
        url_field.Representation = f"• {title}"
        text.insertTextContent(cursor, url_field, False)
        text.insertControlCharacter(
            cursor,
            uno.getConstantByName(
                "com.sun.star.text.ControlCharacter.PARAGRAPH_BREAK"),
            False)

    log("✅  Populated outline slide.")

g_exportedScripts = tag_and_outline_dividers,

# vim: sw=4:et

Hallo

maybe you should not reinvent the »logging« -module?!

https://ask.libreoffice.org/t/how-to-catch-libreoffice-macro-errors-and-forward-to-a-python-log-file/70023

1 Like

the suggestion to use ‘import logging’ is a good one, thanks! i would actually, though, really like to know how to easily report errors to the user from a script like this. logging is great during debugging, but what if my script fails, eg due to the required Master Slide names not being present? I couldn’t find any easy way to open a MsgBox or whatever from the Python library.

30 seconds via google libreoffice python msgbox

feel free to add it to Macros for LibreOffice Impress - The Document Foundation Wiki

1 Like

thanks for the suggestion. I found that ‘scriptforge’ seems to be well supported since at least LO 7.2. It can easily be installed via sudo apt install python3-scriptforge and then allows easy scripts with MsgBox in Python, as well as lots of other (what looks like) very useful features.

import uno
from scriptforge import CreateScriptService
bas = CreateScriptService("Basic")
def popup():
    bas.MsgBox('Display this text in a message box from a Python script')
g_exportedScripts = popup,
# vim: sw=4:et