Chapter 7. Text Content Other than Strings

Chapter 5. Text API Overview looked at using text cursors to move around inside text documents, adding or extracting text strings.

That chapter utilized the XText inheritance hierarchy, which is shown again in Fig. 50.

Diagram of XText and its Super-classes

Fig. 50 :XText and its Super-classes.

The documents manipulated in Chapter 5. Text API Overview only contained character-based text, but can be a lot more varied, including text frames, embedded objects, graphics, shapes, text fields, tables, bookmarks, text sections, footnotes and endnotes, and more.

From the XText service its possible to access the XTextContent interface (see Fig. 50), which belongs to the TextContent service. As Fig. 51 indicates, that service is the parent of many sub-classes which represent different kinds of text document content.

Diagram of The TextContent Service and Some Sub-classes.

Fig. 51 :The TextContent Service and Some Sub-classes.

A more complete hierarchy can be found in the documentation for TextContent (lodoc TextContent service).

The two services highlighted relate to graphical content, which is explained in the next chapter.

Table 5 summarizes content types in terms of their services and access methods. Most of the methods are in Supplier interfaces which are part of the GenericTextDocument or OfficeDocument services in Fig. 26.

Table 5 Creating and Accessing Text Content.

Content Name

Service for Creating Content

Access Method in Supplier

Text Frame

TextFrame

XNameAccess XTextFramesSupplier

getTextFrames()

Embedded Object

TextEmbeddedObject

XComponent XTextEmbeddedObjectSupplier2

getEmbeddedObject()

Graphic Object

TextGraphicObject

XNameAccess XTextGraphicObjectsSupplier

getGraphicObjects()

Shape

text.Shape,

drawing.Shape or a subclass

XDrawPage XDrawPageSupplier

getDrawPage()

Text Field

TextField

XEnumerationAccess XTextFieldsSupplier

getTextFields()

Text Table

TextTable

XNameAccess XTextTablesSupplier

getTextTables()

Bookmark

Bookmark

XNameAccess XBookmarksSupplier

getBookmarks()

Paragraph

Paragraph

XEnumerationAccess on XText

Text Section

TextSection

XNameAccess XTextSectionsSupplier

getTextSections()

Footnote

Footnote

XIndexAccess XFootnotesSupplier

getFootnotes()

End Note

Endnote

XIndexAccess XEndnotesSupplier.getEndnotes()

Reference Mark

ReferenceMark

XNameAccess XReferenceMarksSupplier

getReferenceMarks()

Index

DocumentIndex

XIndexAccess XDocumentIndexesSupplier

getDocumentIndexes()

Link Target

LinkTarget

XNameAccess XLinkTargetSupplier.getLinks()

Redline

RedlinePortion

XEnumerationAccess XRedlinesSupplier

getRedlines()

Content Metadata

InContentMetaData

XDocumentMetadataAccess

Graphic Object and Shape are discussed in the next chapter.

7.1 How to Access Text Content

Most of the examples in this chapter create text document content rather than access it. This is mainly because the different access functions work in a similar way, so you don’t need many examples to get the general idea.

First the document is converted into a supplier, then its getXXX() method is called (see column 3 of Table 5). For example, accessing the graphic objects in a document (see row 3 of Table 5) requires:

# get the graphic objects supplier
ims_supplier = Lo.qi(XTextGraphicObjectsSupplier, doc)

# access the graphic objects collection
xname_access = ims_supplier.getGraphicObjects()

The names associated with the graphic objects in XNameAccess can be extracted with XNameAccess.getElementNames(), and printed:

names = xname_access.getElementNames()
print(f"Number of graphic names: {len(names)}")

names.sort() # sort them, if you want
Lo.print_names(names) # useful for printing long lists

A particular object in an XNameAccess collection is retrieved with getByName():

# get graphic object called "foo"
obj_graphic = xname_access.getByName("foo")

A common next step is to convert the object into a property set, which makes it possible to lookup the properties stored in the object’s service. For instance, the graphic object’s filename or URL can be retrieved using:

props =  Lo.qi(XPropertySet, obj_graphic)
fnm = props.getPropertyValue("GraphicURL") # string

The graphic object’s URL is stored in the GraphicURL property from looking at the documentation for the TextGraphicObject service. It can be (almost) directly accessed by typing lodoc TextGraphicObject service.

It’s possible to call setPropertyValue() to change a property:

props.setPropertyValue("Transparency", 50)

What About the Text Content tha is not covered?

Table 5 has many rows without bold entries, which means we won’t be looking at them.

Except for the very brief descriptions here; for more please consult the Developer’s Guide at https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Working_with_Text_Documents (or type loguide Working with Text Documents). All the examples in that section are in TextDocuments.java at https://api.libreoffice.org/examples/DevelopersGuide/examples.html#Text.

Text Sections. A text section is a grouping of paragraphs which can be assigned their own style settings. More usefully, a section may be located in another file, which is the mechanism underlying master documents. See: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Text_Sections (or type loguide Text Sections).

Footnotes and Endnotes. Footnotes and endnotes are blocks of text that appear in the page footers and at the end of a document. They can be treated as XText objects, so manipulated using the same techniques as the main document text. See: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Footnotes_and_Endnotes (or type loguide Footnotes).

Reference Marks. Reference marks can be inserted throughout a document, and then jumped to via GetReference text fields: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Reference_Marks (or type loguide Reference Marks).

Indexes and Index Marks. Index marks, like reference marks, can be inserted anywhere in a document, but are used to generate indices (collections of information) inside the document. There are several types of index marks used for generating lists of chapter headings (i.e. a book’s index), lists of key words, illustrations, tables, and a bibliography. For details see: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Indexes_and_Index_Marks (or type loguide Indexes).

