Index ¦ Archives ¦ RSS

TextBox: complex LibreOffice Writer content inside shapes

Estimated read time: 5 minutes

TL;DR: see above — it’s now possible to have complex Writer content (charts, tracked changes, tables, fields, etc.) inside drawinglayer shapes, yay! :-)

The problem

Writer in LibreOffice 4.3 can have two kind of shapes: drawinglayer ones or Writer TextFrames. (Let’s ignore OLE objects and Writer pictures for now.) Drawinglayer shapes can be triangles (non-rectangular), rectangles can have rounded corners and so on, but shape text is handled by editeng — the same engine that is used for Impress shapes or Calc cells. OTOH a Writer TextFrame can contain anything that is supported by Writer (Writer fields, styles, tables, etc.), but its drawing capabilities are quite limited: no triangle, rounded corners, etc. Together with CloudOn, we thought the best would be to be able to have both, and started to use the "shape with TextBox" term for this feature.

A user can already sort of to do this by creating a drawinglayer shape, then a Writer TextFrame, and by setting the properties of the Writer TextFrame (position, size, etc) to appear as if the TextFrame would be the shape text of the drawinglayer shape. The idea is to tie these two objects together, so the (UI and API) user sees them as a single object.

Results

I’m providing here a few screenshots. Above, you can see an ODF document having a rectangle with rounded corners, still containing a table.

Given that OOXML has this feature since its birth, I’m also showing a few DOCX documents, which are now handled far better:

  • chart inside a left arrow callout:

  • tracked changes inside a cloud callout:

  • SmartArt inside a snip diagonal corner rectangle:

  • Table of Contents inside a pentagon:

Details

What follows is something you can probably skip if you’re a user — however if you’re a developer and you want to understand how the above is implemented, then read on. ;-)

Situation in 4.3

From the drawinglayer point of view: SwDoc contains an SdrModel (SwDoc::GetOrCreateDrawModel()), which contains a single SdrPage (SdrModel::GetPage()) — Draw/Impress contain multiple sdr pages. The SdrPage contains the shapes: e.g. a triangle is an SdrObjCustomShape. For TextFrames, a placeholder object called SwVirtFlyDrawObj is added to the draw page.

The writer-specific properties of an SdrObject is stored as an SwFrmFmt object, an SwFrmFmt array is a member of SwDoc ("frame format table"). The anchor position and the node index of the frame contents counts as a property.

At UNO level, a single DrawPage object is part of the Component (opened document), which abstracts away the internal SdrPage.

For TextFrames, the UNO API works exactly the same way, except that the implementation stores all properties of the TextFrame in the SwFrmFmt (and some properties are different, compared to a drawinglayer shape).

One remaining detail is how the shape text is represented. In case of drawinglayer shapes, this is provided by editeng: internally an EditTextObject provides a container for paragraphs, at UNO API level SvxUnoTextContent provides an interface that presents paragraphs and their text portions.

For TextFrames, the contents of the frames is stored in a special section in the Writer text node array (in the 3rd toplevel section, while the 5th toplevel section is used for body text), that’s how it can contain anything that’s a valid Writer body text. An offset into this node array of the "content" property of the SwFrmFmt.

Document model

At a document model level, we need a way to describe that an SdrObject (provided by svx) has an associated TextFrame (provided by sw). svx can’t depend on sw, but in the SwFrmFmt of the SdrObject, we can use the so far unused RES_CNTNT ("content") property to point to a TextFrame content.

So behind the scenes the UNO API and the UI does the following when turning on the TextBox bit for a drawinglayer shape:

  • creates a TextFrame

  • connects the SdrObject to the TextFrame

Also, every property of the TextFrame depends on the properties of the SdrObject, think of the followings:

  • position / size is the largest rectangle that fits inside the shape

  • borders are disabled

  • background is transparent

Finding the largest rectangle that fits inside the shape is probably the most interesting here, it’s implemented in SwTextBoxHelper::getTextRectangle(), which uses SdrObjCustomShape::GetTextBounds().

UNO API

The UNO API hides the detail that the TextFrame and the SdrObject are in fact two objects. To get there, the followings are done:

  • SwXShape is modified, so that in the TextBox case not editengine, but the attached TextFrame is accessed when getText() is invoked. This was a bit tricky, as SwXShape doesn’t have an explicit getText() implementation: it overrides queryInterface() instead (see SwTextBoxHelper::queryInterface()).

  • SwXDrawPage (its XEnumerationAccess and XIndexAccess) is modified to ignore TextFrames in the TextBox case

  • SwXTextPortionEnumeration is modified to ignore TextFrames in the TextBox case

  • SwXText::insertTextContent() and SwXText::appendTextContent() is modified to handle the TextBox case

Layout

This was the easiest part: the "merge TextFrame and SdrObj into a shape with TextBox" approach ensured that that we use existing layout features here, no major effort was necessary here.

One interesting detail here was the positioning of as-character anchored shapes having TextBoxes, that’s now handled in SwFlyCntPortion::SetBase().

Filters

The primary point of this feature is to improve Word (and in particular DOCX) compatibility, and of course I wanted to update ODF as necessary as well.

Regarding the new feature, I did the followings:

  • DOCX import now avoids setting service name from original to css.text.TextFrame in case shape has shape text

  • DOCX export now handles the TextBox case: reads Writer text instead of editeng text as necessary

  • ODF export now adds a new optional boolean attribute to make export of the TextBox case possible

  • ODF import now handles the new attribute and act accordingly

Note that regarding backwards compatibility, we keep supporting editengine-based text as well. This has the best of two worlds:

  • existing ODF documents are unchanged, but

  • the TextBox feature is enabled unconditionally in DOCX import to avoid formatting loss

User Interface

I took care of the followings:

  • the context menu of shapes now provides an item to add / remove a TextBox to/from a shape

  • when moving or resizing a shape, the TextBox properties are updated as well

  • when the shape is deleted, the associated TextBox is also deleted

  • editing individual TextBox properties is no longer possible, since they depend on the shape properties

Summary

If you want to try these out yourself, get a daily build and play with it! If something goes wrong, report it to us in the Bugzilla, so we can try fix it before 4.4 gets branched off. Last, but not at least, thanks for CloudOn for funding these improvements! :-)

© Miklos Vajna. Built using Pelican. Theme by Giulio Fidente on github.