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.
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.
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.
Content Name |
Service for Creating Content |
Access Method in Supplier |
---|---|---|
Text Frame |
TextFrame |
|
Embedded Object |
TextEmbeddedObject |
|
Graphic Object |
TextGraphicObject |
|
Shape |
text.Shape, drawing.Shape or a subclass |
|
Text Field |
TextField |
|
Text Table |
TextTable |
|
Bookmark |
Bookmark |
|
Paragraph |
Paragraph |
|
Text Section |
TextSection |
|
Footnote |
Footnote |
|
End Note |
Endnote |
|
Reference Mark |
ReferenceMark |
|
Index |
DocumentIndex |
|
Link Target |
LinkTarget |
|
Redline |
RedlinePortion |
|
Content Metadata |
InContentMetaData |
|
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.
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
from ooodev.write import Write, WriteDoc
# ...
doc = WriteDoc(Write.create_doc(loader=loader))
cursor = doc.get_cursor()
# ...
cursor.append_para("Here's some code:")
tvc = doc.get_view_cursor()
tvc.goto_range(cursor.component.getEnd(), False)
y_pos = tvc.get_position().Y
cursor.end_paragraph()
code_font = Font(name=Info.get_font_mono_name(), size=10)
code_font.apply(cursor.component)
cursor.append_line("public class Hello")
cursor.append_line("{")
cursor.append_line(" public static void main(String args[]")
cursor.append_line(' { System.out.println("Hello World"); }')
cursor.append_para("} // end of Hello class")
# reset the cursor formatting
ParaStyle.default.apply(cursor.component)
# Format the background color of the previous paragraph.
bg_color = ParaBgColor(CommonColor.LIGHT_GRAY)
cursor.style_prev_paragraph(styles=[bg_color])
cursor.append_para("A text frame")
pg = tvc.get_current_page()
frame_color = FrameColor(CommonColor.DEFAULT_BLUE)
# create a border
bdr_sides= Sides(
all=Side(line=BorderLineKind.SOLID, color=CommonColor.RED, width=LineSize.THIN)
)
_ = cursor.add_text_frame(
text="This is a newly created text frame.\nWhich is over on the right of the page, next to the code.",
ypos=y_pos,
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.
WriteTextCursor.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.
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 WriteTextCursor.add_text_frame()
.
This creates a rich set of options for positioning the frame.
In the code fragment above, doc.get_view_cursor()
creates the view cursor,
and tvc.get_position()
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 WriteTextCursor.add_text_frame()
.
Write.add_text_frame()
is defined as:
# in Write Class
@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()
that is invoked when WriteTextCursor.add_formula()
is called:
# in Write Class
@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:
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.
loader = Lo.load_office(Lo.ConnectPipe())
doc = WriteDoc(Write.create_doc(loader=loader))
try:
doc.set_visible()
cursor = doc.get_cursor()
cursor.append_para("Math Questions")
cursor.style_prev_paragraph("Heading 1")
cursor.append_para("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 {}
cursor.add_formula(formula.replace("[", "{").replace("]", "}"))
cursor.end_paragraph()
cursor.append_para(f"Timestamp: {DateUtil.time_stamp()}")
Lo.delay(delay)
doc.save_doc(pth / "mathQuestions.pdf")
doc.close_doc()
Lo.close_office()
except Exception:
Lo.close_office()
raise
return 0
Fig. 54 shows a screenshot of part of mathQuestions.pdf
.
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.
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
cursor.append_para("\nTimestamp: " + DateUtil.time_stamp() + "\n")
cursor.append("Time (according to office): ")
cursor.append_date_time()
cursor.end_paragraph()
DateUtil.time_stamp()
inserts a timestamp (which includes the date and time), and then WriteTextViewCursor.append_date_time()
invokes Write.append_date_time()
which 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.
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).
# in Write Class
@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.
Write.set_page_numbers()
inserts the PageNumber
and PageCount
text fields into the footer’s text area:
# in Write Class
@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())
Write.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.
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 = Path(__file__).parent / "data" / "bondMovies.txt" # source csv file
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.
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.
Write.add_table()
uses this naming scheme in the XTextTable.getCellByName()
method to assign data to cells:
# in Write Class
@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.
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:
# in Write Class
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:
# in Write Class
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:
# in Write Class
@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.
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
cursor.append("This line ends with a bookmark.")
cursor.add_bookmark("ad-bookmark")
cursor.append_line()
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.
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:
# in Write Class
@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 = doc.find_bookmark("ad-bookmark")
bm_range = bookmark.get_anchor()
view_cursor = doc.get_view_cursor()
view_cursor.goto_range(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.