Link Targets. A link target (sometimes called a jump mark) labels a location inside a document. These labels can be included as part of a filename so that the document can be opened at that position. For information, see: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Link_Targets (or type loguide Link Targets).

Redlines. Redlines are the changes recorded when a user edits a document with track changes turned on. Each of the changes is saved as a text fragment (also called a text portion) inside a redline object. See: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Redline (or type loguide Redline).

7.2 Adding a Text Frame to a Document

The TextFrame service inherits many of its properties and interfaces, so its inheritance hierarchy is shown in detail in Fig. 52.

Diagram of The TextFrame Service Hierarchy

Fig. 52 :The TextFrame Service Hierarchy.

Fig. 52 includes two sibling services of TextFrame: TextEmbeddedObject and TextGraphicObject, which is discussed a bit later; in fact, we will only get around to TextGraphicObject in the next chapter.

The BaseFrameProperties service contains most of the frame size and positional properties, such as “Width”, “Height”, and margin and border distance settings.

A TextFrame interface can be converted into a text content (i.e. XTextContent) or a shape (i.e. XShape). Typically, the former is used when adding text to the frame, the latter when manipulating the shape of the frame.

In the Build Doc example, text frame creation is done by Write.add_text_frame(), with Build Doc supplying the frame’s y-axis coordinate position for its anchor:

# code fragment from build doc
from ooodev.format.writer.direct.frame.area import Color as FrameColor
from ooodev.format.writer.direct.frame.borders import Side, Sides, BorderLineKind, LineSize
# ...

Write.append_para(cursor, "Here's some code:")
tvc = Write.get_view_cursor(doc)

tvc = Write.get_view_cursor(doc)
tvc.gotoRange(cursor.getEnd(), False)

ypos = tvc.getPosition().Y

np()
code_font = Font(name=Info.get_font_mono_name(), size=10)
code_font.apply(cursor)

nl("public class Hello")
nl("{")
nl("  public static void main(String args[]")
nl('  {  System.out.println("Hello World");  }')
Write.append_para(cursor, "}  // end of Hello class")

# reset the cursor formatting
ParaStyle.default.apply(cursor)

# Format the background color of the previous paragraph.
bg_color = ParaBgColor(CommonColor.LIGHT_GRAY)
Write.style_prev_paragraph(cursor=cursor, styles=[bg_color])

Write.append_para(cursor, "A text frame")

pg = Write.get_current_page(tvc)

frame_color = FrameColor(CommonColor.DEFAULT_BLUE)
# create a border
bdr_sides= Sides(
    all=Side(line=BorderLineKind.SOLID, color=CommonColor.RED, width=LineSize.THIN)
)

Write.add_text_frame(
    cursor=cursor,
    ypos=ypos,
    text="This is a newly created text frame.\nWhich is over on the right of the page, next to the code.",
    page_num=pg,
    width=UnitMM(40),
    height=UnitMM(15),
    styles=[frame_color, bdr_sides],
)

An anchor specifies how the text content is positioned relative to the ordinary text around it. Anchoring can be relative to a character, paragraph, page, or another frame.

Write.add_text_frame() uses page anchoring, which means that Build Doc must obtain a view cursor, so that an on-screen page position can be calculated. As Fig. 53 shows, the text frame is located on the right of the page, with its top edge level with the start of the code listing.

Screen shot of Text Frame Position in the Document

Fig. 53 :Text Frame Position in the Document.

ooodev.format.writer.direct.frame.type module contains size and position classes such as Anchor class, which is used to specify the frame’s anchor type that can be passed to Write.add_text_frame(). This creates a rich set of options for positioning the frame.

In the code fragment above, Write.get_view_cursor() creates the view cursor, and XTextViewCursor.getPosition() returns its (x, y) coordinate on the page. The y-coordinate is stored in yPos until after the code listing has been inserted into the document, and then passed to Write.add_text_frame().

Write.add_text_frame() is defined as:

