Estimated read time: 5 minutes
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 whengetText()
is invoked. This was a bit tricky, asSwXShape
doesn’t have an explicitgetText()
implementation: it overridesqueryInterface()
instead (seeSwTextBoxHelper::queryInterface()
). -
SwXDrawPage
(itsXEnumerationAccess
andXIndexAccess
) is modified to ignore TextFrames in the TextBox case -
SwXTextPortionEnumeration
is modified to ignore TextFrames in the TextBox case -
SwXText::insertTextContent()
andSwXText::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! :-)