@classmethod
def add_text_frame(
    cls,
    *,
    cursor: XTextCursor,
    text: str = "",
    ypos: int | UnitT = 300,
    width: int | UnitT = 5000,
    height: int | UnitT = 5000,
    page_num: int = 1,
    border_color: Color | None = None,
    background_color: Color | None = None,
    styles: Iterable[StyleT] = None,
) -> XTextFrame:

    result = None
    cargs = CancelEventArgs(Write.add_text_frame.__qualname__)
    cargs.event_data = {
        "cursor": cursor,
        "ypos": ypos,
        "text": text,
        "width": width,
        "height": height,
        "page_num": page_num,
        "border_color": border_color,
        "background_color": background_color,
    }
    _Events().trigger(WriteNamedEvent.TEXT_FRAME_ADDING, cargs)
    if cargs.cancel:
        return False

    arg_ypos = cast(Union[int, UnitT], cargs.event_data["ypos"])
    text = cargs.event_data["text"]
    arg_width = cast(Union[int, UnitT], cargs.event_data["width"])
    arg_height = cast(Union[int, UnitT], cargs.event_data["height"])
    page_num = cargs.event_data["page_num"]
    border_color = cargs.event_data["border_color"]
    background_color = cargs.event_data["background_color"]

    try:
        ypos = arg_ypos.get_value_mm100()
    except AttributeError:
        ypos = int(arg_ypos)
    try:
        width = arg_width.get_value_mm100()
    except AttributeError:
        width = int(arg_width)
    try:
        height = arg_height.get_value_mm100()
    except AttributeError:
        height = int(arg_height)

    xframe = mLo.Lo.create_instance_msf(XTextFrame, "com.sun.star.text.TextFrame", raise_err=True)

    try:
        tf_shape = mLo.Lo.qi(XShape, xframe, True)

        # set dimensions of the text frame
        tf_shape.setSize(UnoSize(width, height))

        #  anchor the text frame
        frame_props = mLo.Lo.qi(XPropertySet, xframe, True)
        # if page number is Not include for TextContentAnchorType.AT_PAGE
        # then Lo Default so At AT_PARAGRAPH
        if not page_num or page_num < 1:
            frame_props.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH)
        else:
            frame_props.setPropertyValue("AnchorType", TextContentAnchorType.AT_PAGE)
            frame_props.setPropertyValue("AnchorPageNo", page_num)

        frame_props.setPropertyValue("FrameIsAutomaticHeight", True)  # will grow if necessary

        # add a red border around all 4 sides
        border = BorderLine()
        border.OuterLineWidth = 1
        if border_color is not None:
            border.Color = border_color

        frame_props.setPropertyValue("TopBorder", border)
        frame_props.setPropertyValue("BottomBorder", border)
        frame_props.setPropertyValue("LeftBorder", border)
        frame_props.setPropertyValue("RightBorder", border)

        # make the text frame blue
        if background_color is not None:
            frame_props.setPropertyValue("BackTransparent", False)  # not transparent
            frame_props.setPropertyValue("BackColor", background_color)  # light blue

        # Set the horizontal and vertical position
        frame_props.setPropertyValue("HoriOrient", HoriOrientation.RIGHT)
        frame_props.setPropertyValue("VertOrient", VertOrientation.NONE)
        frame_props.setPropertyValue("VertOrientPosition", ypos)  # down from top

        # insert text frame into document (order is important here)
        cls._append_text_content(cursor, xframe)
        cls.end_paragraph(cursor)

        if text:
            xframe_text = xframe.getText()
            xtext_range = mLo.Lo.qi(XTextRange, xframe_text.createTextCursor(), True)
            xframe_text.insertString(xtext_range, text, False)
            result = xframe

        if styles:
            srv = ("com.sun.star.text.TextFrame", "com.sun.star.text.ChainedTextFrame")
            for style in styles:
                if style.support_service(*srv):
                    style.apply(xframe)

    except Exception as e:
        raise Exception("Insertion of text frame failed:") from e
    _Events().trigger(WriteNamedEvent.TEXT_FRAME_ADDED, EventArgs.from_args(cargs))
    return result

add_text_frame() starts by creating a TextFrame service, and accessing its XTextFrame interface:

xframe = Lo.create_instance_msf(XTextFrame, "com.sun.star.text.TextFrame")

The service name for a text frame is listed as “TextFrame” in row 1 of Table 5, but Lo.create_instance_msf() requires a fully qualified name. Almost all the text content services, including TextFrame, are in the com.sun.star.text package.

The XTextFrame interface is converted into XShape so the frame’s dimensions can be set. The interface is also cast to XPropertySet so that various frame properties can be initialized; these properties are defined in the TextFrame and BaseFrameProperties services (see Fig. 51).

The “AnchorType” property uses the AT_PAGE anchor constant to tie the frame to the page. There are five anchor constants: AT_PARAGRAPH, AT_CHARACTER, AS_CHARACTER, AT_PAGE, and AT_FRAME, which are defined in the TextContentAnchorType enumeration.

The difference between AT_CHARACTER and AS_CHARACTER relates to how the surrounding text is wrapped around the text content. “AS” means that the text content is treated as a single (perhaps very large) character inside the text, while “AT” means that the text frame’s upper-left corner is positioned at that character location.

The frame’s page position is dealt with a few lines later by the HoriOrient and VertOrient properties. The HoriOrientation and VertOrientation constants are a convenient way of positioning a frame at the corners or edges of the page. However, VertOrientPosition is used to set the vertical position using the yPos coordinate, and switch off the VertOrient vertical orientation.

Towards the end of Write.add_text_frame(), the frame is added to the document by calling a version of Write.append() that expects an XTextContent object:

# internal method call by Write.append() when adding text
@classmethod
def _append_text_content(cls, cursor: XTextCursor, text_content: XTextContent) -> None:
    xtext = cursor.getText()
    xtext.insertTextContent(cursor, text_content, False)
    cursor.gotoEnd(False)

It utilizes the XText.insertTextContent() method.

The last task of Write.add_text_frame(), is to insert some text into the frame.

XTextFrame inherits XTextContent, and so has access to the getText() method (see Fig. 52). This means that all the text manipulations possible in a document are also possible inside a frame.

The ordering of the tasks at the end of add_text_frame() is important. Office prefers that an empty text content be added to the document, and the data inserted afterwards.

7.3 Adding a Text Embedded Object to a Document

Text embedded object content support OLE (Microsoft’s Object Linking and Embedding), and is typically used to create a frame linked to an external Office document. Probably, its most popular use is to link to a chart, but we’ll delay looking at that until Chapter 33.

The best way of getting an idea of what OLE objects are available is to go to the Writer application’s Insert menu, Object, “OLE Object” dialog. In my version of Office, it lists Office spreadsheet, chart, drawing, presentation, and formula documents, and a range of Microsoft and PDF types.

Note that text embedded objects aren’t utilized for adding graphics to a document.

That’s easier to do using the TextGraphicObject or GraphicObjectShape services, which is described next.

In this section we look at how to insert mathematical formulae into a text document.

The example code is in Math Questions, but most of the formula embedding is performed by Write.add_formula():

@classmethod
def add_formula(cls, cursor: XTextCursor, formula: str) -> bool:
    cargs = CancelEventArgs(Write.add_formula.__qualname__)
    cargs.event_data = {"cursor": cursor, "formula": formula}
    _Events().trigger(WriteNamedEvent.FORMULA_ADDING, cargs)
    if cargs.cancel:
        return False
    formula = cargs.event_data["formula"]
    embed_content = Lo.create_instance_msf(
        XTextContent, "com.sun.star.text.TextEmbeddedObject", raise_err=True
    )
    try:
        # set class ID for type of object being inserted
        props = Lo.qi(XPropertySet, embed_content, True)
        props.setPropertyValue("CLSID", Lo.CLSID.MATH)
        props.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER)

        # insert object in document
        cls._append_text_content(cursor=cursor, text_content=embed_content)
        cls.end_line(cursor)

        # access object's model
        embed_obj_supplier = Lo.qi(XEmbeddedObjectSupplier2, embed_content, True)
        embed_obj_model = embed_obj_supplier.getEmbeddedObject()

        formula_props = Lo.qi(XPropertySet, embed_obj_model, True)
        formula_props.setPropertyValue("Formula", formula)
        Lo.print(f'Inserted formula "{formula}"')
    except Exception as e:
        raise Exception(f'Insertion fo formula "{formula}" failed:') from e
    _Events().trigger(WriteNamedEvent.FORMULA_ADDED, EventArgs.from_args(cargs))
    return True

A math formula is passed to add_formula() as a string in a format this is explained shortly.

The method begins by creating a TextEmbeddedObject service, and referring to it using the XTextContent interface:

embed_content = Lo.create_instance_msf(
        XTextContent, "com.sun.star.text.TextEmbeddedObject", raise_err=True
    )

Details about embedded objects are given in row 2 of Table 5.

Unlike TextFrame which has an XTextFrame interface, there’s no XTextEmbeddedObject interface for TextEmbeddedObject. This can be confirmed by looking at the TextFrame inheritance hierarchy in Fig. 51. There is an XEmbeddedObjectSuppler, but that’s for accessing objects, not creating them. Instead XTextContent interface is utilized in Lo.create_instance_msf() because it’s the most specific interface available.

The XTextContent interface is converted to XPropertySet so the “CLSID” and “AnchorType” properties can be set. “CLSID” is specific to TextEmbeddedObject – its value is the OLE class ID for the embedded document. The Lo.CLSID contains the class ID constants for Office’s documents.

The “AnchorType” property is set to AS_CHARACTER so the formula string will be anchored in the document in the same way as a string of characters.

As with the text frame in Write.add_text_frame(), an empty text content is added to the document first, then filled with the formula.

The embedded object’s content is accessed via the XEmbeddedObjectSupplier2 interface which has a get method for obtaining the object:

# access object's model
embed_obj_supplier = Lo.qi(XEmbeddedObjectSupplier2, embed_content, True)
embed_obj_model = embed_obj_supplier.getEmbeddedObject()

The properties for this empty object (embed_obj_model) are accessed, and the formula string is assigned to the “Formula” property:

formula_props = Lo.qi(XPropertySet, embed_obj_model, True)
formula_props.setPropertyValue("Formula", formula)

7.3.1 What’s a Formula String?

Although the working of Write.add_formula() has been explained, the format of the formula string that’s passed to it has not been explained. There’s a good overview of the notation in the “Commands Reference” appendix of Office’s “Math Guide”, available at https://libreoffice.org/get-help/documentation For example, the formula string: “1 {5}over{9} + 3 {5}over{9} = 5 {1}over{9}” is rendered as:

\[1 \frac{5}{9} + 3 \frac{5}{9} = 5 \frac{1}{9}\]

7.3.2 Building Formulae

Math Questions is mainly a for-loop for randomly generating numbers and constructing simple formulae strings. Ten formulae are added to the document, which is saved as mathQuestions.pdf. The main() function:

def main() -> int:

    delay = 2_000  # delay so users can see changes.

    with Lo.Loader(Lo.ConnectSocket()) as loader:

        doc = Write.create_doc(loader=loader)

        try:
            GUI.set_visible(is_visible=True, odoc=doc)

            cursor = Write.get_cursor(doc)
            Write.append_para(cursor, "Math Questions")
            Write.style_prev_paragraph(cursor, "Heading 1")

            Write.append_para(cursor, "Solve the following formulae for x:\n")

            # lock screen updating and add formulas
            # locking screen is not strictly necessary but is faster when add lost of input.
            with Lo.ControllerLock():
                for _ in range(10):  # generate 10 random formulae
                    iA = random.randint(0, 7) + 2
                    iB = random.randint(0, 7) + 2
                    iC = random.randint(0, 8) + 1
                    iD = random.randint(0, 7) + 2
                    iE = random.randint(0, 8) + 1
                    iF1 = random.randint(0, 7) + 2

                    choice = random.randint(0, 2)

                    # formulas should be wrapped in {} but for formatting reasons it is easier to work with [] and replace later.
                    if choice == 0:
                        formula = f"[[[sqrt[{iA}x]] over {iB}] + [{iC} over {iD}]=[{iE} over {iF1} ]]"
                    elif choice == 1:
                        formula = f"[[[{iA}x] over {iB}] + [{iC} over {iD}]=[{iE} over {iF1}]]"
                    else:
                        formula = f"[{iA}x + {iB} = {iC}]"

                    # replace [] with {}
                    Write.add_formula(cursor, formula.replace("[", "{").replace("]", "}"))
                    Write.end_paragraph(cursor)

            Write.append_para(cursor, f"Timestamp: {DateUtil.time_stamp()}")

            Lo.delay(delay)
            Lo.save_doc(doc, "mathQuestions.pdf")

        finally:
            Lo.close_doc(doc)

    return 0

Fig. 54 shows a screenshot of part of mathQuestions.pdf.

Screen shot of Math Formulae in a Text Document

Fig. 54 :Math Formulae in a Text Document.

7.4 Text Fields

A text field differs from other text content in that its data is generated dynamically by the document, or by an external source such as a database. Document-generated text fields include text showing the current date, the page number, the total number of pages in the document, and cross-references to other areas in the text. We’ll look at three examples: the DateTime, PageNumber, and PageCount text fields.

When a text field depends on an external source, there are two fields to initialize: the master field representing the external source, and the dependent field for the data used in the document; only the dependent field is visible. Here we won’t be giving any dependent/master field examples, but there’s one in the Development Guide section on text fields, at: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Text_Fields (or type loguide Text Fields).

It utilizes the User master field, which allows the external source to be user-defined data. The code appears in the TextDocuments.java example at https://api.libreoffice.org/examples/DevelopersGuide/examples.html#Text.

Different kinds of text field are implemented as sub-classes of the TextField service. You can see the complete hierarchy in the online documentation for TextField. Fig. 55 presents a simplified version.

Diagram of Simplified Hierarchy for the TextField Service

Fig. 55 :Simplified Hierarchy for the TextField Service.

7.4.1 The DateTime TextField

The Build Doc example ends with a few lines that appear to do the same thing twice:

# code fragment from build doc
Write.append_para(cursor, "\nTimestamp: " + DateUtil.time_stamp() + "\n")
Write.append(cursor, "Time (according to office): ")
Write.append_date_time(cursor=cursor)
Write.end_paragraph(cursor)

DateUtil.time_stamp() inserts a timestamp (which includes the date and time), and then Write.append_date_time() inserts the date and time. Although these may seem to be the same, time_stamp() adds a string while append_date_time() creates a text field. The difference becomes apparent if you open the file some time after it was created.

Fig. 56 shows two screenshots of the time-stamped parts of the document taken after it was first generated, and nearly 50 minutes later.

Screen shot of the document Timestamps.

Fig. 56 :Screenshots of the Timestamps.

The text field timestamp is updated each time the file is opened in edit mode (which is the default in Writer).

This dynamic updating occurs in all text fields. For example, if you add some pages to a document, all the places in the document that use the PageCount text field will be updated to show the new length.

Write.append_date_time() creates a DateTime service, and returns its XTextField interface (see Fig. 55). The TextField service only contains two properties, with most being in the subclass (DateTime in this case).

@classmethod
def append_date_time(cls, cursor: XTextCursor) -> None:
    dt_field = Lo.create_instance_msf(XTextField, "com.sun.star.text.TextField.DateTime")
    Props.set_property(dt_field, "IsDate", True)  # so date is reported
    xtext_content = Lo.qi(XTextContent, dt_field, True)
    cls._append_text_content(cursor, xtext_content)
    cls.append(cursor, "; ")

    dt_field = Lo.create_instance_msf(XTextField, "com.sun.star.text.TextField.DateTime")
    Props.set_property(dt_field, "IsDate", False)  # so time is reported
    xtext_content = Lo.qi(XTextContent, dt_field, True)
    cls._append_text_content(cursor, xtext_content)

The method adds two DateTime text fields to the document. The first has its “IsDate” property set to true, so that the current date is inserted; the second sets “IsDate” to false so the current time is shown.

7.4.2 The PageNumber and PageCount Text Fields

As discussed most of Story Creator in Chapter 6. Text Styles, but skipped over how page numbers were added to the document’s page footer. The footer is shown in Fig. 57.

Screen shot of Page Footer using Text Fields

Fig. 57 :Page Footer using Text Fields.

Write.set_page_numbers() inserts the PageNumber and PageCount text fields into the footer’s text area:

@classmethod
def set_page_numbers(cls, text_doc: XTextDocument) -> None:
    props = Info.get_style_props(doc=text_doc, family_style_name="PageStyles", prop_set_nm="Standard")
    if props is None:
        raise PropertiesError("Could not access the standard page style")

    try:
        props.setPropertyValue("FooterIsOn", True)
        #   footer must be turned on in the document
        footer_text = Lo.qi(XText, props.getPropertyValue("FooterText"), True)
        footer_cursor = footer_text.createTextCursor()

        Props.set_property(
            prop_set=footer_cursor, name="CharFontName", value=Info.get_font_general_name()
        )
        Props.set_property(prop_set=footer_cursor, name="CharHeight", value=12.0)
        Props.set_property(prop_set=footer_cursor, name="ParaAdjust", value=ParagraphAdjust.CENTER)

        # add text fields to the footer
        pg_number = cls.get_page_number()
        pg_xcontent = Lo.qi(XTextContent, pg_number)
        if pg_xcontent is None:
            raise MissingInterfaceError(
                XTextContent, f"Missing interface for page number. {XTextContent.__pyunointerface__}"
            )
        cls._append_text_content(cursor=footer_cursor, text_content=pg_xcontent)
        cls._append_text(cursor=footer_cursor, text=" of ")
        pg_count = cls.get_page_count()
        pg_count_xcontent = Lo.qi(XTextContent, pg_count)
        if pg_count_xcontent is None:
            raise MissingInterfaceError(
                XTextContent, f"Missing interface for page count. {XTextContent.__pyunointerface__}"
            )
        cls._append_text_content(cursor=footer_cursor, text_content=pg_count_xcontent)
    except Exception as e:
        raise Exception("Unable to set page numbers") from e

@staticmethod
def get_page_number() -> XTextField:
    num_field = Lo.create_instance_msf(XTextField, "com.sun.star.text.TextField.PageNumber")
    Props.set_property(prop_set=num_field, name="NumberingType", value=NumberingType.ARABIC)
    Props.set_property(prop_set=num_field, name="SubType", value=PageNumberType.CURRENT)
    return num_field

@staticmethod
def get_page_count() -> XTextField:
    pc_field = Lo.create_instance_msf(XTextField, "com.sun.star.text.TextField.PageCount")
    Props.set_property(prop_set=pc_field, name="NumberingType", value=NumberingType.ARABIC)
    return pc_field

Write.set_page_numbers() starts by accessing the “Standard” property set (style) for the page style family. Via its properties, the method turns on footer functionality and accesses the footer text area as an XText object.

An XTextCursor is created for the footer text area, and properties are configured:

footer_text = Lo.qi(XText, props.getPropertyValue("FooterText"), True)
footer_cursor = footer_text.createTextCursor()
Props.set_property(
    prop_set=footer_cursor, name="CharFontName", value=Info.get_font_general_name()
)

These properties will be applied to the text and text fields added afterwards:

Write.append(footer_cursor, Write.get_page_number())
Wirte.append(footer_cursor, " of ")
Write.append(footer_cursor, Write.get_page_count())

get_page_number() and get_page_count() deal with the properties for the PageNumber and PageCount fields.

7.5 Adding a Text Table to a Document

The Make Table example reads in data about James Bond movies from bondMovies.txt and stores it as a text table in table.odt. The first few rows are shown in Fig. 58.

Screen shot of A Bond Movies Table

Fig. 58 :A Bond Movies Table.

The bondMovies.txt file is read by read_table() utilizing Python file processing with pythons csv.reader. It returns a 2D-list:

# example partial result from read_table()
[
    ["Title",  "Year", "Actor", "Director"],
    ["Dr. No", "1962", "Sean Connery", "Terence Young"],
    ["From Russia with Love", "1963", "Sean Connery", "Terence Young"],
]

Each line in bondMovies.txt is converted into a string array by pulling out the sub-strings delimited by tab characters.

read_table() ignores lines in the file that are know not to be csv lines. First valid row in the list contains the table’s header text.

The first few lines of bondMovies.txt are:

// http://en.wikipedia.org/wiki/James_Bond#Ian_Fleming_novels

Title Year Actor Director

Dr. No 1962 Sean Connery Terence Young
From Russia with Love 1963 Sean Connery Terence Young
Goldfinger 1964 Sean Connery Guy Hamilton
Thunderball 1965 Sean Connery Terence Young
You Only Live Twice 1967 Sean Connery Lewis Gilbert
On Her Majesty's Secret Service 1969 George Lazenby Peter R. Hunt
Diamonds Are Forever 1971 Sean Connery Guy Hamilton
Live and Let Die 1973 Roger Moore Guy Hamilton
The Man with the Golden Gun 1974 Roger Moore Guy Hamilton
The Spy Who Loved Me 1977 Roger Moore Lewis Gilbert
    :

The main() function for Make Table is:

def main() -> int:

    fnm = FileIO.get_absolute_path("../../../../resources/txt/bondMovies.txt")  # source csv file
    if not fnm.exists():
        print("resource image 'bondMovies.txt' not found.")
        print("Unable to continue.")
        return 1

    tbl_data = read_table(fnm)

    delay = 2_000  # delay so users can see changes.

    with Lo.Loader(Lo.ConnectSocket()) as loader:

        doc = Write.create_doc(loader=loader)

        try:
            GUI.set_visible(is_visible=True, odoc=doc)

            cursor = Write.get_cursor(doc)

            Write.append_para(cursor, "Table of Bond Movies")
            Write.style_prev_paragraph(cursor, "Heading 1")
            Write.append_para(cursor, 'The following table comes form "bondMovies.txt"\n')

            # Lock display updating for faster writing of table into document.
            with Lo.ControllerLock():
                Write.add_table(cursor=cursor, table_data=tbl_data)
                Write.end_paragraph(cursor)

            Lo.delay(delay)
            Write.append(cursor, f"Timestamp: {DateUtil.time_stamp()}")
            Lo.delay(delay)
            Lo.save_doc(doc, "table.odt")

        finally:
            Lo.close_doc(doc)

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Write.add_table() does the work of converting the list of rows into a text table.

Fig. 59 shows the hierarchy for the TextTable service: it’s a subclass of TextContent and supports the XTextTable interface.

Diagram of The Text Table Hierarchy

Fig. 59 :The TextTable Hierarchy.

XTextTable contains methods for accessing a table in terms of its rows, columns, and cells. The cells are referred to using names, based on letters for columns and integers for rows, as in Fig. 60.

Screen shot of he Cell Names in a Table

Fig. 60 :The Cell Names in a Table.

Write.add_table() uses this naming scheme in the XTextTable.getCellByName() method to assign data to cells:

@classmethod
def add_table(
    cls,
    cursor: XTextCursor,
    table_data: Table,
    header_bg_color: Color | None = CommonColor.DARK_BLUE,
    header_fg_color: Color | None = CommonColor.WHITE,
    tbl_bg_color: Color | None = CommonColor.LIGHT_BLUE,
    tbl_fg_color: Color | None = CommonColor.BLACK,
    first_row_header: bool = True,
    styles: Iterable[StyleT] = None,
) -> XTextTable:

    cargs = CancelEventArgs(Write.add_table.__qualname__)
    cargs.event_data = {
        "cursor": cursor,
        "table_data": table_data,
        "header_bg_color": header_bg_color,
        "header_fg_color": header_fg_color,
        "tbl_bg_color": tbl_bg_color,
        "tbl_fg_color": tbl_fg_color,
        "first_row_header": first_row_header,
        "styles": styles,
    }
    _Events().trigger(WriteNamedEvent.TABLE_ADDING, cargs)
    if cargs.cancel:
        return False

    header_bg_color = cargs.event_data["header_bg_color"]
    header_fg_color = cargs.event_data["header_fg_color"]
    tbl_bg_color = cargs.event_data["tbl_bg_color"]
    tbl_fg_color = cargs.event_data["tbl_fg_color"]
    first_row_header = cargs.event_data["first_row_header"]

    def make_cell_name(row: int, col: int) -> str:
        return TableHelper.make_cell_name(row=row + 1, col=col + 1)

    def set_cell_header(cell_name: str, data: str, table: XTextTable) -> None:
        cell_text = mLo.Lo.qi(XText, table.getCellByName(cell_name), True)
        if first_row_header and header_fg_color is not None:
            text_cursor = cell_text.createTextCursor()
            mProps.Props.set(text_cursor, CharColor=header_fg_color)

        cell_text.setString(str(data))

    def set_cell_text(cell_name: str, data: str, table: XTextTable) -> None:
        cell_text = mLo.Lo.qi(XText, table.getCellByName(cell_name), True)
        if first_row_header is False or tbl_fg_color is not None:
            text_cursor = cell_text.createTextCursor()
            props = {}
            if not first_row_header:
                # By default the first row has a style by the name of: Table Heading
                # Table Contents is the default for cell that are not in the header row.
                props["ParaStyleName"] = "Table Contents"
            if tbl_fg_color is not None:
                props["CharColor"] = tbl_fg_color
            mProps.Props.set(text_cursor, **props)

        cell_text.setString(str(data))

    num_rows = len(table_data)
    if num_rows == 0:
        raise ValueError("table_data has no values")
    try:
        table = mLo.Lo.create_instance_msf(XTextTable, "com.sun.star.text.TextTable")
        if table is None:
            raise ValueError("Null Value")
    except Exception as e:
        raise mEx.CreateInstanceMsfError(XTextTable, "com.sun.star.text.TextTable")

    try:
        num_cols = len(table_data[0])
        mLo.Lo.print(f"Creating table rows: {num_rows}, cols: {num_cols}")
        table.initialize(num_rows, num_cols)

        # insert the table into the document
        cls._append_text_content(cursor, table)
        cls.end_paragraph(cursor)

        table_props = mLo.Lo.qi(XPropertySet, table, True)

        # set table properties
        if header_bg_color is not None or tbl_bg_color is not None:
            table_props.setPropertyValue("BackTransparent", False)  # not transparent
        if tbl_bg_color is not None:
            table_props.setPropertyValue("BackColor", tbl_bg_color)

        # set color of first row (i.e. the header)
        if first_row_header and header_bg_color is not None:
            rows = table.getRows()
            mProps.Props.set(rows.getByIndex(0), BackColor=header_bg_color)

        #  write table header
        if first_row_header:
            row_data = table_data[0]
            for x in range(num_cols):
                set_cell_header(make_cell_name(0, x), row_data[x], table)
                # e.g. "A1", "B1", "C1", etc

            # insert table body
            for y in range(1, num_rows):  # start in 2nd row
                row_data = table_data[y]
                for x in range(num_cols):
                    set_cell_text(make_cell_name(y, x), row_data[x], table)
        else:
            # insert table body
            for y in range(0, num_rows):  # start in 1st row
                row_data = table_data[y]
                for x in range(num_cols):
                    set_cell_text(make_cell_name(y, x), row_data[x], table)

        if styles:
            srv = ("com.sun.star.text.TextTable",)
            for style in styles:
                if style.support_service(*srv):
                    style.apply(table)
    except Exception as e:
        raise Exception("Table insertion failed:") from e
    _Events().trigger(WriteNamedEvent.TABLE_ADDED, EventArgs.from_args(cargs))
    return table

A TextTable service with an XTextTable interface is created at the start of add_table(). Then the required number of rows and columns is calculated so that XTextTable.initialize() can be called to specify the table’s dimensions.

num_rows = len(table_data)
...

# use the first row to get the number of column
num_cols = len(table_data[0])
Lo.print(f"Creating table rows: {num_rows}, cols: {num_cols}")
table.initialize(num_rows, num_cols)

Table-wide properties are set (properties are listed in the TextTable documentation). Note that if “BackTransparent” isn’t set to false then Office crashes when the program tries to save the document.

The color property of the header row is set to dark blue (CommonColor.DARK_BLUE) by default. This requires a call to XTextTable.getRows() to return an XTableRows object representing all the rows. This object inherits XIndexAccess, so the first row is accessed with index 0.

# set color of first row (i.e. the header)
if header_bg_color is not None:
    rows = table.getRows()
    Props.set_property(prop_set=rows.getByIndex(0), name="BackColor", value=header_bg_color)

The filling of the table with data is performed by two loops. The first deals with adding text to the header row, the second deals with all the other rows.

make_cell_name() converts an (x, y) integer pair into a cell name like those in Fig. 60:

make_cell_name() uses TableHelper methods to make the conversion.

Write.set_cell_header() uses TextTable.getCellByName() to access a cell, which is of type XCell. We’ll study XCell in Part 4: Calc because it’s used for representing cells in a spreadsheet.

The Cell service supports both the XCell and XText interfaces, as in Fig. 61.

Diagram of The Cell Service

Fig. 61 :The Cell Service.

This means that Lo.qi() can convert an XCell instance into XText, which makes the cell’s text and properties accessible to a text cursor. set_cell_header() implements these features:

def set_cell_header(cell_name: str, data: str, table: XTextTable) -> None:
    cell_text = mLo.Lo.qi(XText, table.getCellByName(cell_name), True)
    if first_row_header and header_fg_color is not None:
        text_cursor = cell_text.createTextCursor()
        mProps.Props.set(text_cursor, CharColor=header_fg_color)

    cell_text.setString(str(data))

The cell’s CharColor property is changed so the inserted text in the header row is white (CommonColor.WHITE) by default, as in Fig. 58.

set_cell_text() like set_cell_header() optionally changes the text’s color:

def set_cell_text(cell_name: str, data: str, table: XTextTable) -> None:
    cell_text = mLo.Lo.qi(XText, table.getCellByName(cell_name), True)
    if first_row_header is False or tbl_fg_color is not None:
        text_cursor = cell_text.createTextCursor()
        props = {}
        if not first_row_header:
            # By default the first row has a style by the name of: Table Heading
            # Table Contents is the default for cell that are not in the header row.
            props["ParaStyleName"] = "Table Contents"
        if tbl_fg_color is not None:
            props["CharColor"] = tbl_fg_color
        mProps.Props.set(text_cursor, **props)

    cell_text.setString(str(data))

7.6 Adding a Bookmark to the Document

Write.add_bookmark() adds a named bookmark at the current cursor position:

@classmethod
def add_bookmark(cls, cursor: XTextCursor, name: str) -> None:
    cargs = CancelEventArgs(Write.add_bookmark.__qualname__)
    cargs.event_data = {"cursor": cursor, "name": name}
    _Events().trigger(WriteNamedEvent.BOOKMARK_ADDING, cargs)
    if cargs.cancel:
        return False

    # get name from event args in case it has been changed.
    name = cargs.event_data["name"]

    try:
        bmk_content = Lo.create_instance_msf(XTextContent, "com.sun.star.text.Bookmark")
        if bmk_content is None:
            raise ValueError("Null Value")
    except Exception as e:
        raise CreateInstanceMsfError(XTextContent, "com.sun.star.text.Bookmark") from e
    try:
        bmk_named = Lo.qi(XNamed, bmk_content, True)
        bmk_named.setName(name)

        cls._append_text_content(cursor, bmk_content)
    except Exception as e:
        raise Exception("Unable to add bookmark") from e
    _Events().trigger(WriteNamedEvent.BOOKMARK_ADDED, EventArgs.from_args(cargs))
    return True

The Bookmark service doesn’t have a specific interface (such as XBookmark), so Lo.create_instance_msf() returns an XTextContent interface. These services and interfaces are summarized by Fig. 62.

Diagram of The Bookmark Service and Interfaces

Fig. 62 :The Bookmark Service and Interfaces.

Bookmark supports XNamed, which allows it to be viewed as a named collection of bookmarks (note the plural). This is useful when searching for a bookmark or adding one, as in the Build Doc example. It calls Write.add_bookmark() to add a bookmark called ad-Bookmark to the document:

# code fragment from build doc
append("This line ends with a bookmark.")
Write.add_bookmark(cursor=cursor, name="ad-bookmark")

Bookmarks, such as ad-bookmark, are not rendered when the document is opened, which means that nothing appears after the “The line ends with a bookmark.” string in “build.odt”.

However, bookmarks are listed in Writer’s “Navigator” window (press F5), as in Fig. 63.

Screen shot The Writer Navigator Window

Fig. 63 :The Writer Navigator Window.

Clicking on the bookmark causes Writer to jump to its location in the document.

Using Bookmarks; One programming use of bookmarks is for moving a cursor around a document. Just as with real-world bookmarks, you can add one at some important location in a document and jump to that position at a later time.

Write.find_bookmark() finds a bookmark by name, returning it as an XTextContent instance:

@staticmethod
def find_bookmark(text_doc: XTextDocument, bm_name: str) -> XTextContent | None:
    supplier = Lo.qi(XBookmarksSupplier, text_doc, True)

    named_bookmarks = supplier.getBookmarks()
    obookmark = None

    try:
        obookmark = named_bookmarks.getByName(bm_name)
    except Exception:
        Lo.print(f"Bookmark '{bm_name}' not found")
        return None
    return Lo.qi(XTextContent, obookmark)

find_bookmark() can’t return an XBookmark object since there’s no such interface (see Fig. 62), but XTextContent is a good alternative. XTextContent has a getAnchor() method which returns an XTextRange that can be used for positioning a cursor. The following code fragment from Build Doc illustrates the idea:

# code fragment form build doc
# move view cursor to bookmark position
bookmark = Write.find_bookmark(doc, "ad-bookmark")
bm_range = bookmark.getAnchor()

view_cursor = Write.get_view_cursor(doc)
view_cursor.gotoRange(bm_range, False)

The call to gotoRange() moves the view cursor to the ad-bookmark position, which causes an on-screen change. gotoRange() can be employed with any type of cursor.