diff --git a/_constraints b/_constraints
index 5217115..1409d7e 100644
--- a/_constraints
+++ b/_constraints
@@ -11,4 +11,14 @@
1200
+
+
+ aarch64
+
+
+
+ asimdrdm
+
+
+
diff --git a/bsc1182969.patch b/bsc1182969.patch
new file mode 100644
index 0000000..19e500c
--- /dev/null
+++ b/bsc1182969.patch
@@ -0,0 +1,6228 @@
+From 0fa895203fc42b506ea0c5e230821cf8f4804c29 Mon Sep 17 00:00:00 2001
+From: Mike Kaganski
+Date: Sun, 6 Jun 2021 21:50:53 +0300
+Subject: [PATCH] Fix and unify the two methods that get scaled text size
+
+GetTextFitToSizeScale and SdrTextObj::GetFontScaleY both didn't
+initialize outliners properly, and thus returned wrong results.
+
+Change-Id: I6fe63c51ed838a0d0fafdfa03597cac97ce29831
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116765
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+(cherry picked from commit a1ae30166e92a0a40dff06740f0bb8e9ee63f70a)
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116704
+Tested-by: Jenkins CollaboraOffice
+
+Unify some code managing coordinates depending on text direction
+
+Change-Id: I12163e83a6a4d4e7cb85eed690b787c47ee2e7e5
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/115107
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116861
+Tested-by: Mike Kaganski
+
+UITest: introduce guarded context managers
+
+They should be used wherever we need to make sure to execute
+some action in tests regardless of the assertion success. They
+follow their plain counterparts, and execute finalizing code
+at all outcomes. This e.g. allows to assert when a dialog is
+open, and have it properly closed on failure, so that the test
+is not left in hung state.
+
+Only two wrappers are implemented now: load_file and
+execute_dialog_through_action.
+
+sc/qa/uitest/chart/chartLegend.py is updated to use them.
+
+Change-Id: Ib9cf44304f0d3ab8fa3377922ed36d2d866031b0
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116692
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116867
+Tested-by: Jenkins CollaboraOffice
+
+editengine-columns: Create document model and dialog page
+
+Change-Id: I056aad9474ca18134d1f1686a53618cc9ab3d525
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116038
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116868
+Tested-by: Jenkins CollaboraOffice
+Reviewed-by: Miklos Vajna
+
+editengine-columns: ODF support [API CHANGE]
+
+This uses existing ODF markup, as used by Writer's text frame:
+style::columns child element of style:graphic-properties, its
+fo:column-count and fo:column-gap attributes. No ODF extension
+is required.
+
+Since currently only columns with same width and spacing are
+implemented, without additional settings, style:column child
+elements are exported, but ignored on import.
+
+This adds new property to css::drawing::TextProperties service:
+TextColumns (of type css::text::XTextColumns).
+
+Change-Id: I7e63293e5814b281ceec8a9632e696322d3629e8
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116035
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116871
+Tested-by: Jenkins CollaboraOffice
+Reviewed-by: Miklos Vajna
+
+editengine-columns: Implement layout
+
+This changes the way how different parts access positions of lines and
+paragraphs. Now there is ImpEditEngine::IterateLineAreas, which performs
+uniform iteration over all ParaPortions and lines in order, calling a
+user-provided callback function for each portion and line; it passes
+all information about current portion, line, area, and column to the
+callback, and checks the return from the callback, to decide if it needs
+to continue iteration (in case when callback indicated that if doesn't
+need further data), and if it needs calling the callback for the rest of
+current portion's lines.
+
+This allows to have the code that calculates and iterates dimensions of
+lines in one central place, without the need to have duplicating logic
+in several places.
+
+One important exception is ImpEditEngine::Paint, which iterates without
+ImpEditEngine::IterateLineAreas, because it does many atomic paint
+operations in different points of iteration process, and implementing
+ImpEditEngine::IterateLineAreas to call callback in the required places
+would require increased complexity, which is left for a future change.
+To make that possible, ImpEditEngine::IterFlag should be extended to
+indicate additional requirements.
+
+Note that in fact, ImpEditEngine::Paint was taken as the model for
+implementation of ImpEditEngine::IterateLineAreas, with its detailed
+handling of all the vertical offsets like additional line spacing and
+interparagraph spacings that depend on context.
+
+The notable result of the centralization of the iteration code is slight
+change of heights reported by ImpEditEngine::CalcTextHeight. Previously
+it simply added all pre-calculated heights of portions, and not taking
+into account all the spacing handling that ImpEditEngine::Paint did,
+which was inconsistent (calculated height was different from painted
+height). Now ImpEditEngine::CalcTextHeight should provide more accurate
+results, which required small changes in the unit tests.
+
+Change-Id: I33cbb978deb974b314d36fda8674186a03991107
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116034
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116876
+Tested-by: Jenkins CollaboraOffice
+Reviewed-by: Miklos Vajna
+
+editengine-columns: PPTX support (tdf#118458)
+
+The unit tests that used to check the workaround using tables to
+emulate columns (implemented in tdf#120028) are changed to test
+import of the columns.
+
+This reverts some commits related to the mentioned workaround,
+namely aef569ed83a3ccc02639e5b2a1c7cc131ba262fc,
+ c50ae6a282ed83762bf634fed5c91033eb305c88,
+ 7b64bd90637a6722438bf873b1ded74ab3424c46,
+ 33696b2820ce3c8b21b753d2c2bf92345ecb9276,
+ 99dff69b561a8fe2d9437e6aa67a9581a6666f41.
+
+Change-Id: I97693ad4a981780e822070938992f274920df5a8
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116738
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116881
+Tested-by: Jenkins CollaboraOffice
+Reviewed-by: Miklos Vajna
+
+This API change is since 7-2.
+
+Change-Id: I5ca672a9c67f9a7082ef72c63b388c953d77c22c
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116843
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+(cherry picked from commit 25bd0c7ddf0b45c8628f0c7b1b57d3eb428bf1e5)
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116710
+Tested-by: Jenkins CollaboraOffice
+
+svxcore: provide UNO constructor for com.sun.star.text.TextColumns
+
+This allows to create it e.g. in Basic macros using CreateUnoService
+
+Change-Id: I949d3b92c83cd9e763244f70b22f0f367b93cb48
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116970
+Tested-by: Jenkins
+Reviewed-by: Mike Kaganski
+(cherry picked from commit 01379acedcdad4fc08c61b73b8500e758b88d5ae)
+Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116903
+Tested-by: Jenkins CollaboraOffice
+---
+ .../xshape/data/reference/tdf90839-1.xml | 74 +--
+ .../xshape/data/reference/tdf90839-2.xml | 64 +-
+ .../xshape/data/reference/tdf90839-3.xml | 64 +-
+ .../xshape/data/reference/tdf90839-4.xml | 64 +-
+ cui/Library_cui.mk | 1 +
+ cui/UIConfig_cui.mk | 1 +
+ cui/source/factory/dlgfact.cxx | 5 +
+ cui/source/inc/TextColumnsPage.hxx | 40 ++
+ cui/source/tabpages/TextColumnsPage.cxx | 82 +++
+ cui/source/tabpages/textanim.cxx | 2 +
+ cui/uiconfig/ui/textcolumnstabpage.ui | 111 ++++
+ cui/uiconfig/ui/textdialog.ui | 48 ++
+ editeng/source/editeng/editeng.cxx | 31 +-
+ editeng/source/editeng/impedit.cxx | 131 ++--
+ editeng/source/editeng/impedit.hxx | 72 ++-
+ editeng/source/editeng/impedit2.cxx | 573 +++++++++++-------
+ editeng/source/editeng/impedit3.cxx | 516 ++++++++--------
+ editeng/source/editeng/impedit4.cxx | 4 +-
+ editeng/source/outliner/outlin2.cxx | 10 +
+ include/editeng/editeng.hxx | 4 +
+ include/editeng/outliner.hxx | 4 +
+ include/oox/ppt/pptimport.hxx | 2 -
+ include/oox/ppt/pptshape.hxx | 3 +-
+ include/svl/solar.hrc | 2 +-
+ include/svx/SvxXTextColumns.hxx | 22 +
+ include/svx/dialogs.hrc | 1 +
+ include/svx/strings.hrc | 2 +
+ include/svx/svddef.hxx | 8 +-
+ include/svx/svdotext.hxx | 9 +-
+ include/svx/unoshprp.hxx | 6 +-
+ .../com/sun/star/drawing/TextProperties.idl | 8 +
+ oox/inc/drawingml/table/tableproperties.hxx | 3 -
+ oox/inc/drawingml/textbodyproperties.hxx | 2 -
+ .../drawingml/table/tableproperties.cxx | 61 +-
+ .../drawingml/textbodypropertiescontext.cxx | 14 +-
+ oox/source/export/drawingml.cxx | 24 +-
+ oox/source/ppt/pptimport.cxx | 1 -
+ oox/source/ppt/pptshape.cxx | 33 +-
+ oox/source/ppt/slidepersist.cxx | 33 +-
+ oox/source/token/properties.txt | 1 +
+ sc/qa/uitest/chart/chartLegend.py | 183 +++---
+ sc/qa/unit/filters-test.cxx | 2 +-
+ sd/qa/uitest/impress_tests/tdf91762.py | 8 +-
+ .../uitest/impress_tests/textColumnsDialog.py | 53 ++
+ sd/qa/unit/data/pptx/tdf120028b.pptx | Bin 29838 -> 0 bytes
+ sd/qa/unit/data/xml/n593612_0.xml | 4 +-
+ sd/qa/unit/data/xml/n758621_1.xml | 4 +-
+ sd/qa/unit/export-tests-ooxml2.cxx | 125 +++-
+ sd/qa/unit/export-tests.cxx | 69 +++
+ sd/qa/unit/import-tests.cxx | 96 +--
+ sd/qa/unit/layout-tests.cxx | 107 +++-
+ sd/qa/unit/misc-tests.cxx | 49 ++
+ sd/qa/unit/tiledrendering/LOKitSearchTest.cxx | 4 +-
+ sd/source/core/stlsheet.cxx | 13 +
+ svx/Library_svxcore.mk | 1 +
+ .../sdr/properties/attributeproperties.cxx | 3 +-
+ .../sdr/properties/captionproperties.cxx | 1 +
+ .../sdr/properties/circleproperties.cxx | 1 +
+ .../sdr/properties/connectorproperties.cxx | 1 +
+ .../sdr/properties/customshapeproperties.cxx | 2 +-
+ .../sdr/properties/graphicproperties.cxx | 2 +-
+ .../sdr/properties/measureproperties.cxx | 1 +
+ svx/source/sdr/properties/textproperties.cxx | 7 +
+ svx/source/svdraw/svdattr.cxx | 9 +
+ svx/source/svdraw/svdotext.cxx | 97 ++-
+ svx/source/svdraw/svdotextdecomposition.cxx | 4 +
+ svx/source/svdraw/svdotxed.cxx | 1 +
+ svx/source/svdraw/svdoutl.cxx | 1 +
+ svx/source/unodraw/SvxXTextColumns.cxx | 334 ++++++++++
+ svx/source/unodraw/unomod.cxx | 5 +
+ svx/source/unodraw/unopool.cxx | 7 +
+ svx/source/unodraw/unoshape.cxx | 48 +-
+ svx/util/svxcore.component | 4 +
+ sw/inc/unomap.hxx | 11 -
+ sw/inc/unosett.hxx | 65 --
+ sw/source/core/layout/atrfrm.cxx | 116 +++-
+ sw/source/core/unocore/unocoll.cxx | 3 +-
+ sw/source/core/unocore/unomap.cxx | 17 -
+ sw/source/core/unocore/unomap1.cxx | 6 -
+ sw/source/core/unocore/unosett.cxx | 291 ---------
+ .../sheet/xsheetannotationshapesupplier.cxx | 2 +-
+ uitest/uitest/uihelper/guarded.py | 51 ++
+ .../draw/XMLShapePropertySetContext.cxx | 3 +
+ xmloff/source/draw/sdpropls.cxx | 6 +
+ xmloff/source/text/XMLTextColumnsExport.cxx | 2 +
+ xmloff/source/text/txtprhdl.cxx | 3 +
+ 89 files changed, 2462 insertions(+), 1496 deletions(-)
+ create mode 100644 cui/source/inc/TextColumnsPage.hxx
+ create mode 100644 cui/source/tabpages/TextColumnsPage.cxx
+ create mode 100644 cui/uiconfig/ui/textcolumnstabpage.ui
+ create mode 100644 include/svx/SvxXTextColumns.hxx
+ create mode 100644 sd/qa/uitest/impress_tests/textColumnsDialog.py
+ create mode 100644 svx/source/unodraw/SvxXTextColumns.cxx
+ create mode 100644 uitest/uitest/uihelper/guarded.py
+
+diff --git a/chart2/qa/extras/xshape/data/reference/tdf90839-1.xml b/chart2/qa/extras/xshape/data/reference/tdf90839-1.xml
+index 2af62c344940..3586969602c1 100644
+--- a/chart2/qa/extras/xshape/data/reference/tdf90839-1.xml
++++ b/chart2/qa/extras/xshape/data/reference/tdf90839-1.xml
+@@ -175,15 +175,15 @@
+
+
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+
+
+@@ -192,21 +192,21 @@
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -215,21 +215,21 @@
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -238,21 +238,21 @@
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -262,20 +262,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -284,36 +284,36 @@
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+
+
+-
+-
++
++
+
+
+
+diff --git a/chart2/qa/extras/xshape/data/reference/tdf90839-2.xml b/chart2/qa/extras/xshape/data/reference/tdf90839-2.xml
+index c8afb487a4ae..0c1c1eba61b1 100644
+--- a/chart2/qa/extras/xshape/data/reference/tdf90839-2.xml
++++ b/chart2/qa/extras/xshape/data/reference/tdf90839-2.xml
+@@ -14,7 +14,7 @@
+
+
+
+-
++
+
+
+
+@@ -30,7 +30,7 @@
+
+
+
+-
++
+
+
+
+@@ -40,11 +40,11 @@
+
+
+
+-
++
+
+
+
+-
++
+
+
+
+@@ -175,15 +175,15 @@
+
+
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+
+
+@@ -193,20 +193,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -216,20 +216,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -239,20 +239,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -262,20 +262,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -285,49 +285,49 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+diff --git a/chart2/qa/extras/xshape/data/reference/tdf90839-3.xml b/chart2/qa/extras/xshape/data/reference/tdf90839-3.xml
+index 7b67bd226da6..390d3e1615b8 100644
+--- a/chart2/qa/extras/xshape/data/reference/tdf90839-3.xml
++++ b/chart2/qa/extras/xshape/data/reference/tdf90839-3.xml
+@@ -14,7 +14,7 @@
+
+
+
+-
++
+
+
+
+@@ -30,7 +30,7 @@
+
+
+
+-
++
+
+
+
+@@ -40,11 +40,11 @@
+
+
+
+-
++
+
+
+
+-
++
+
+
+
+@@ -175,15 +175,15 @@
+
+
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+
+
+@@ -193,20 +193,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -216,20 +216,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -239,20 +239,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -262,20 +262,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -285,49 +285,49 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+diff --git a/chart2/qa/extras/xshape/data/reference/tdf90839-4.xml b/chart2/qa/extras/xshape/data/reference/tdf90839-4.xml
+index 7fabc697c751..2b107e9f81cc 100644
+--- a/chart2/qa/extras/xshape/data/reference/tdf90839-4.xml
++++ b/chart2/qa/extras/xshape/data/reference/tdf90839-4.xml
+@@ -14,7 +14,7 @@
+
+
+
+-
++
+
+
+
+@@ -30,7 +30,7 @@
+
+
+
+-
++
+
+
+
+@@ -40,11 +40,11 @@
+
+
+
+-
++
+
+
+
+-
++
+
+
+
+@@ -175,15 +175,15 @@
+
+
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+-
++
+
+
+
+@@ -193,20 +193,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -216,20 +216,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -239,20 +239,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -262,20 +262,20 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+-
++
+
+-
++
+
+
+
+@@ -285,49 +285,49 @@
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+
+
+
+-
++
+
+
+
+diff --git a/cui/Library_cui.mk b/cui/Library_cui.mk
+index ff221a9cc55a..e702d6ae94ff 100644
+--- a/cui/Library_cui.mk
++++ b/cui/Library_cui.mk
+@@ -212,6 +212,7 @@ $(eval $(call gb_Library_add_exception_objects,cui,\
+ cui/source/tabpages/tabstpge \
+ cui/source/tabpages/textanim \
+ cui/source/tabpages/textattr \
++ cui/source/tabpages/TextColumnsPage \
+ cui/source/tabpages/tparea \
+ cui/source/tabpages/tpbitmap \
+ cui/source/tabpages/tpcolor \
+diff --git a/cui/UIConfig_cui.mk b/cui/UIConfig_cui.mk
+index 153d6fe98fda..58fd66525310 100644
+--- a/cui/UIConfig_cui.mk
++++ b/cui/UIConfig_cui.mk
+@@ -200,6 +200,7 @@ $(eval $(call gb_UIConfig_add_uifiles,cui,\
+ cui/uiconfig/ui/swpossizepage \
+ cui/uiconfig/ui/textattrtabpage \
+ cui/uiconfig/ui/textanimtabpage \
++ cui/uiconfig/ui/textcolumnstabpage \
+ cui/uiconfig/ui/textdialog \
+ cui/uiconfig/ui/textflowpage \
+ cui/uiconfig/ui/thesaurus \
+diff --git a/cui/source/factory/dlgfact.cxx b/cui/source/factory/dlgfact.cxx
+index e43b027602c4..3ed88fbe4995 100644
+--- a/cui/source/factory/dlgfact.cxx
++++ b/cui/source/factory/dlgfact.cxx
+@@ -90,6 +90,7 @@
+ #include
+ #include
+ #include
++#include
+
+ using namespace ::com::sun::star;
+ using namespace ::com::sun::star::frame;
+@@ -1483,6 +1484,8 @@ CreateTabPage AbstractDialogFactory_Impl::GetTabPageCreatorFunc( sal_uInt16 nId
+ return SvxGrfCropPage::Create;
+ case RID_SVXPAGE_MACROASSIGN :
+ return SfxMacroTabPage::Create;
++ case RID_SVXPAGE_TEXTCOLUMNS:
++ return SvxTextColumnsPage::Create;
+ default:
+ break;
+ }
+@@ -1546,6 +1549,8 @@ GetTabPageRanges AbstractDialogFactory_Impl::GetTabPageRangesFunc( sal_uInt16 nI
+ return SvxPageDescPage::GetRanges;
+ case RID_SVXPAGE_ASIAN_LAYOUT:
+ return SvxAsianLayoutPage::GetRanges;
++ case RID_SVXPAGE_TEXTCOLUMNS:
++ return SvxTextColumnsPage::GetRanges;
+ default:
+ break;
+ }
+diff --git a/cui/source/inc/TextColumnsPage.hxx b/cui/source/inc/TextColumnsPage.hxx
+new file mode 100644
+index 000000000000..6153cd27a520
+--- /dev/null
++++ b/cui/source/inc/TextColumnsPage.hxx
+@@ -0,0 +1,40 @@
++/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
++/*
++ * This file is part of the LibreOffice project.
++ *
++ * This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
++ */
++
++#pragma once
++
++#include
++
++#include
++
++#include
++
++/// Tab page for EditEngine columns properties
++class SvxTextColumnsPage : public SfxTabPage
++{
++private:
++ static const sal_uInt16 pRanges[];
++
++ std::unique_ptr m_xColumnsNumber;
++ std::unique_ptr m_xColumnsSpacing;
++
++public:
++ SvxTextColumnsPage(weld::Container* pPage, weld::DialogController* pController,
++ const SfxItemSet& rInAttrs);
++ virtual ~SvxTextColumnsPage() override;
++
++ static std::unique_ptr
++ Create(weld::Container* pPage, weld::DialogController* pController, const SfxItemSet*);
++ static const sal_uInt16* GetRanges() { return pRanges; }
++
++ virtual bool FillItemSet(SfxItemSet*) override;
++ virtual void Reset(const SfxItemSet*) override;
++};
++
++/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
+diff --git a/cui/source/tabpages/TextColumnsPage.cxx b/cui/source/tabpages/TextColumnsPage.cxx
+new file mode 100644
+index 000000000000..db83722e6be1
+--- /dev/null
++++ b/cui/source/tabpages/TextColumnsPage.cxx
+@@ -0,0 +1,82 @@
++/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
++/*
++ * This file is part of the LibreOffice project.
++ *
++ * This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
++ */
++
++#include
++
++#include
++#include
++#include
++#include
++
++#include
++
++const sal_uInt16 SvxTextColumnsPage::pRanges[]
++ = { SDRATTR_TEXTCOLUMNS_FIRST, SDRATTR_TEXTCOLUMNS_LAST, 0 };
++
++SvxTextColumnsPage::SvxTextColumnsPage(weld::Container* pPage, weld::DialogController* pController,
++ const SfxItemSet& rInAttrs)
++ : SfxTabPage(pPage, pController, "cui/ui/textcolumnstabpage.ui", "TextColumnsPage", &rInAttrs)
++ , m_xColumnsNumber(m_xBuilder->weld_spin_button("FLD_COL_NUMBER"))
++ , m_xColumnsSpacing(
++ m_xBuilder->weld_metric_spin_button("MTR_FLD_COL_SPACING", GetModuleFieldUnit(rInAttrs)))
++{
++}
++
++SvxTextColumnsPage::~SvxTextColumnsPage() = default;
++
++// read the passed item set
++void SvxTextColumnsPage::Reset(const SfxItemSet* rAttrs)
++{
++ SfxItemPool* pPool = rAttrs->GetPool();
++ assert(pPool);
++
++ {
++ auto pItem = GetItem(*rAttrs, SDRATTR_TEXTCOLUMNS_NUMBER);
++ if (!pItem)
++ pItem = &pPool->GetDefaultItem(SDRATTR_TEXTCOLUMNS_NUMBER);
++ m_xColumnsNumber->set_value(pItem->GetValue());
++ m_xColumnsNumber->save_value();
++ }
++
++ {
++ MapUnit eUnit = pPool->GetMetric(SDRATTR_TEXTCOLUMNS_SPACING);
++ auto pItem = GetItem(*rAttrs, SDRATTR_TEXTCOLUMNS_SPACING);
++ if (!pItem)
++ pItem = &pPool->GetDefaultItem(SDRATTR_TEXTCOLUMNS_SPACING);
++ SetMetricValue(*m_xColumnsSpacing, pItem->GetValue(), eUnit);
++ m_xColumnsSpacing->save_value();
++ }
++}
++
++// fill the passed item set with dialog box attributes
++bool SvxTextColumnsPage::FillItemSet(SfxItemSet* rAttrs)
++{
++ if (m_xColumnsNumber->get_value_changed_from_saved())
++ rAttrs->Put(SfxInt16Item(SDRATTR_TEXTCOLUMNS_NUMBER, m_xColumnsNumber->get_value()));
++
++ if (m_xColumnsSpacing->get_value_changed_from_saved())
++ {
++ SfxItemPool* pPool = rAttrs->GetPool();
++ assert(pPool);
++ MapUnit eUnit = pPool->GetMetric(SDRATTR_TEXTCOLUMNS_SPACING);
++ sal_Int32 nValue = GetCoreValue(*m_xColumnsSpacing, eUnit);
++ rAttrs->Put(SdrMetricItem(SDRATTR_TEXTCOLUMNS_SPACING, nValue));
++ }
++
++ return true;
++}
++
++std::unique_ptr SvxTextColumnsPage::Create(weld::Container* pPage,
++ weld::DialogController* pController,
++ const SfxItemSet* rAttrs)
++{
++ return std::make_unique(pPage, pController, *rAttrs);
++}
++
++/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
+diff --git a/cui/source/tabpages/textanim.cxx b/cui/source/tabpages/textanim.cxx
+index a7ee4b3aade9..b4660fc19b8d 100644
+--- a/cui/source/tabpages/textanim.cxx
++++ b/cui/source/tabpages/textanim.cxx
+@@ -19,6 +19,7 @@
+
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -47,6 +48,7 @@ SvxTextTabDialog::SvxTextTabDialog(weld::Window* pParent, const SfxItemSet* pAtt
+ {
+ AddTabPage("RID_SVXPAGE_TEXTATTR", SvxTextAttrPage::Create, nullptr);
+ AddTabPage("RID_SVXPAGE_TEXTANIMATION", SvxTextAnimationPage::Create, nullptr);
++ AddTabPage("RID_SVXPAGE_TEXTCOLUMNS", SvxTextColumnsPage::Create, nullptr);
+ }
+
+ /*************************************************************************
+diff --git a/cui/uiconfig/ui/textcolumnstabpage.ui b/cui/uiconfig/ui/textcolumnstabpage.ui
+new file mode 100644
+index 000000000000..596b155dbe1b
+--- /dev/null
++++ b/cui/uiconfig/ui/textcolumnstabpage.ui
+@@ -0,0 +1,111 @@
++
++
++
++
++
++
++
++
+diff --git a/cui/uiconfig/ui/textdialog.ui b/cui/uiconfig/ui/textdialog.ui
+index a7cddf8500fd..4fcf813c71ba 100644
+--- a/cui/uiconfig/ui/textdialog.ui
++++ b/cui/uiconfig/ui/textdialog.ui
+@@ -187,6 +187,54 @@
+ False
+
+
++
++
++
++ True
++ False
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++ 2
++
++
++
++
++ True
++ False
++ Text Columns
++
++
++ 2
++ False
++
++
+
+
+ False
+diff --git a/editeng/source/editeng/editeng.cxx b/editeng/source/editeng/editeng.cxx
+index 3c06cbf96f51..7592a3f2b964 100644
+--- a/editeng/source/editeng/editeng.cxx
++++ b/editeng/source/editeng/editeng.cxx
+@@ -448,6 +448,11 @@ bool EditEngine::GetDirectVertical() const
+ return pImpEditEngine->GetDirectVertical();
+ }
+
++void EditEngine::SetTextColumns(sal_Int16 nColumns, sal_Int32 nSpacing)
++{
++ pImpEditEngine->SetTextColumns(nColumns, nSpacing);
++}
++
+ void EditEngine::SetFixedCellHeight( bool bUseFixedCellHeight )
+ {
+ pImpEditEngine->SetFixedCellHeight( bUseFixedCellHeight );
+@@ -555,6 +560,11 @@ void EditEngine::SetMaxAutoPaperSize( const Size& rSz )
+ pImpEditEngine->SetMaxAutoPaperSize( rSz );
+ }
+
++void EditEngine::SetMinColumnWrapHeight(tools::Long nVal)
++{
++ pImpEditEngine->SetMinColumnWrapHeight(nVal);
++}
++
+ OUString EditEngine::GetText( LineEnd eEnd ) const
+ {
+ return pImpEditEngine->GetEditDoc().GetText( eEnd );
+@@ -2013,29 +2023,12 @@ bool EditEngine::IsTextPos( const Point& rPaperPos, sal_uInt16 nBorder )
+ if ( !pImpEditEngine->IsFormatted() )
+ pImpEditEngine->FormatDoc();
+
+- bool bTextPos = false;
+ // take unrotated positions for calculation here
+ Point aDocPos = GetDocPos( rPaperPos );
+
+ if ( ( aDocPos.Y() > 0 ) && ( aDocPos.Y() < static_cast(pImpEditEngine->GetTextHeight()) ) )
+- {
+- EditPaM aPaM = pImpEditEngine->GetPaM( aDocPos, false );
+- if ( aPaM.GetNode() )
+- {
+- const ParaPortion* pParaPortion = pImpEditEngine->FindParaPortion( aPaM.GetNode() );
+- DBG_ASSERT( pParaPortion, "ParaPortion?" );
+-
+- sal_Int32 nLine = pParaPortion->GetLineNumber( aPaM.GetIndex() );
+- const EditLine& rLine = pParaPortion->GetLines()[nLine];
+- Range aLineXPosStartEnd = pImpEditEngine->GetLineXPosStartEnd( pParaPortion, &rLine );
+- if ( ( aDocPos.X() >= aLineXPosStartEnd.Min() - nBorder ) &&
+- ( aDocPos.X() <= aLineXPosStartEnd.Max() + nBorder ) )
+- {
+- bTextPos = true;
+- }
+- }
+- }
+- return bTextPos;
++ return pImpEditEngine->IsTextPos(aDocPos, nBorder);
++ return false;
+ }
+
+ void EditEngine::SetEditTextObjectPool( SfxItemPool* pPool )
+diff --git a/editeng/source/editeng/impedit.cxx b/editeng/source/editeng/impedit.cxx
+index f226f983f593..138d73825b55 100644
+--- a/editeng/source/editeng/impedit.cxx
++++ b/editeng/source/editeng/impedit.cxx
+@@ -518,107 +518,116 @@ void ImpEditView::DrawSelectionXOR( EditSelection aTmpSel, vcl::Region* pRegion,
+ bool bStartHandleVisible = false;
+ bool bEndHandleVisible = false;
+
+- for ( sal_Int32 nPara = nStartPara; nPara <= nEndPara; nPara++ )
+- {
+- ParaPortion* pTmpPortion = pEditEngine->GetParaPortions().SafeGetObject( nPara );
+- if (!pTmpPortion)
++ auto DrawHighlight = [&, nStartLine = sal_Int32(0), nEndLine = sal_Int32(0)](
++ const ImpEditEngine::LineAreaInfo& rInfo) mutable {
++ if (!rInfo.pLine) // Begin of ParaPortion
+ {
+- SAL_WARN( "editeng", "Portion in Selection not found!" );
+- continue;
+- }
+-
+- DBG_ASSERT( !pTmpPortion->IsInvalid(), "Portion in Selection not formatted!" );
+-
+- if ( !pTmpPortion->IsVisible() || pTmpPortion->IsInvalid() )
+- continue;
+-
+- tools::Long nParaStart = pEditEngine->GetParaPortions().GetYOffset( pTmpPortion );
+- if ( ( nParaStart + pTmpPortion->GetHeight() ) < GetVisDocTop() )
+- continue;
+- if ( nParaStart > GetVisDocBottom() )
+- break;
+-
+- sal_uInt16 nStartLine = 0;
+- sal_uInt16 nEndLine = pTmpPortion->GetLines().Count() -1;
+- if ( nPara == nStartPara )
+- nStartLine = pTmpPortion->GetLines().FindLine( aTmpSel.Min().GetIndex(), false );
+- if ( nPara == nEndPara )
+- nEndLine = pTmpPortion->GetLines().FindLine( aTmpSel.Max().GetIndex(), true );
++ if (rInfo.nPortion < nStartPara)
++ return ImpEditEngine::CallbackResult::SkipThisPortion;
++ if (rInfo.nPortion > nEndPara)
++ return ImpEditEngine::CallbackResult::Stop;
++ DBG_ASSERT(!rInfo.rPortion.IsInvalid(), "Portion in Selection not formatted!");
++ if (rInfo.rPortion.IsInvalid())
++ return ImpEditEngine::CallbackResult::SkipThisPortion;
++
++ if (rInfo.nPortion == nStartPara)
++ nStartLine = rInfo.rPortion.GetLines().FindLine(aTmpSel.Min().GetIndex(), false);
++ else
++ nStartLine = 0;
+
+- for ( sal_uInt16 nLine = nStartLine; nLine <= nEndLine; nLine++ )
++ if (rInfo.nPortion == nEndPara)
++ nEndLine = rInfo.rPortion.GetLines().FindLine(aTmpSel.Max().GetIndex(), true);
++ else
++ nEndLine = rInfo.rPortion.GetLines().Count() - 1;
++ }
++ else // This is a correct ParaPortion
+ {
+- const EditLine& rLine = pTmpPortion->GetLines()[nLine];
++ if (rInfo.nLine < nStartLine)
++ return ImpEditEngine::CallbackResult::Continue;
++ if (rInfo.nLine > nEndLine)
++ return ImpEditEngine::CallbackResult::SkipThisPortion;
+
+ bool bPartOfLine = false;
+- sal_Int32 nStartIndex = rLine.GetStart();
+- sal_Int32 nEndIndex = rLine.GetEnd();
+- if ( ( nPara == nStartPara ) && ( nLine == nStartLine ) && ( nStartIndex != aTmpSel.Min().GetIndex() ) )
++ sal_Int32 nStartIndex = rInfo.pLine->GetStart();
++ sal_Int32 nEndIndex = rInfo.pLine->GetEnd();
++ if ((rInfo.nPortion == nStartPara) && (rInfo.nLine == nStartLine)
++ && (nStartIndex != aTmpSel.Min().GetIndex()))
+ {
+ nStartIndex = aTmpSel.Min().GetIndex();
+ bPartOfLine = true;
+ }
+- if ( ( nPara == nEndPara ) && ( nLine == nEndLine ) && ( nEndIndex != aTmpSel.Max().GetIndex() ) )
++ if ((rInfo.nPortion == nEndPara) && (rInfo.nLine == nEndLine)
++ && (nEndIndex != aTmpSel.Max().GetIndex()))
+ {
+ nEndIndex = aTmpSel.Max().GetIndex();
+ bPartOfLine = true;
+ }
+
+ // Can happen if at the beginning of a wrapped line.
+- if ( nEndIndex < nStartIndex )
++ if (nEndIndex < nStartIndex)
+ nEndIndex = nStartIndex;
+
+- tools::Rectangle aTmpRect( pEditEngine->pImpEditEngine->GetEditCursor( pTmpPortion, nStartIndex ) );
+- Point aTopLeft( aTmpRect.TopLeft() );
+- Point aBottomRight( aTmpRect.BottomRight() );
+-
+- aTopLeft.AdjustY(nParaStart );
+- aBottomRight.AdjustY(nParaStart );
++ tools::Rectangle aTmpRect(pEditEngine->pImpEditEngine->GetEditCursor(
++ &rInfo.rPortion, rInfo.pLine, nStartIndex, GetCursorFlags::NONE));
++ aTmpRect.Move(0, pEditEngine->pImpEditEngine->getTopDirectionAware(rInfo.aArea));
+
+ // Only paint if in the visible range ...
+- if ( aTopLeft.Y() > GetVisDocBottom() )
+- break;
++ if (aTmpRect.Top() > GetVisDocBottom())
++ return ImpEditEngine::CallbackResult::Continue;
+
+- if ( aBottomRight.Y() < GetVisDocTop() )
+- continue;
++ if (aTmpRect.Bottom() < GetVisDocTop())
++ return ImpEditEngine::CallbackResult::Continue;
+
+- if ( ( nPara == nStartPara ) && ( nLine == nStartLine ) )
++ if ((rInfo.nPortion == nStartPara) && (rInfo.nLine == nStartLine))
+ bStartHandleVisible = true;
+- if ( ( nPara == nEndPara ) && ( nLine == nEndLine ) )
++ if ((rInfo.nPortion == nEndPara) && (rInfo.nLine == nEndLine))
+ bEndHandleVisible = true;
+
+ // Now that we have Bidi, the first/last index doesn't have to be the 'most outside' position
+- if ( !bPartOfLine )
++ if (!bPartOfLine)
+ {
+- Range aLineXPosStartEnd = pEditEngine->GetLineXPosStartEnd(pTmpPortion, &rLine);
+- aTopLeft.setX( aLineXPosStartEnd.Min() );
+- aBottomRight.setX( aLineXPosStartEnd.Max() );
+- ImplDrawHighlightRect( pTarget, aTopLeft, aBottomRight, pPolyPoly.get() );
++ Range aLineXPosStartEnd
++ = pEditEngine->GetLineXPosStartEnd(&rInfo.rPortion, rInfo.pLine);
++ aTmpRect.SetLeft(aLineXPosStartEnd.Min());
++ aTmpRect.SetRight(aLineXPosStartEnd.Max());
++ aTmpRect.Move(pEditEngine->pImpEditEngine->getLeftDirectionAware(rInfo.aArea), 0);
++ ImplDrawHighlightRect(pTarget, aTmpRect.TopLeft(), aTmpRect.BottomRight(),
++ pPolyPoly.get());
+ }
+ else
+ {
+ sal_Int32 nTmpStartIndex = nStartIndex;
+ sal_Int32 nWritingDirStart, nTmpEndIndex;
++ const sal_Int32 nLeftOffset
++ = pEditEngine->pImpEditEngine->getLeftDirectionAware(rInfo.aArea);
+
+- while ( nTmpStartIndex < nEndIndex )
++ while (nTmpStartIndex < nEndIndex)
+ {
+- pEditEngine->pImpEditEngine->GetRightToLeft( nPara, nTmpStartIndex+1, &nWritingDirStart, &nTmpEndIndex );
+- if ( nTmpEndIndex > nEndIndex )
++ pEditEngine->pImpEditEngine->GetRightToLeft(rInfo.nPortion, nTmpStartIndex + 1,
++ &nWritingDirStart, &nTmpEndIndex);
++ if (nTmpEndIndex > nEndIndex)
+ nTmpEndIndex = nEndIndex;
+
+- DBG_ASSERT( nTmpEndIndex > nTmpStartIndex, "DrawSelectionXOR, Start >= End?" );
++ DBG_ASSERT(nTmpEndIndex > nTmpStartIndex, "DrawSelectionXOR, Start >= End?");
+
+- tools::Long nX1 = pEditEngine->GetXPos(pTmpPortion, &rLine, nTmpStartIndex, true);
+- tools::Long nX2 = pEditEngine->GetXPos(pTmpPortion, &rLine, nTmpEndIndex);
++ tools::Long nX1
++ = pEditEngine->GetXPos(&rInfo.rPortion, rInfo.pLine, nTmpStartIndex, true);
++ tools::Long nX2
++ = pEditEngine->GetXPos(&rInfo.rPortion, rInfo.pLine, nTmpEndIndex);
+
+- Point aPt1( std::min( nX1, nX2 ), aTopLeft.Y() );
+- Point aPt2( std::max( nX1, nX2 ), aBottomRight.Y() );
++ aTmpRect.SetLeft(std::min(nX1, nX2));
++ aTmpRect.SetRight(std::max(nX1, nX2));
++ aTmpRect.Move(nLeftOffset, 0);
+
+- ImplDrawHighlightRect( pTarget, aPt1, aPt2, pPolyPoly.get() );
++ ImplDrawHighlightRect(pTarget, aTmpRect.TopLeft(), aTmpRect.BottomRight(),
++ pPolyPoly.get());
+ nTmpStartIndex = nTmpEndIndex;
+ }
+ }
+ }
+- }
++ return ImpEditEngine::CallbackResult::Continue;
++ };
++ pEditEngine->pImpEditEngine->IterateLineAreas(DrawHighlight, ImpEditEngine::IterFlag::none);
+
+ if (comphelper::LibreOfficeKit::isActive() && mpViewShell && pOutWin)
+ lokSelectionCallback(pPolyPoly, bStartHandleVisible, bEndHandleVisible);
+@@ -1791,6 +1800,8 @@ const SvxFieldItem* ImpEditView::GetField( const Point& rPos, sal_Int32* pPara,
+
+ Point aDocPos( GetDocPos( rPos ) );
+ EditPaM aPaM = pEditEngine->GetPaM(aDocPos, false);
++ if (!aPaM)
++ return nullptr;
+
+ if ( aPaM.GetIndex() == aPaM.GetNode()->Len() )
+ {
+@@ -1829,6 +1840,8 @@ bool ImpEditView::IsBulletArea( const Point& rPos, sal_Int32* pPara )
+
+ Point aDocPos( GetDocPos( rPos ) );
+ EditPaM aPaM = pEditEngine->GetPaM(aDocPos, false);
++ if (!aPaM)
++ return false;
+
+ if ( aPaM.GetIndex() == 0 )
+ {
+diff --git a/editeng/source/editeng/impedit.hxx b/editeng/source/editeng/impedit.hxx
+index 3adffe49b1eb..dd797469a81f 100644
+--- a/editeng/source/editeng/impedit.hxx
++++ b/editeng/source/editeng/impedit.hxx
+@@ -54,8 +54,10 @@
+ #include
+ #include
+
++#include
+ #include
+ #include
++#include
+ #include
+
+ class EditView;
+@@ -490,6 +492,7 @@ private:
+ Size aPaperSize; // Layout
+ Size aMinAutoPaperSize; // Layout ?
+ Size aMaxAutoPaperSize; // Layout ?
++ tools::Long mnMinColumnWrapHeight = 0; // Corresponds to graphic object height
+ EditDoc aEditDoc; // Document content
+
+ // Engine Specific data ...
+@@ -549,8 +552,8 @@ private:
+ // For Formatting / Update...
+ std::vector > aDeletedNodes;
+ tools::Rectangle aInvalidRect;
+- sal_uInt32 nCurTextHeight;
+- sal_uInt32 nCurTextHeightNTP; // without trailing empty paragraphs
++ tools::Long nCurTextHeight;
++ tools::Long nCurTextHeightNTP; // without trailing empty paragraphs
+ sal_uInt16 nOnePixelInRef;
+
+ IdleFormattter aIdleFormatter;
+@@ -562,6 +565,9 @@ private:
+ sal_Int32 mnOverflowingLine = -1;
+ bool mbNeedsChainingHandling = false;
+
++ sal_Int16 mnColumns = 1;
++ sal_Int32 mnColumnSpacing = 0;
++
+ // If it is detected at one point that the StatusHdl has to be called, but
+ // this should not happen immediately (critical section):
+ Timer aStatusTimer;
+@@ -614,8 +620,9 @@ private:
+
+ std::unique_ptr GetEmptyTextObject();
+
++ std::tuple GetPortionAndLine(Point aDocPos);
+ EditPaM GetPaM( Point aDocPos, bool bSmart = true );
+- EditPaM GetPaM( ParaPortion* pPortion, Point aPos, bool bSmart );
++ bool IsTextPos(const Point& rDocPos, sal_uInt16 nBorder);
+ tools::Long GetXPos(const ParaPortion* pParaPortion, const EditLine* pLine, sal_Int32 nIndex, bool bPreferPortionStart = false) const;
+ tools::Long GetPortionXOffset(const ParaPortion* pParaPortion, const EditLine* pLine, sal_Int32 nTextPortion) const;
+ sal_Int32 GetChar(const ParaPortion* pParaPortion, const EditLine* pLine, tools::Long nX, bool bSmart = true);
+@@ -765,8 +772,8 @@ private:
+ css::uno::Reference < css::i18n::XBreakIterator > const & ImplGetBreakIterator() const;
+ css::uno::Reference < css::i18n::XExtendedInputSequenceChecker > const & ImplGetInputSequenceChecker() const;
+
+- void ImplUpdateOverflowingParaNum( sal_uInt32 );
+- void ImplUpdateOverflowingLineNum( sal_uInt32, sal_uInt32, sal_uInt32 );
++ void ImplUpdateOverflowingParaNum(tools::Long);
++ void ImplUpdateOverflowingLineNum(tools::Long, sal_uInt32, tools::Long);
+
+ void CreateSpellInfo( bool bMultipleDocs );
+ /// Obtains a view shell ID from the active EditView.
+@@ -780,6 +787,8 @@ private:
+ const ParaPortionList& GetParaPortions() const { return aParaPortionList; }
+ ParaPortionList& GetParaPortions() { return aParaPortionList; }
+
++ tools::Long Calc1ColumnTextHeight(tools::Long* pHeightNTP);
++
+ protected:
+ virtual void Notify( SfxBroadcaster& rBC, const SfxHint& rHint ) override;
+
+@@ -807,6 +816,8 @@ public:
+ void SetRotation( TextRotation nRotation);
+ TextRotation GetRotation() const { return GetEditDoc().GetRotation(); }
+
++ void SetTextColumns(sal_Int16 nColumns, sal_Int32 nSpacing);
++
+ bool IsPageOverflow( ) const;
+
+ void SetFixedCellHeight( bool bUseFixedCellHeight );
+@@ -830,6 +841,8 @@ public:
+ const Size& GetMaxAutoPaperSize() const { return aMaxAutoPaperSize; }
+ void SetMaxAutoPaperSize( const Size& rSz ) { aMaxAutoPaperSize = rSz; }
+
++ void SetMinColumnWrapHeight(tools::Long nVal) { mnMinColumnWrapHeight = nVal; }
++
+ void FormatDoc();
+ void FormatFullDoc();
+ void UpdateViews( EditView* pCurView = nullptr );
+@@ -884,7 +897,7 @@ public:
+
+ EditSelection MoveParagraphs( Range aParagraphs, sal_Int32 nNewPos, EditView* pCurView );
+
+- sal_uInt32 CalcTextHeight( sal_uInt32* pHeightNTP );
++ tools::Long CalcTextHeight( tools::Long* pHeightNTP );
+ sal_uInt32 GetTextHeight() const;
+ sal_uInt32 GetTextHeightNTP() const;
+ sal_uInt32 CalcTextWidth( bool bIgnoreExtraSpace);
+@@ -916,7 +929,8 @@ public:
+ }
+
+ tools::Rectangle PaMtoEditCursor( EditPaM aPaM, GetCursorFlags nFlags = GetCursorFlags::NONE );
+- tools::Rectangle GetEditCursor( ParaPortion* pPortion, sal_Int32 nIndex, GetCursorFlags nFlags = GetCursorFlags::NONE );
++ tools::Rectangle GetEditCursor(const ParaPortion* pPortion, const EditLine* pLine,
++ sal_Int32 nIndex, GetCursorFlags nFlags);
+
+ bool IsModified() const { return aEditDoc.IsModified(); }
+ void SetModifyFlag( bool b ) { aEditDoc.SetModified( b ); }
+@@ -1116,6 +1130,50 @@ public:
+ void Dispose();
+ void SetLOKSpecialPaperSize(const Size& rSize) { aLOKSpecialPaperSize = rSize; }
+ const Size& GetLOKSpecialPaperSize() const { return aLOKSpecialPaperSize; }
++
++ enum class CallbackResult
++ {
++ Continue,
++ SkipThisPortion, // Do not call callback until next portion
++ Stop, // Stop iteration
++ };
++ struct LineAreaInfo
++ {
++ sal_Int32 nColumn; // Column number; when overflowing, equal to total number of columns
++ ParaPortion& rPortion; // Current ParaPortion
++ sal_Int32 nPortion;
++ EditLine* pLine; // Current line, or nullptr for paragraph start
++ sal_Int32 nLine;
++ tools::Rectangle aArea; // The area for the line (or for rPortion's first line offset)
++ tools::Long nHeightNeededToNotWrap;
++ };
++ using IterateLinesAreasFunc = std::function;
++ enum IterFlag // bitmask
++ {
++ none = 0,
++ inclILS = 1, // rArea includes interline space
++ };
++
++ void IterateLineAreas(const IterateLinesAreasFunc& f, IterFlag eOptions);
++
++ tools::Long GetColumnWidth(const Size& rPaperSize) const;
++ Point MoveToNextLine(Point& rMovePos, tools::Long nLineHeight, sal_Int32& nColumn,
++ Point aOrigin, tools::Long* pnHeightNeededToNotWrap = nullptr) const;
++
++ tools::Long getXDirectionAware(const Point& pt) const;
++ tools::Long getYDirectionAware(const Point& pt) const;
++ tools::Long getWidthDirectionAware(const Size& sz) const;
++ tools::Long getHeightDirectionAware(const Size& sz) const;
++ void adjustXDirectionAware(Point& pt, tools::Long x) const;
++ void adjustYDirectionAware(Point& pt, tools::Long y) const;
++ void setXDirectionAware(Point& pt, tools::Long x) const;
++ void setYDirectionAware(Point& pt, tools::Long y) const;
++ tools::Long getYOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const;
++ bool isXOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const;
++ tools::Long getLeftDirectionAware(const tools::Rectangle& rect) const;
++ tools::Long getRightDirectionAware(const tools::Rectangle& rect) const;
++ tools::Long getTopDirectionAware(const tools::Rectangle& rect) const;
++ tools::Long getBottomDirectionAware(const tools::Rectangle& rect) const;
+ };
+
+ inline EPaM ImpEditEngine::CreateEPaM( const EditPaM& rPaM )
+diff --git a/editeng/source/editeng/impedit2.cxx b/editeng/source/editeng/impedit2.cxx
+index 2c83c1165856..4980c5730de8 100644
+--- a/editeng/source/editeng/impedit2.cxx
++++ b/editeng/source/editeng/impedit2.cxx
+@@ -59,12 +59,14 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -536,22 +538,47 @@ void ImpEditEngine::Command( const CommandEvent& rCEvt, EditView* pView )
+ if ( !IsFormatted() )
+ FormatDoc();
+
+- ParaPortion* pParaPortion = GetParaPortions().SafeGetObject( GetEditDoc().GetPos( aPaM.GetNode() ) );
++ sal_Int32 nPortionPos = GetEditDoc().GetPos(aPaM.GetNode());
++ ParaPortion* pParaPortion = GetParaPortions().SafeGetObject(nPortionPos);
+ if (pParaPortion)
+ {
+- sal_Int32 nLine = pParaPortion->GetLines().FindLine( aPaM.GetIndex(), true );
+- const EditLine& rLine = pParaPortion->GetLines()[nLine];
+- std::unique_ptr aRects(new tools::Rectangle[ mpIMEInfos->nLen ]);
+- for (sal_Int32 i = 0; i < mpIMEInfos->nLen; ++i)
+- {
+- sal_Int32 nInputPos = mpIMEInfos->aPos.GetIndex() + i;
+- if ( nInputPos > rLine.GetEnd() )
+- nInputPos = rLine.GetEnd();
+- tools::Rectangle aR2 = GetEditCursor( pParaPortion, nInputPos );
+- aRects[ i ] = pView->GetImpEditView()->GetWindowPos( aR2 );
+- }
++ const sal_Int32 nMinPos = mpIMEInfos->aPos.GetIndex();
++ const sal_Int32 nMaxPos = nMinPos + mpIMEInfos->nLen - 1;
++ std::vector aRects(mpIMEInfos->nLen);
++
++ auto CollectCharPositions = [&](const LineAreaInfo& rInfo) {
++ if (!rInfo.pLine) // Start of ParaPortion
++ {
++ if (rInfo.nPortion < nPortionPos)
++ return CallbackResult::SkipThisPortion;
++ if (rInfo.nPortion > nPortionPos)
++ return CallbackResult::Stop;
++ assert(&rInfo.rPortion == pParaPortion);
++ }
++ else // This is the needed ParaPortion
++ {
++ if (rInfo.pLine->GetStart() > nMaxPos)
++ return CallbackResult::Stop;
++ if (rInfo.pLine->GetEnd() < nMinPos)
++ return CallbackResult::Continue;
++ for (sal_Int32 n = nMinPos; n <= nMaxPos; ++n)
++ {
++ if (rInfo.pLine->IsIn(n))
++ {
++ tools::Rectangle aR = GetEditCursor(pParaPortion, rInfo.pLine, n,
++ GetCursorFlags::NONE);
++ aR.Move(getLeftDirectionAware(rInfo.aArea),
++ getTopDirectionAware(rInfo.aArea));
++ aRects[n - nMinPos] = pView->GetImpEditView()->GetWindowPos(aR);
++ }
++ }
++ }
++ return CallbackResult::Continue;
++ };
++ IterateLineAreas(CollectCharPositions, IterFlag::none);
++
+ if (vcl::Window* pWindow = pView->GetWindow())
+- pWindow->SetCompositionCharRect( aRects.get(), mpIMEInfos->nLen );
++ pWindow->SetCompositionCharRect(aRects.data(), aRects.size());
+ }
+ }
+ }
+@@ -3027,71 +3054,242 @@ EditPaM ImpEditEngine::InsertLineBreak(const EditSelection& aCurSel)
+
+ // Helper functions
+
++tools::Rectangle ImpEditEngine::GetEditCursor(const ParaPortion* pPortion, const EditLine* pLine,
++ sal_Int32 nIndex, GetCursorFlags nFlags)
++{
++ assert(pPortion && pLine);
++ // nIndex might be not in the line
++ // Search within the line...
++ tools::Long nX;
++
++ if ((nIndex == pLine->GetStart()) && (nFlags & GetCursorFlags::StartOfLine))
++ {
++ Range aXRange = GetLineXPosStartEnd(pPortion, pLine);
++ nX = !IsRightToLeft(GetEditDoc().GetPos(pPortion->GetNode())) ? aXRange.Min()
++ : aXRange.Max();
++ }
++ else if ((nIndex == pLine->GetEnd()) && (nFlags & GetCursorFlags::EndOfLine))
++ {
++ Range aXRange = GetLineXPosStartEnd(pPortion, pLine);
++ nX = !IsRightToLeft(GetEditDoc().GetPos(pPortion->GetNode())) ? aXRange.Max()
++ : aXRange.Min();
++ }
++ else
++ {
++ nX = GetXPos(pPortion, pLine, nIndex, bool(nFlags & GetCursorFlags::PreferPortionStart));
++ }
++
++ tools::Rectangle aEditCursor;
++ aEditCursor.SetLeft(nX);
++ aEditCursor.SetRight(nX);
++
++ aEditCursor.SetBottom(pLine->GetHeight() - 1);
++ if (nFlags & GetCursorFlags::TextOnly)
++ aEditCursor.SetTop(aEditCursor.Bottom() - pLine->GetTxtHeight() + 1);
++ else
++ aEditCursor.SetTop(aEditCursor.Bottom()
++ - std::min(pLine->GetTxtHeight(), pLine->GetHeight()) + 1);
++ return aEditCursor;
++}
++
+ tools::Rectangle ImpEditEngine::PaMtoEditCursor( EditPaM aPaM, GetCursorFlags nFlags )
+ {
+ OSL_ENSURE( GetUpdateMode(), "Must not be reached when Update=FALSE: PaMtoEditCursor" );
+
+ tools::Rectangle aEditCursor;
+- tools::Long nY = 0;
+- for ( sal_Int32 nPortion = 0; nPortion < GetParaPortions().Count(); nPortion++ )
+- {
+- ParaPortion* pPortion = GetParaPortions()[nPortion];
+- ContentNode* pNode = pPortion->GetNode();
+- OSL_ENSURE( pNode, "Invalid Node in Portion!" );
+- if ( pNode != aPaM.GetNode() )
++ const sal_Int32 nIndex = aPaM.GetIndex();
++ const ParaPortion* pPortion = nullptr;
++ const EditLine* pLastLine = nullptr;
++ tools::Rectangle aLineArea;
++
++ auto FindPortionLineAndArea
++ = [&, bEOL(bool(nFlags & GetCursorFlags::EndOfLine))](const LineAreaInfo& rInfo) {
++ if (!rInfo.pLine) // start of ParaPortion
+ {
+- nY += pPortion->GetHeight();
++ ContentNode* pNode = rInfo.rPortion.GetNode();
++ OSL_ENSURE(pNode, "Invalid Node in Portion!");
++ if (pNode != aPaM.GetNode())
++ return CallbackResult::SkipThisPortion;
++ pPortion = &rInfo.rPortion;
+ }
+- else
++ else // guaranteed that this is the correct ParaPortion
+ {
+- aEditCursor = GetEditCursor( pPortion, aPaM.GetIndex(), nFlags );
+- aEditCursor.AdjustTop(nY );
+- aEditCursor.AdjustBottom(nY );
+- return aEditCursor;
++ pLastLine = rInfo.pLine;
++ aLineArea = rInfo.aArea;
++ if ((rInfo.pLine->GetStart() == nIndex) || (rInfo.pLine->IsIn(nIndex, bEOL)))
++ return CallbackResult::Stop;
+ }
++ return CallbackResult::Continue;
++ };
++ IterateLineAreas(FindPortionLineAndArea, IterFlag::none);
++
++ if (pLastLine)
++ {
++ aEditCursor = GetEditCursor(pPortion, pLastLine, nIndex, nFlags);
++ aEditCursor.Move(getLeftDirectionAware(aLineArea), getTopDirectionAware(aLineArea));
+ }
+- OSL_FAIL( "Portion not found!" );
++ else
++ OSL_FAIL("Line not found!");
++
+ return aEditCursor;
+ }
+
++void ImpEditEngine::IterateLineAreas(const IterateLinesAreasFunc& f, IterFlag eOptions)
++{
++ const Point aOrigin(0, 0);
++ Point aLineStart(aOrigin);
++ const tools::Long nVertLineSpacing = CalcVertLineSpacing(aLineStart);
++ const tools::Long nColumnWidth = GetColumnWidth(aPaperSize);
++ sal_Int32 nColumn = 0;
++ for (sal_Int32 n = 0, nPortions = GetParaPortions().Count(); n < nPortions; ++n)
++ {
++ ParaPortion& rPortion = *GetParaPortions()[n];
++ bool bSkipThis = true;
++ if (rPortion.IsVisible())
++ {
++ // when typing idle formatting, asynchronous Paint. Invisible Portions may be invalid.
++ if (rPortion.IsInvalid())
++ return;
++
++ LineAreaInfo aInfo{
++ nColumn, // nColumn
++ rPortion, // rPortion
++ n, // nPortion
++ nullptr, // pLine
++ 0, // nLine
++ { aLineStart, Size{ nColumnWidth, rPortion.GetFirstLineOffset() } }, // aArea
++ 0 // nHeightNeededToNotWrap
++ };
++ auto eResult = f(aInfo);
++ if (eResult == CallbackResult::Stop)
++ return;
++ bSkipThis = eResult == CallbackResult::SkipThisPortion;
++
++ sal_uInt16 nSBL = 0;
++ if (!aStatus.IsOutliner())
++ {
++ const SvxLineSpacingItem& rLSItem
++ = rPortion.GetNode()->GetContentAttribs().GetItem(EE_PARA_SBL);
++ nSBL = (rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix)
++ ? GetYValue(rLSItem.GetInterLineSpace())
++ : 0;
++ }
++
++ adjustYDirectionAware(aLineStart, rPortion.GetFirstLineOffset());
++ for (sal_Int32 nLine = 0, nLines = rPortion.GetLines().Count(); nLine < nLines; nLine++)
++ {
++ EditLine& rLine = rPortion.GetLines()[nLine];
++ tools::Long nLineHeight = rLine.GetHeight();
++ if (nLine != nLines - 1)
++ nLineHeight += nVertLineSpacing;
++ MoveToNextLine(aLineStart, nLineHeight, nColumn, aOrigin,
++ &aInfo.nHeightNeededToNotWrap);
++ const bool bInclILS = eOptions & IterFlag::inclILS;
++ if (bInclILS && (nLine != nLines - 1) && !aStatus.IsOutliner())
++ {
++ adjustYDirectionAware(aLineStart, nSBL);
++ nLineHeight += nSBL;
++ }
++
++ if (!bSkipThis)
++ {
++ Point aOtherCorner(aLineStart);
++ adjustXDirectionAware(aOtherCorner, nColumnWidth);
++ adjustYDirectionAware(aOtherCorner, -nLineHeight);
++
++ // Calls to f() for each line
++ aInfo.nColumn = nColumn;
++ aInfo.pLine = &rLine;
++ aInfo.nLine = nLine;
++ aInfo.aArea = tools::Rectangle::Justify(aLineStart, aOtherCorner);
++ eResult = f(aInfo);
++ if (eResult == CallbackResult::Stop)
++ return;
++ bSkipThis = eResult == CallbackResult::SkipThisPortion;
++ }
++
++ if (!bInclILS && (nLine != nLines - 1) && !aStatus.IsOutliner())
++ adjustYDirectionAware(aLineStart, nSBL);
++ }
++ if (!aStatus.IsOutliner())
++ {
++ const SvxULSpaceItem& rULItem
++ = rPortion.GetNode()->GetContentAttribs().GetItem(EE_PARA_ULSPACE);
++ tools::Long nUL = GetYValue(rULItem.GetLower());
++ adjustYDirectionAware(aLineStart, nUL);
++ }
++ }
++ // Invisible ParaPortion has no height (see ParaPortion::GetHeight), don't handle it
++ }
++}
++
++std::tuple
++ImpEditEngine::GetPortionAndLine(Point aDocPos)
++{
++ // First find the column from the point
++ sal_Int32 nClickColumn = 0;
++ for (tools::Long nColumnStart = 0, nColumnWidth = GetColumnWidth(aPaperSize);;
++ nColumnStart += mnColumnSpacing + nColumnWidth, ++nClickColumn)
++ {
++ if (aDocPos.X() <= nColumnStart + nColumnWidth + mnColumnSpacing / 2)
++ break;
++ if (nClickColumn >= mnColumns - 1)
++ break;
++ }
++
++ const ParaPortion* pLastPortion = nullptr;
++ const EditLine* pLastLine = nullptr;
++ tools::Long nLineStartX = 0;
++
++ auto FindLastMatchingPortionAndLine = [&](const LineAreaInfo& rInfo) {
++ if (rInfo.pLine) // Only handle lines, not ParaPortion starts
++ {
++ if (rInfo.nColumn > nClickColumn)
++ return CallbackResult::Stop;
++ pLastPortion = &rInfo.rPortion; // Candidate paragraph
++ pLastLine = rInfo.pLine; // Last visible line not later than click position
++ nLineStartX = getLeftDirectionAware(rInfo.aArea);
++ if (rInfo.nColumn == nClickColumn && getBottomDirectionAware(rInfo.aArea) > aDocPos.Y())
++ return CallbackResult::Stop; // Found it
++ }
++ return CallbackResult::Continue;
++ };
++ IterateLineAreas(FindLastMatchingPortionAndLine, IterFlag::inclILS);
++
++ return { pLastPortion, pLastLine, nLineStartX };
++}
++
+ EditPaM ImpEditEngine::GetPaM( Point aDocPos, bool bSmart )
+ {
+ OSL_ENSURE( GetUpdateMode(), "Must not be reached when Update=FALSE: GetPaM" );
+
+- tools::Long nY = 0;
+- EditPaM aPaM;
+- sal_Int32 nPortion;
+- for ( nPortion = 0; nPortion < GetParaPortions().Count(); nPortion++ )
++ if (const auto& [pPortion, pLine, nLineStartX] = GetPortionAndLine(aDocPos); pPortion)
+ {
+- ParaPortion* pPortion = GetParaPortions()[nPortion];
+- const tools::Long nTmpHeight = pPortion->GetHeight(); // should also be correct for !bVisible!
+- nY += nTmpHeight;
+- if ( nY > aDocPos.Y() )
+- {
+- nY -= nTmpHeight;
+- aDocPos.AdjustY( -nY );
+- // Skip invisible Portions:
+- while ( pPortion && !pPortion->IsVisible() )
+- {
+- nPortion++;
+- pPortion = GetParaPortions().SafeGetObject( nPortion );
+- }
+- SAL_WARN_IF(!pPortion, "editeng", "worrying lack of any visible paragraph");
+- if (!pPortion)
+- return aPaM;
+- return GetPaM(pPortion, aDocPos, bSmart);
++ sal_Int32 nCurIndex
++ = GetChar(pPortion, pLine, aDocPos.X() - nLineStartX, bSmart);
++ EditPaM aPaM(pPortion->GetNode(), nCurIndex);
+
++ if (nCurIndex && (nCurIndex == pLine->GetEnd())
++ && (pLine != &pPortion->GetLines()[pPortion->GetLines().Count() - 1]))
++ {
++ aPaM = CursorLeft(aPaM);
+ }
++
++ return aPaM;
+ }
+- // Then search for the last visible:
+- nPortion = GetParaPortions().Count()-1;
+- while ( nPortion && !GetParaPortions()[nPortion]->IsVisible() )
+- nPortion--;
++ return {};
++}
+
+- OSL_ENSURE( GetParaPortions()[nPortion]->IsVisible(), "No visible paragraph found: GetPaM" );
+- aPaM.SetNode( GetParaPortions()[nPortion]->GetNode() );
+- aPaM.SetIndex( GetParaPortions()[nPortion]->GetNode()->Len() );
+- return aPaM;
++bool ImpEditEngine::IsTextPos(const Point& rDocPos, sal_uInt16 nBorder)
++{
++ if (const auto& [pPortion, pLine, nLineStartX] = GetPortionAndLine(rDocPos); pPortion)
++ {
++ Range aLineXPosStartEnd = GetLineXPosStartEnd(pPortion, pLine);
++ if ((rDocPos.X() >= nLineStartX + aLineXPosStartEnd.Min() - nBorder)
++ && (rDocPos.X() <= nLineStartX + aLineXPosStartEnd.Max() + nBorder))
++ return true;
++ }
++ return false;
+ }
+
+ sal_uInt32 ImpEditEngine::GetTextHeight() const
+@@ -3236,28 +3434,114 @@ sal_uInt32 ImpEditEngine::GetTextHeightNTP() const
+ return nCurTextHeightNTP;
+ }
+
+-sal_uInt32 ImpEditEngine::CalcTextHeight( sal_uInt32* pHeightNTP )
++tools::Long ImpEditEngine::Calc1ColumnTextHeight(tools::Long* pHeightNTP)
+ {
+- OSL_ENSURE( GetUpdateMode(), "Should not be used when Update=FALSE: CalcTextHeight" );
+- sal_uInt32 nY = 0;
+- sal_uInt32 nPH;
+- sal_uInt32 nEmptyHeight = 0;
+- for ( sal_Int32 nPortion = 0; nPortion < GetParaPortions().Count(); nPortion++ ) {
+- ParaPortion* pPortion = GetParaPortions()[nPortion];
+- nPH = pPortion->GetHeight();
+- nY += nPH;
+- if( pHeightNTP ) {
+- if ( pPortion->IsEmpty() )
+- nEmptyHeight += nPH;
+- else
+- nEmptyHeight = 0;
++ tools::Long nHeight = 0;
++ // Pretend that we have ~infinite height to get total height
++ comphelper::ValueRestorationGuard aGuard(nCurTextHeight,
++ std::numeric_limits::max());
++
++ auto FindLastLineBottom = [&](const LineAreaInfo& rInfo) {
++ if (rInfo.pLine)
++ {
++ nHeight = getBottomDirectionAware(rInfo.aArea) + 1;
++ if (pHeightNTP && !rInfo.rPortion.IsEmpty())
++ *pHeightNTP = nHeight;
+ }
+- }
++ return CallbackResult::Continue;
++ };
++ IterateLineAreas(FindLastLineBottom, IterFlag::none);
++ return nHeight;
++}
+
+- if ( pHeightNTP )
+- *pHeightNTP = nY - nEmptyHeight;
++tools::Long ImpEditEngine::CalcTextHeight(tools::Long* pHeightNTP)
++{
++ OSL_ENSURE( GetUpdateMode(), "Should not be used when Update=FALSE: CalcTextHeight" );
+
+- return nY;
++ if (mnColumns <= 1)
++ return Calc1ColumnTextHeight(pHeightNTP); // All text fits into a single column - done!
++
++ // The final column height can be smaller than total height divided by number of columns (taking
++ // into account first line offset and interline spacing, that aren't considered in positioning
++ // after the wrap). The wrap should only happen after the minimal height is exceeded.
++ tools::Long nTentativeColHeight = mnMinColumnWrapHeight;
++ tools::Long nWantedIncrease = 0;
++ tools::Long nCurrentTextHeight;
++
++ // This does the necessary column balancing for the case when the text does not fit min height.
++ // When the height of column (taken from nCurTextHeight) is too small, the last column will
++ // overflow, so the resulting height of the text will exceed the set column height. Increasing
++ // the column height step by step by the minimal value that allows one of columns to accomodate
++ // one line more, we finally get to the point where all the text fits. At each iteration, the
++ // height is only increased, so it's impossible to have infinite layout loops. The found value
++ // is the global minimum.
++ //
++ // E.g., given the following four line heights:
++ // Line 1: 10;
++ // Line 2: 12;
++ // Line 3: 10;
++ // Line 4: 10;
++ // number of columns 3, and the minimal paper height of 5, the iterations would be:
++ // * Tentative column height is set to 5
++ //
++ // * Line 1 is attempted to go to column 0. Overflow is 5 => moved to column 1.
++ // * Line 2 is attempted to go to column 1 after Line 1; overflow is 17 => moved to column 2.
++ // * Line 3 is attempted to go to column 2 after Line 2; overflow is 17, stays in max column 2.
++ // * Line 4 goes to column 2 after Line 3.
++ // * Final iteration columns are: {empty}, {Line 1}, {Line 2, Line 3, Line 4}
++ // * Total text height is max({0, 10, 32}) == 32 > Tentative column height 5 => NEXT ITERATION
++ // * Minimal height increase that allows at least one column to accomodate one more line is
++ // min({5, 17, 17}) = 5.
++ // * Tentative column height is set to 5 + 5 = 10.
++ //
++ // * Line 1 goes to column 0, no overflow.
++ // * Line 2 is attempted to go to column 0 after Line 1; overflow is 12 => moved to column 1.
++ // * Line 3 is attempted to go to column 1 after Line 2; overflow is 12 => moved to column 2.
++ // * Line 4 is attempted to go to column 2 after Line 3; overflow is 10, stays in max column 2.
++ // * Final iteration columns are: {Line 1}, {Line 2}, {Line 3, Line 4}
++ // * Total text height is max({10, 12, 20}) == 20 > Tentative column height 10 => NEXT ITERATION
++ // * Minimal height increase that allows at least one column to accomodate one more line is
++ // min({12, 12, 10}) = 10.
++ // * Tentative column height is set to 10 + 10 == 20.
++ //
++ // * Line 1 goes to column 0, no overflow.
++ // * Line 2 is attempted to go to column 0 after Line 1; overflow is 2 => moved to column 1.
++ // * Line 3 is attempted to go to column 1 after Line 2; overflow is 2 => moved to column 2.
++ // * Line 4 is attempted to go to column 2 after Line 3; no overflow.
++ // * Final iteration columns are: {Line 1}, {Line 2}, {Line 3, Line 4}
++ // * Total text height is max({10, 12, 20}) == 20 == Tentative column height 20 => END.
++ do
++ {
++ nTentativeColHeight += nWantedIncrease;
++ nWantedIncrease = std::numeric_limits::max();
++ nCurrentTextHeight = 0;
++ auto GetHeightAndWantedIncrease = [&, minHeight = tools::Long(0), lastCol = sal_Int32(0)](
++ const LineAreaInfo& rInfo) mutable {
++ if (rInfo.pLine)
++ {
++ if (lastCol != rInfo.nColumn)
++ {
++ minHeight = std::max(nCurrentTextHeight,
++ minHeight); // total height can't be less than previous columns
++ nWantedIncrease = std::min(rInfo.nHeightNeededToNotWrap, nWantedIncrease);
++ }
++ lastCol = rInfo.nColumn;
++ nCurrentTextHeight = std::max(getBottomDirectionAware(rInfo.aArea) + 1, minHeight);
++ if (pHeightNTP)
++ {
++ if (rInfo.rPortion.IsEmpty())
++
++ *pHeightNTP = std::max(*pHeightNTP, minHeight);
++ else
++ *pHeightNTP = nCurrentTextHeight;
++ }
++ }
++ return CallbackResult::Continue;
++ };
++ comphelper::ValueRestorationGuard aGuard(nCurTextHeight, nTentativeColHeight);
++ IterateLineAreas(GetHeightAndWantedIncrease, IterFlag::none);
++ } while (nCurrentTextHeight > nTentativeColHeight && nWantedIncrease > 0);
++ return nCurrentTextHeight;
+ }
+
+ sal_Int32 ImpEditEngine::GetLineCount( sal_Int32 nParagraph ) const
+@@ -3683,62 +3967,6 @@ Range ImpEditEngine::GetInvalidYOffsets( ParaPortion* pPortion )
+ return aRange;
+ }
+
+-EditPaM ImpEditEngine::GetPaM( ParaPortion* pPortion, Point aDocPos, bool bSmart )
+-{
+- OSL_ENSURE( pPortion->IsVisible(), "Why GetPaM() for an invisible paragraph?" );
+- OSL_ENSURE( IsFormatted(), "GetPaM: Not formatted" );
+-
+- sal_Int32 nCurIndex = 0;
+- EditPaM aPaM;
+- aPaM.SetNode( pPortion->GetNode() );
+-
+- const SvxLineSpacingItem& rLSItem = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_SBL );
+- sal_uInt16 nSBL = ( rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix )
+- ? GetYValue( rLSItem.GetInterLineSpace() ) : 0;
+-
+- tools::Long nY = pPortion->GetFirstLineOffset();
+-
+- OSL_ENSURE( pPortion->GetLines().Count(), "Empty ParaPortion in GetPaM!" );
+-
+- const EditLine* pLine = nullptr;
+- for ( sal_Int32 nLine = 0; nLine < pPortion->GetLines().Count(); nLine++ )
+- {
+- const EditLine& rTmpLine = pPortion->GetLines()[nLine];
+- nY += rTmpLine.GetHeight();
+- if ( !aStatus.IsOutliner() )
+- nY += nSBL;
+- if ( nY > aDocPos.Y() )
+- {
+- pLine = &rTmpLine;
+- break; // correct Y-position is not of interest
+- }
+-
+- nCurIndex = nCurIndex + rTmpLine.GetLen();
+- }
+-
+- if ( !pLine ) // may happen only in the range of SA!
+- {
+-#if OSL_DEBUG_LEVEL > 0
+- const SvxULSpaceItem& rULSpace = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_ULSPACE );
+- OSL_ENSURE( nY+GetYValue( rULSpace.GetLower() ) >= aDocPos.Y() , "Index in no line, GetPaM ?" );
+-#endif
+- aPaM.SetIndex( pPortion->GetNode()->Len() );
+- return aPaM;
+- }
+-
+- // If no line found, only just X-Position => Index
+- nCurIndex = GetChar( pPortion, pLine, aDocPos.X(), bSmart );
+- aPaM.SetIndex( nCurIndex );
+-
+- if ( nCurIndex && ( nCurIndex == pLine->GetEnd() ) &&
+- ( pLine != &pPortion->GetLines()[pPortion->GetLines().Count()-1] ) )
+- {
+- aPaM = CursorLeft( aPaM );
+- }
+-
+- return aPaM;
+-}
+-
+ sal_Int32 ImpEditEngine::GetChar(
+ const ParaPortion* pParaPortion, const EditLine* pLine, tools::Long nXPos, bool bSmart)
+ {
+@@ -4202,91 +4430,6 @@ void ImpEditEngine::CalcHeight( ParaPortion* pPortion )
+ }
+ }
+
+-tools::Rectangle ImpEditEngine::GetEditCursor( ParaPortion* pPortion, sal_Int32 nIndex, GetCursorFlags nFlags )
+-{
+- OSL_ENSURE( pPortion->IsVisible(), "Why GetEditCursor() for an invisible paragraph?" );
+- OSL_ENSURE( IsFormatted() || GetTextRanger(), "GetEditCursor: Not formatted" );
+-
+- /*
+- GetCursorFlags::EndOfLine: If after the last character of a wrapped line, remaining
+- at the end of the line, not the beginning of the next one.
+- Purpose: - END => really after the last character
+- - Selection...
+- */
+-
+- tools::Long nY = pPortion->GetFirstLineOffset();
+-
+- const SvxLineSpacingItem& rLSItem = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_SBL );
+- sal_uInt16 nSBL = ( rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix )
+- ? GetYValue( rLSItem.GetInterLineSpace() ) : 0;
+-
+- sal_Int32 nCurIndex = 0;
+- sal_Int32 nLineCount = pPortion->GetLines().Count();
+- OSL_ENSURE( nLineCount, "Empty ParaPortion in GetEditCursor!" );
+- if (nLineCount == 0)
+- return tools::Rectangle();
+- const EditLine* pLine = nullptr;
+- bool bEOL( nFlags & GetCursorFlags::EndOfLine );
+- for (sal_Int32 nLine = 0; nLine < nLineCount; ++nLine)
+- {
+- const EditLine& rTmpLine = pPortion->GetLines()[nLine];
+- if ( ( rTmpLine.GetStart() == nIndex ) || ( rTmpLine.IsIn( nIndex, bEOL ) ) )
+- {
+- pLine = &rTmpLine;
+- break;
+- }
+-
+- nCurIndex = nCurIndex + rTmpLine.GetLen();
+- nY += rTmpLine.GetHeight();
+- if ( !aStatus.IsOutliner() )
+- nY += nSBL;
+- }
+- if ( !pLine )
+- {
+- // Cursor at the End of the paragraph.
+- OSL_ENSURE( nIndex == nCurIndex, "Index dead wrong in GetEditCursor!" );
+-
+- pLine = &pPortion->GetLines()[nLineCount-1];
+- nY -= pLine->GetHeight();
+- if ( !aStatus.IsOutliner() )
+- nY -= nSBL;
+- }
+-
+- tools::Rectangle aEditCursor;
+-
+- aEditCursor.SetTop( nY );
+- nY += pLine->GetHeight();
+- aEditCursor.SetBottom( nY-1 );
+-
+- // Search within the line...
+- tools::Long nX;
+-
+- if ( ( nIndex == pLine->GetStart() ) && ( nFlags & GetCursorFlags::StartOfLine ) )
+- {
+- Range aXRange = GetLineXPosStartEnd( pPortion, pLine );
+- nX = !IsRightToLeft( GetEditDoc().GetPos( pPortion->GetNode() ) ) ? aXRange.Min() : aXRange.Max();
+- }
+- else if ( ( nIndex == pLine->GetEnd() ) && ( nFlags & GetCursorFlags::EndOfLine ) )
+- {
+- Range aXRange = GetLineXPosStartEnd( pPortion, pLine );
+- nX = !IsRightToLeft( GetEditDoc().GetPos( pPortion->GetNode() ) ) ? aXRange.Max() : aXRange.Min();
+- }
+- else
+- {
+- nX = GetXPos( pPortion, pLine, nIndex, bool( nFlags & GetCursorFlags::PreferPortionStart ) );
+- }
+-
+- aEditCursor.SetLeft(nX);
+- aEditCursor.SetRight(nX);
+-
+- if ( nFlags & GetCursorFlags::TextOnly )
+- aEditCursor.SetTop( aEditCursor.Bottom() - pLine->GetTxtHeight() + 1 );
+- else
+- aEditCursor.SetTop( aEditCursor.Bottom() - std::min( pLine->GetTxtHeight(), pLine->GetHeight() ) + 1 );
+-
+- return aEditCursor;
+-}
+-
+ void ImpEditEngine::SetValidPaperSize( const Size& rNewSz )
+ {
+ aPaperSize = rNewSz;
+diff --git a/editeng/source/editeng/impedit3.cxx b/editeng/source/editeng/impedit3.cxx
+index 7df8aaa92bf4..e74958031a96 100644
+--- a/editeng/source/editeng/impedit3.cxx
++++ b/editeng/source/editeng/impedit3.cxx
+@@ -69,6 +69,7 @@
+ #include
+ #include
+ #include
++#include
+
+ #include
+
+@@ -374,8 +375,8 @@ void ImpEditEngine::FormatDoc()
+
+ // Here already, so that not always in CreateLines...
+ bool bMapChanged = ImpCheckRefMapMode();
++ std::set aRepaintParas;
+
+- aInvalidRect = tools::Rectangle(); // make empty
+ for ( sal_Int32 nPara = 0; nPara < GetParaPortions().Count(); nPara++ )
+ {
+ ParaPortion* pParaPortion = GetParaPortions()[nPara];
+@@ -410,46 +411,21 @@ void ImpEditEngine::FormatDoc()
+ pParaPortion->SetMustRepaint( false );
+ }
+
+- // InvalidRect set only once...
+- if ( aInvalidRect.IsEmpty() )
+- {
+- // For Paperwidth 0 (AutoPageSize) it would otherwise be Empty()...
+- tools::Long nWidth = std::max( tools::Long(1), ( !IsVertical() ? aPaperSize.Width() : aPaperSize.Height() ) );
+- Range aInvRange( GetInvalidYOffsets( pParaPortion ) );
+- aInvalidRect = tools::Rectangle( Point( 0, nY+aInvRange.Min() ),
+- Size( nWidth, aInvRange.Len() ) );
+- }
+- else
+- {
+- aInvalidRect.SetBottom( nY + pParaPortion->GetHeight() );
+- }
+- }
+- else if ( bGrow )
+- {
+- aInvalidRect.SetBottom( nY + pParaPortion->GetHeight() );
++ aRepaintParas.insert(nPara);
+ }
+ nY += pParaPortion->GetHeight();
+ }
+
++ aInvalidRect = tools::Rectangle(); // make empty
++
+ // One can also get into the formatting through UpdateMode ON=>OFF=>ON...
+ // enable optimization first after Vobis delivery...
+ {
+- sal_uInt32 nNewHeightNTP;
+- sal_uInt32 nNewHeight = CalcTextHeight( &nNewHeightNTP );
++ tools::Long nNewHeightNTP;
++ tools::Long nNewHeight = CalcTextHeight(&nNewHeightNTP);
+ tools::Long nDiff = nNewHeight - nCurTextHeight;
+ if ( nDiff )
+ aStatus.GetStatusWord() |= !IsVertical() ? EditStatusFlags::TextHeightChanged : EditStatusFlags::TEXTWIDTHCHANGED;
+- if ( nNewHeight < nCurTextHeight )
+- {
+- aInvalidRect.SetBottom( static_cast(std::max( nNewHeight, nCurTextHeight )) );
+- if ( aInvalidRect.IsEmpty() )
+- {
+- aInvalidRect.SetTop( 0 );
+- // Left and Right are not evaluated, are however set due to IsEmpty.
+- aInvalidRect.SetLeft( 0 );
+- aInvalidRect.SetRight( !IsVertical() ? aPaperSize.Width() : aPaperSize.Height() );
+- }
+- }
+
+ nCurTextHeight = nNewHeight;
+ nCurTextHeightNTP = nNewHeightNTP;
+@@ -473,6 +449,20 @@ void ImpEditEngine::FormatDoc()
+ }
+ }
+ }
++
++ if (nDiff)
++ aInvalidRect.Union(tools::Rectangle::Justify(
++ { 0, nNewHeight }, { getWidthDirectionAware(aPaperSize), nCurTextHeight }));
++
++ if (!aRepaintParas.empty())
++ {
++ auto CombineRepaintParasAreas = [&](const LineAreaInfo& rInfo) {
++ if (aRepaintParas.count(rInfo.nPortion))
++ aInvalidRect.Union(rInfo.aArea);
++ return CallbackResult::Continue;
++ };
++ IterateLineAreas(CombineRepaintParasAreas, IterFlag::inclILS);
++ }
+ }
+
+ bIsFormatting = false;
+@@ -566,10 +556,10 @@ void ImpEditEngine::CheckPageOverflow()
+ {
+ SAL_INFO("editeng.chaining", "[CONTROL_STATUS] AutoPageSize is " << (( aStatus.GetControlWord() & EEControlBits::AUTOPAGESIZE ) ? "ON" : "OFF") );
+
+- sal_uInt32 nBoxHeight = GetMaxAutoPaperSize().Height();
++ tools::Long nBoxHeight = GetMaxAutoPaperSize().Height();
+ SAL_INFO("editeng.chaining", "[OVERFLOW-CHECK] Current MaxAutoPaperHeight is " << nBoxHeight);
+
+- sal_uInt32 nTxtHeight = CalcTextHeight(nullptr);
++ tools::Long nTxtHeight = CalcTextHeight(nullptr);
+ SAL_INFO("editeng.chaining", "[OVERFLOW-CHECK] Current Text Height is " << nTxtHeight);
+
+ sal_uInt32 nParaCount = GetParaPortions().Count();
+@@ -598,6 +588,13 @@ static sal_Int32 ImplCalculateFontIndependentLineSpacing( const sal_Int32 nFontH
+ return ( nFontHeight * 12 ) / 10; // + 20%
+ }
+
++tools::Long ImpEditEngine::GetColumnWidth(const Size& rPaperSize) const
++{
++ assert(mnColumns >= 1);
++ tools::Long nWidth = IsVertical() ? rPaperSize.Height() : rPaperSize.Width();
++ return (nWidth - mnColumnSpacing * (mnColumns - 1)) / mnColumns;
++}
++
+ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY )
+ {
+ ParaPortion* pParaPortion = GetParaPortions()[nPara];
+@@ -785,11 +782,8 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY )
+ }
+ }
+
+- tools::Long nMaxLineWidth;
+- if ( !IsVertical() )
+- nMaxLineWidth = aStatus.AutoPageWidth() ? aMaxAutoPaperSize.Width() : aPaperSize.Width();
+- else
+- nMaxLineWidth = aStatus.AutoPageHeight() ? aMaxAutoPaperSize.Height() : aPaperSize.Height();
++ const bool bAutoSize = IsVertical() ? aStatus.AutoPageHeight() : aStatus.AutoPageWidth();
++ tools::Long nMaxLineWidth = GetColumnWidth(bAutoSize ? aMaxAutoPaperSize : aPaperSize);
+
+ nMaxLineWidth -= GetXValue( rLRItem.GetRight() );
+ nMaxLineWidth -= nStartX;
+@@ -797,7 +791,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY )
+ // If PaperSize == long_max, one cannot take away any negative
+ // first line indent. (Overflow)
+ if ( ( nMaxLineWidth < 0 ) && ( nStartX < 0 ) )
+- nMaxLineWidth = ( !IsVertical() ? aPaperSize.Width() : aPaperSize.Height() ) - GetXValue( rLRItem.GetRight() );
++ nMaxLineWidth = GetColumnWidth(aPaperSize) - GetXValue(rLRItem.GetRight());
+
+ // If still less than 0, it may be just the right edge.
+ if ( nMaxLineWidth <= 0 )
+@@ -824,7 +818,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY )
+ {
+ GetTextRanger()->SetVertical( IsVertical() );
+
+- tools::Long nTextY = nStartPosY + GetEditCursor( pParaPortion, pLine->GetStart() ).Top();
++ tools::Long nTextY = nStartPosY + GetEditCursor( pParaPortion, pLine, pLine->GetStart(), GetCursorFlags::NONE ).Top();
+ if ( !bSameLineAgain )
+ {
+ SeekCursor( pNode, nTmpPos+1, aTmpFont );
+@@ -885,7 +879,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY )
+ nTextExtraYOffset += std::max( static_cast(nTextLineHeight / 10), tools::Long(1) );
+ if ( ( nTextY + nTextExtraYOffset ) > GetTextRanger()->GetBoundRect().Bottom() )
+ {
+- nXWidth = !IsVertical() ? GetPaperSize().Width() : GetPaperSize().Height();
++ nXWidth = getWidthDirectionAware(GetPaperSize());
+ if ( !nXWidth ) // AutoPaperSize
+ nXWidth = 0x7FFFFFFF;
+ }
+@@ -1468,7 +1462,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY )
+ // has to be used for the Alignment. If it does not fit or if it
+ // will change the paper width, it will be formatted again for
+ // Justification! = LEFT anyway.
+- tools::Long nMaxLineWidthFix = ( !IsVertical() ? aPaperSize.Width() : aPaperSize.Height() )
++ tools::Long nMaxLineWidthFix = GetColumnWidth(aPaperSize)
+ - GetXValue( rLRItem.GetRight() ) - nStartX;
+ if ( aTextSize.Width() < nMaxLineWidthFix )
+ nMaxLineWidth = nMaxLineWidthFix;
+@@ -1725,7 +1719,7 @@ void ImpEditEngine::CreateAndInsertEmptyLine( ParaPortion* pParaPortion )
+ {
+ sal_Int32 nPara = GetParaPortions().GetPos( pParaPortion );
+ SvxAdjust eJustification = GetJustification( nPara );
+- tools::Long nMaxLineWidth = !IsVertical() ? aPaperSize.Width() : aPaperSize.Height();
++ tools::Long nMaxLineWidth = GetColumnWidth(aPaperSize);
+ nMaxLineWidth -= GetXValue( rLRItem.GetRight() );
+ if ( nMaxLineWidth < 0 )
+ nMaxLineWidth = 1;
+@@ -2636,6 +2630,20 @@ void ImpEditEngine::SetRotation(TextRotation nRotation)
+ }
+ }
+
++void ImpEditEngine::SetTextColumns(sal_Int16 nColumns, sal_Int32 nSpacing)
++{
++ if (mnColumns != nColumns || mnColumnSpacing != nSpacing)
++ {
++ mnColumns = nColumns;
++ mnColumnSpacing = nSpacing;
++ if (IsFormatted())
++ {
++ FormatFullDoc();
++ UpdateViews(GetActiveView());
++ }
++ }
++}
++
+ void ImpEditEngine::SetFixedCellHeight( bool bUseFixedCellHeight )
+ {
+ if ( IsFixedCellHeight() != bUseFixedCellHeight )
+@@ -2949,6 +2957,177 @@ void ImpEditEngine::RecalcFormatterFontMetrics( FormatterFontMetric& rCurMetrics
+ }
+ }
+
++tools::Long ImpEditEngine::getXDirectionAware(const Point& pt) const
++{
++ if (!IsVertical())
++ return pt.X();
++ else
++ return pt.Y();
++}
++
++tools::Long ImpEditEngine::getYDirectionAware(const Point& pt) const
++{
++ if (!IsVertical())
++ return pt.Y();
++ else
++ return pt.X();
++}
++
++tools::Long ImpEditEngine::getWidthDirectionAware(const Size& sz) const
++{
++ return !IsVertical() ? sz.Width() : sz.Height();
++}
++
++tools::Long ImpEditEngine::getHeightDirectionAware(const Size& sz) const
++{
++ return !IsVertical() ? sz.Height() : sz.Width();
++}
++
++void ImpEditEngine::adjustXDirectionAware(Point& pt, tools::Long x) const
++{
++ if (!IsVertical())
++ pt.AdjustX(x);
++ else
++ pt.AdjustY(IsTopToBottom() ? x : -x);
++}
++
++void ImpEditEngine::adjustYDirectionAware(Point& pt, tools::Long y) const
++{
++ if (!IsVertical())
++ pt.AdjustY(y);
++ else
++ pt.AdjustX(IsTopToBottom() ? -y : y);
++}
++
++void ImpEditEngine::setXDirectionAware(Point& pt, tools::Long x) const
++{
++ if (!IsVertical())
++ pt.setX(x);
++ else
++ pt.setY(x);
++}
++
++void ImpEditEngine::setYDirectionAware(Point& pt, tools::Long y) const
++{
++ if (!IsVertical())
++ pt.setY(y);
++ else
++ pt.setX(y);
++}
++
++tools::Long ImpEditEngine::getYOverflowDirectionAware(const Point& pt,
++ const tools::Rectangle& rectMax) const
++{
++ tools::Long nRes;
++ if (!IsVertical())
++ nRes = pt.Y() - rectMax.Bottom();
++ else if (IsTopToBottom())
++ nRes = rectMax.Left() - pt.X();
++ else
++ nRes = pt.X() - rectMax.Right();
++ return std::max(nRes, tools::Long(0));
++}
++
++bool ImpEditEngine::isXOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const
++{
++ if (!IsVertical())
++ return pt.X() > rectMax.Right();
++
++ if (IsTopToBottom())
++ return pt.Y() > rectMax.Bottom();
++ else
++ return pt.Y() < rectMax.Top();
++}
++
++tools::Long ImpEditEngine::getLeftDirectionAware(const tools::Rectangle& rect) const
++{
++ if (!IsVertical())
++ return rect.Left();
++
++ if (IsTopToBottom())
++ return rect.Top();
++ else
++ return rect.Bottom();
++}
++
++tools::Long ImpEditEngine::getRightDirectionAware(const tools::Rectangle& rect) const
++{
++ if (!IsVertical())
++ return rect.Right();
++
++ if (IsTopToBottom())
++ return rect.Bottom();
++ else
++ return rect.Top();
++}
++
++tools::Long ImpEditEngine::getTopDirectionAware(const tools::Rectangle& rect) const
++{
++ if (!IsVertical())
++ return rect.Top();
++
++ if (IsTopToBottom())
++ return rect.Right();
++ else
++ return rect.Left();
++}
++
++tools::Long ImpEditEngine::getBottomDirectionAware(const tools::Rectangle& rect) const
++{
++ if (!IsVertical())
++ return rect.Bottom();
++
++ if (IsTopToBottom())
++ return rect.Left();
++ else
++ return rect.Right();
++}
++
++// Returns the resulting shift for the point; allows to apply the same shift to other points
++Point ImpEditEngine::MoveToNextLine(
++ Point& rMovePos, // [in, out] Point that will move to the next line
++ tools::Long nLineHeight, // [in] Y-direction move distance (direction-aware)
++ sal_Int32& rColumn, // [in, out] current column number
++ Point aOrigin, // [in] Origin point to calculate limits and initial Y position in a new column
++ tools::Long* pnHeightNeededToNotWrap // On column wrap, returns how much more height is needed
++) const
++{
++ const Point aOld = rMovePos;
++
++ // Move the point by the requested distance in Y direction
++ adjustYDirectionAware(rMovePos, nLineHeight);
++ // Check if the resulting position has moved beyond the limits, and more columns left.
++ // The limits are defined by a rectangle starting from aOrigin with width of aPaperSize
++ // and height of nCurTextHeight
++ Size aActPaperSize(aPaperSize);
++ if (IsVertical())
++ aActPaperSize.setWidth(nCurTextHeight);
++ else
++ aActPaperSize.setHeight(nCurTextHeight);
++ tools::Long nNeeded = getYOverflowDirectionAware(rMovePos, { aOrigin, aActPaperSize });
++ if (pnHeightNeededToNotWrap)
++ *pnHeightNeededToNotWrap = nNeeded;
++ if (nNeeded && rColumn < mnColumns)
++ {
++ ++rColumn;
++ // If we didn't fit into the last column, indicate that only by setting the column number
++ // to the total number of columns; do not adjust
++ if (rColumn < mnColumns)
++ {
++ // Set Y position of the point to that of aOrigin
++ setYDirectionAware(rMovePos, getYDirectionAware(aOrigin));
++ // Move the point by the requested distance in Y direction
++ adjustYDirectionAware(rMovePos, nLineHeight);
++ // Move the point by the column+spacing distance in X direction
++ adjustXDirectionAware(rMovePos, GetColumnWidth(aPaperSize) + mnColumnSpacing);
++ }
++ }
++
++ return rMovePos - aOld;
++}
++
++// TODO: use IterateLineAreas in ImpEditEngine::Paint, to avoid algorithm duplication
++
+ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Point aStartPos, bool bStripOnly, Degree10 nOrientation )
+ {
+ if ( !GetUpdateMode() && !bStripOnly )
+@@ -2987,6 +3166,7 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+
+ tools::Long nVertLineSpacing = CalcVertLineSpacing(aStartPos);
+
++ sal_Int32 nColumn = 0;
+
+ // Over all the paragraphs...
+
+@@ -3018,17 +3198,7 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+
+ bool bEndOfParagraphWritten(false);
+
+- if ( !IsVertical() )
+- aStartPos.AdjustY(pPortion->GetFirstLineOffset() );
+- else
+- {
+- if( IsTopToBottom() )
+- aStartPos.AdjustX( -(pPortion->GetFirstLineOffset()) );
+- else
+- aStartPos.AdjustX(pPortion->GetFirstLineOffset() );
+- }
+-
+- Point aParaStart( aStartPos );
++ adjustYDirectionAware(aStartPos, pPortion->GetFirstLineOffset());
+
+ const SvxLineSpacingItem& rLSItem = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_SBL );
+ sal_uInt16 nSBL = ( rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix )
+@@ -3040,34 +3210,13 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ pLine = &pPortion->GetLines()[nLine];
+ nIndex = pLine->GetStart();
+ DBG_ASSERT( pLine, "NULL-Pointer in the line iterator in UpdateViews" );
++ tools::Long nLineHeight = pLine->GetHeight();
++ if (nLine != nLastLine)
++ nLineHeight += nVertLineSpacing;
++ MoveToNextLine(aStartPos, nLineHeight, nColumn, aOrigin);
+ aTmpPos = aStartPos;
+- if ( !IsVertical() )
+- {
+- aTmpPos.AdjustX(pLine->GetStartPosX() );
+- aTmpPos.AdjustY(pLine->GetMaxAscent() );
+- aStartPos.AdjustY(pLine->GetHeight() );
+- if (nLine != nLastLine)
+- aStartPos.AdjustY(nVertLineSpacing );
+- }
+- else
+- {
+- if ( IsTopToBottom() )
+- {
+- aTmpPos.AdjustY(pLine->GetStartPosX() );
+- aTmpPos.AdjustX( -(pLine->GetMaxAscent()) );
+- aStartPos.AdjustX( -(pLine->GetHeight()) );
+- if (nLine != nLastLine)
+- aStartPos.AdjustX( -nVertLineSpacing );
+- }
+- else
+- {
+- aTmpPos.AdjustY( -(pLine->GetStartPosX()) );
+- aTmpPos.AdjustX(pLine->GetMaxAscent() );
+- aStartPos.AdjustX(pLine->GetHeight() );
+- if (nLine != nLastLine)
+- aStartPos.AdjustX(nVertLineSpacing );
+- }
+- }
++ adjustXDirectionAware(aTmpPos, pLine->GetStartPosX());
++ adjustYDirectionAware(aTmpPos, pLine->GetMaxAscent() - nLineHeight);
+
+ if ( ( !IsVertical() && ( aStartPos.Y() > aClipRect.Top() ) )
+ || ( IsVertical() && IsTopToBottom() && aStartPos.X() < aClipRect.Right() )
+@@ -3081,7 +3230,9 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ // does, too. No change for not-layouting (painting).
+ if(0 == nLine) // && !bStripOnly)
+ {
+- GetEditEnginePtr()->PaintingFirstLine( n, aParaStart, aTmpPos.Y(), aOrigin, nOrientation, pOutDev );
++ Point aLineStart(aStartPos);
++ adjustYDirectionAware(aLineStart, -nLineHeight);
++ GetEditEnginePtr()->PaintingFirstLine( n, aLineStart, aTmpPos.Y(), aOrigin, nOrientation, pOutDev );
+
+ // Remember whether a bullet was painted.
+ const SfxBoolItem& rBulletState = pEditEngine->GetParaAttrib(n, EE_PARA_BULLETSTATE);
+@@ -3100,27 +3251,10 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ const TextPortion& rTextPortion = pPortion->GetTextPortions()[nPortion];
+
+ tools::Long nPortionXOffset = GetPortionXOffset( pPortion, pLine, nPortion );
+- if ( !IsVertical() )
+- {
+- aTmpPos.setX( aStartPos.X() + nPortionXOffset );
+- if ( aTmpPos.X() > aClipRect.Right() )
+- break; // No further output in line necessary
+- }
+- else
+- {
+- if( IsTopToBottom() )
+- {
+- aTmpPos.setY( aStartPos.Y() + nPortionXOffset );
+- if ( aTmpPos.Y() > aClipRect.Bottom() )
+- break; // No further output in line necessary
+- }
+- else
+- {
+- aTmpPos.setY( aStartPos.Y() - nPortionXOffset );
+- if (aTmpPos.Y() < aClipRect.Top())
+- break; // No further output in line necessary
+- }
+- }
++ setXDirectionAware(aTmpPos, getXDirectionAware(aStartPos));
++ adjustXDirectionAware(aTmpPos, nPortionXOffset);
++ if (isXOverflowDirectionAware(aTmpPos, aClipRect))
++ break; // No further output in line necessary
+
+ switch ( rTextPortion.GetKind() )
+ {
+@@ -3208,44 +3342,12 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ const tools::Long nAdvanceY = -pLine->GetMaxAscent();
+
+ Point aTopLeftRectPos( aTmpPos );
+- if ( !IsVertical() )
+- {
+- aTopLeftRectPos.AdjustX(nAdvanceX );
+- aTopLeftRectPos.AdjustY(nAdvanceY );
+- }
+- else
+- {
+- if( IsTopToBottom() )
+- {
+- aTopLeftRectPos.AdjustY( -nAdvanceX );
+- aTopLeftRectPos.AdjustX(nAdvanceY );
+- }
+- else
+- {
+- aTopLeftRectPos.AdjustY(nAdvanceX );
+- aTopLeftRectPos.AdjustX( -nAdvanceY );
+- }
+- }
++ adjustXDirectionAware(aTopLeftRectPos, nAdvanceX);
++ adjustYDirectionAware(aTopLeftRectPos, nAdvanceY);
+
+ Point aBottomRightRectPos( aTopLeftRectPos );
+- if ( !IsVertical() )
+- {
+- aBottomRightRectPos.AdjustX(2 * nHalfBlankWidth );
+- aBottomRightRectPos.AdjustY(pLine->GetHeight() );
+- }
+- else
+- {
+- if (IsTopToBottom())
+- {
+- aBottomRightRectPos.AdjustX(pLine->GetHeight() );
+- aBottomRightRectPos.AdjustY( -(2 * nHalfBlankWidth) );
+- }
+- else
+- {
+- aBottomRightRectPos.AdjustX( -(pLine->GetHeight()) );
+- aBottomRightRectPos.AdjustY(2 * nHalfBlankWidth );
+- }
+- }
++ adjustXDirectionAware(aBottomRightRectPos, 2 * nHalfBlankWidth);
++ adjustYDirectionAware(aBottomRightRectPos, pLine->GetHeight());
+
+ pOutDev->Push( PushFlags::FILLCOLOR );
+ pOutDev->Push( PushFlags::LINECOLOR );
+@@ -3271,17 +3373,8 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ const Size aSlashSize = aTmpFont.QuickGetTextSize( pOutDev, aSlash, 0, 1 );
+ Point aSlashPos( aTmpPos );
+ const tools::Long nAddX = nHalfBlankWidth - aSlashSize.Width() / 2;
+- if ( !IsVertical() )
+- {
+- aSlashPos.setX( aTopLeftRectPos.X() + nAddX );
+- }
+- else
+- {
+- if (IsTopToBottom())
+- aSlashPos.setY( aTopLeftRectPos.Y() + nAddX );
+- else
+- aSlashPos.setY( aTopLeftRectPos.Y() - nAddX );
+- }
++ setXDirectionAware(aSlashPos, getXDirectionAware(aTopLeftRectPos));
++ adjustXDirectionAware(aSlashPos, nAddX);
+
+ aTmpFont.QuickDrawText( pOutDev, aSlashPos, aSlash, 0, 1 );
+
+@@ -3325,24 +3418,8 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ // what will lead to a compressed look with multiple lines
+ const sal_uInt16 nMaxAscent(pLine->GetMaxAscent());
+
+- if ( !IsVertical() )
+- {
+- aStartPos.AdjustY(nMaxAscent );
+- aTmpPos.AdjustY(nMaxAscent );
+- }
+- else
+- {
+- if (IsTopToBottom())
+- {
+- aTmpPos.AdjustX( -nMaxAscent );
+- aStartPos.AdjustX( -nMaxAscent );
+- }
+- else
+- {
+- aTmpPos.AdjustX(nMaxAscent );
+- aStartPos.AdjustX(nMaxAscent );
+- }
+- }
++ aTmpPos += MoveToNextLine(aStartPos, nMaxAscent,
++ nColumn, aOrigin);
+ }
+ std::vector< sal_Int32 >::iterator curIt = itSubLines;
+ ++itSubLines;
+@@ -3509,15 +3586,7 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ if ( aTmpFont.GetEscapement() )
+ {
+ tools::Long nDiff = aTmpFont.GetFontSize().Height() * aTmpFont.GetEscapement() / 100L;
+- if ( !IsVertical() )
+- aOutPos.AdjustY( -nDiff );
+- else
+- {
+- if (IsTopToBottom())
+- aOutPos.AdjustX(nDiff );
+- else
+- aOutPos.AdjustX( -nDiff );
+- }
++ adjustYDirectionAware(aOutPos, -nDiff);
+ aRedLineTmpPos = aOutPos;
+ aTmpFont.SetEscapement( 0 );
+ }
+@@ -3644,13 +3713,7 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ if( _nEsc )
+ {
+ tools::Long nShift = (_nEsc * aTmpFont.GetFontSize().Height()) / 100L;
+- if( !IsVertical() )
+- aRedLineTmpPos.AdjustY( -nShift );
+- else
+- if (IsTopToBottom())
+- aRedLineTmpPos.AdjustX(nShift );
+- else
+- aRedLineTmpPos.AdjustX( -nShift );
++ adjustYDirectionAware(aRedLineTmpPos, -nShift);
+ }
+ }
+ Color aOldColor( pOutDev->GetLineColor() );
+@@ -3764,23 +3827,11 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+
+ if ( ( nLine != nLastLine ) && !aStatus.IsOutliner() )
+ {
+- if ( !IsVertical() )
+- aStartPos.AdjustY(nSBL );
+- else
+- {
+- if( IsTopToBottom() )
+- aStartPos.AdjustX( -nSBL );
+- else
+- aStartPos.AdjustX(nSBL );
+- }
++ adjustYDirectionAware(aStartPos, nSBL);
+ }
+
+ // no more visible actions?
+- if ( !IsVertical() && ( aStartPos.Y() >= aClipRect.Bottom() ) )
+- break;
+- else if ( IsVertical() && IsTopToBottom() && ( aStartPos.X() <= aClipRect.Left() ) )
+- break;
+- else if (IsVertical() && !IsTopToBottom() && (aStartPos.X() >= aClipRect.Right()))
++ if (getYOverflowDirectionAware(aStartPos, aClipRect))
+ break;
+ }
+
+@@ -3788,15 +3839,7 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ {
+ const SvxULSpaceItem& rULItem = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_ULSPACE );
+ tools::Long nUL = GetYValue( rULItem.GetLower() );
+- if ( !IsVertical() )
+- aStartPos.AdjustY(nUL );
+- else
+- {
+- if (IsTopToBottom())
+- aStartPos.AdjustX( -nUL );
+- else
+- aStartPos.AdjustX(nUL );
+- }
++ adjustYDirectionAware(aStartPos, nUL);
+ }
+
+ // #108052# Safer way for #i108052# and #i118881#: If for the current ParaPortion
+@@ -3822,26 +3865,14 @@ void ImpEditEngine::Paint( OutputDevice* pOutDev, tools::Rectangle aClipRect, Po
+ }
+ else
+ {
+- if ( !IsVertical() )
+- aStartPos.AdjustY(nParaHeight );
+- else
+- {
+- if (IsTopToBottom())
+- aStartPos.AdjustX( -nParaHeight );
+- else
+- aStartPos.AdjustX(nParaHeight );
+- }
++ adjustYDirectionAware(aStartPos, nParaHeight);
+ }
+
+ if ( pPDFExtOutDevData )
+ pPDFExtOutDevData->EndStructureElement();
+
+ // no more visible actions?
+- if ( !IsVertical() && ( aStartPos.Y() > aClipRect.Bottom() ) )
+- break;
+- if ( IsVertical() && IsTopToBottom() && ( aStartPos.X() < aClipRect.Left() ) )
+- break;
+- if (IsVertical() && !IsTopToBottom() && ( aStartPos.X() > aClipRect.Right() ) )
++ if (getYOverflowDirectionAware(aStartPos, aClipRect))
+ break;
+ }
+ }
+@@ -3861,26 +3892,16 @@ void ImpEditEngine::Paint( ImpEditView* pView, const tools::Rectangle& rRect, Ou
+
+ Point aStartPos;
+ if ( !IsVertical() )
+- {
+ aStartPos = pView->GetOutputArea().TopLeft();
+- aStartPos.AdjustX( -(pView->GetVisDocLeft()) );
+- aStartPos.AdjustY( -(pView->GetVisDocTop()) );
+- }
+ else
+ {
+ if( IsTopToBottom() )
+- {
+ aStartPos = pView->GetOutputArea().TopRight();
+- aStartPos.AdjustX(pView->GetVisDocTop() );
+- aStartPos.AdjustY( -(pView->GetVisDocLeft()) );
+- }
+ else
+- {
+ aStartPos = pView->GetOutputArea().BottomLeft();
+- aStartPos.AdjustX( -(pView->GetVisDocTop()) );
+- aStartPos.AdjustY(pView->GetVisDocLeft() );
+- }
+ }
++ adjustXDirectionAware(aStartPos, -(pView->GetVisDocLeft()));
++ adjustYDirectionAware(aStartPos, -(pView->GetVisDocTop()));
+
+ // If Doc-width < Output Area,Width and not wrapped fields,
+ // the fields usually protrude if > line.
+@@ -4029,7 +4050,7 @@ EditSelection ImpEditEngine::MoveParagraphs( Range aOldPositions, sal_Int32 nNew
+ {
+ aInvalidRect = tools::Rectangle(); // make empty
+ aInvalidRect.SetLeft( 0 );
+- aInvalidRect.SetRight( aPaperSize.Width() );
++ aInvalidRect.SetRight(GetColumnWidth(aPaperSize));
+ aInvalidRect.SetTop( GetParaPortions().GetYOffset( pUpperPortion ) );
+ aInvalidRect.SetBottom( GetParaPortions().GetYOffset( pLowerPortion ) + pLowerPortion->GetHeight() );
+
+@@ -4159,19 +4180,14 @@ tools::Long ImpEditEngine::CalcVertLineSpacing(Point& rStartPos) const
+ }
+ }
+
+- tools::Long nTotalSpace = IsVertical() ? aPaperSize.Width() : aPaperSize.Height();
++ tools::Long nTotalSpace = getHeightDirectionAware(aPaperSize);
+ nTotalSpace -= nTotalOccupiedHeight;
+ if (nTotalSpace <= 0 || nTotalLineCount <= 1)
+ return 0;
+
++ // Shift the text to the right for the asian layout mode.
+ if (IsVertical())
+- {
+- if( IsTopToBottom() )
+- // Shift the text to the right for the asian layout mode.
+- rStartPos.AdjustX(nTotalSpace );
+- else
+- rStartPos.AdjustX( -nTotalSpace );
+- }
++ adjustYDirectionAware(rStartPos, -nTotalSpace);
+
+ return nTotalSpace / (nTotalLineCount-1);
+ }
+@@ -4248,7 +4264,7 @@ void ImpEditEngine::SetFlatMode( bool bFlat )
+
+ void ImpEditEngine::SetCharStretching( sal_uInt16 nX, sal_uInt16 nY )
+ {
+- bool bChanged(false);
++ bool bChanged;
+ if ( !IsVertical() )
+ {
+ bChanged = nStretchX!=nX || nStretchY!=nY;
+@@ -4632,10 +4648,10 @@ void ImpEditEngine::ImplExpandCompressedPortions( EditLine* pLine, ParaPortion*
+ }
+ }
+
+-void ImpEditEngine::ImplUpdateOverflowingParaNum(sal_uInt32 nPaperHeight)
++void ImpEditEngine::ImplUpdateOverflowingParaNum(tools::Long nPaperHeight)
+ {
+- sal_uInt32 nY = 0;
+- sal_uInt32 nPH;
++ tools::Long nY = 0;
++ tools::Long nPH;
+
+ for ( sal_Int32 nPara = 0; nPara < GetParaPortions().Count(); nPara++ ) {
+ ParaPortion* pPara = GetParaPortions()[nPara];
+@@ -4651,12 +4667,12 @@ void ImpEditEngine::ImplUpdateOverflowingParaNum(sal_uInt32 nPaperHeight)
+ }
+ }
+
+-void ImpEditEngine::ImplUpdateOverflowingLineNum(sal_uInt32 nPaperHeight,
++void ImpEditEngine::ImplUpdateOverflowingLineNum(tools::Long nPaperHeight,
+ sal_uInt32 nOverflowingPara,
+- sal_uInt32 nHeightBeforeOverflowingPara)
++ tools::Long nHeightBeforeOverflowingPara)
+ {
+- sal_uInt32 nY = nHeightBeforeOverflowingPara;
+- sal_uInt32 nLH;
++ tools::Long nY = nHeightBeforeOverflowingPara;
++ tools::Long nLH;
+
+ ParaPortion *pPara = GetParaPortions()[nOverflowingPara];
+
+diff --git a/editeng/source/editeng/impedit4.cxx b/editeng/source/editeng/impedit4.cxx
+index e42a36c5e826..c41b8169a4cb 100644
+--- a/editeng/source/editeng/impedit4.cxx
++++ b/editeng/source/editeng/impedit4.cxx
+@@ -1094,7 +1094,7 @@ std::unique_ptr ImpEditEngine::CreateTextObject( EditSelection a
+ // sleeper set up when Olli paragraphs not hacked!
+ if ( bAllowBigObjects && bOnlyFullParagraphs && IsFormatted() && GetUpdateMode() && ( nTextPortions >= nBigObjectStart ) )
+ {
+- XParaPortionList* pXList = new XParaPortionList( GetRefDevice(), aPaperSize.Width(), nStretchX, nStretchY );
++ XParaPortionList* pXList = new XParaPortionList( GetRefDevice(), GetColumnWidth(aPaperSize), nStretchX, nStretchY );
+ pTxtObj->mpImpl->SetPortionInfo(std::unique_ptr(pXList));
+ for ( nNode = nStartNode; nNode <= nEndNode; nNode++ )
+ {
+@@ -1177,7 +1177,7 @@ EditSelection ImpEditEngine::InsertTextObject( const EditTextObject& rTextObject
+ bool bUsePortionInfo = false;
+ XParaPortionList* pPortionInfo = rTextObject.mpImpl->GetPortionInfo();
+
+- if ( pPortionInfo && ( static_cast(pPortionInfo->GetPaperWidth()) == aPaperSize.Width() )
++ if ( pPortionInfo && ( static_cast(pPortionInfo->GetPaperWidth()) == GetColumnWidth(aPaperSize) )
+ && ( pPortionInfo->GetRefMapMode() == GetRefDevice()->GetMapMode() )
+ && ( pPortionInfo->GetStretchX() == nStretchX )
+ && ( pPortionInfo->GetStretchY() == nStretchY ) )
+diff --git a/editeng/source/outliner/outlin2.cxx b/editeng/source/outliner/outlin2.cxx
+index ff3d1583a5f1..ba556de35c7a 100644
+--- a/editeng/source/outliner/outlin2.cxx
++++ b/editeng/source/outliner/outlin2.cxx
+@@ -223,6 +223,11 @@ void Outliner::SetMaxAutoPaperSize( const Size& rSz )
+ pEditEngine->SetMaxAutoPaperSize( rSz );
+ }
+
++void Outliner::SetMinColumnWrapHeight(tools::Long nVal)
++{
++ pEditEngine->SetMinColumnWrapHeight(nVal);
++}
++
+ bool Outliner::IsExpanded( Paragraph const * pPara ) const
+ {
+ return pParaList->HasVisibleChildren( pPara );
+@@ -532,6 +537,11 @@ bool Outliner::IsTopToBottom() const
+ return pEditEngine->IsTopToBottom();
+ }
+
++void Outliner::SetTextColumns(sal_Int16 nColumns, sal_Int32 nSpacing)
++{
++ pEditEngine->SetTextColumns(nColumns, nSpacing);
++}
++
+ void Outliner::SetFixedCellHeight( bool bUseFixedCellHeight )
+ {
+ pEditEngine->SetFixedCellHeight( bUseFixedCellHeight );
+diff --git a/include/editeng/editeng.hxx b/include/editeng/editeng.hxx
+index 575e43d49e3f..18b136037acb 100644
+--- a/include/editeng/editeng.hxx
++++ b/include/editeng/editeng.hxx
+@@ -242,6 +242,8 @@ public:
+ void SetRotation(TextRotation nRotation);
+ TextRotation GetRotation() const;
+
++ void SetTextColumns(sal_Int16 nColumns, sal_Int32 nSpacing);
++
+ void SetFixedCellHeight( bool bUseFixedCellHeight );
+
+ void SetDefaultHorizontalTextDirection( EEHorizontalTextDirection eHTextDir );
+@@ -270,6 +272,8 @@ public:
+ const Size& GetMaxAutoPaperSize() const;
+ void SetMaxAutoPaperSize( const Size& rSz );
+
++ void SetMinColumnWrapHeight(tools::Long nVal);
++
+ OUString GetText( LineEnd eEnd = LINEEND_LF ) const;
+ OUString GetText( const ESelection& rSelection ) const;
+ sal_uInt32 GetTextLen() const;
+diff --git a/include/editeng/outliner.hxx b/include/editeng/outliner.hxx
+index a6bc9fbd7ff2..d063040e4bc9 100644
+--- a/include/editeng/outliner.hxx
++++ b/include/editeng/outliner.hxx
+@@ -657,6 +657,8 @@ public:
+ bool IsVertical() const;
+ bool IsTopToBottom() const;
+
++ void SetTextColumns(sal_Int16 nColumns, sal_Int32 nSpacing);
++
+ void SetFixedCellHeight( bool bUseFixedCellHeight );
+
+ void SetDefaultHorizontalTextDirection( EEHorizontalTextDirection eHTextDir );
+@@ -786,6 +788,8 @@ public:
+ const Size& GetMaxAutoPaperSize() const;
+ void SetMaxAutoPaperSize( const Size& rSz );
+
++ void SetMinColumnWrapHeight(tools::Long nVal);
++
+ void SetDefTab( sal_uInt16 nTab );
+
+ bool IsFlatMode() const;
+diff --git a/include/oox/ppt/pptimport.hxx b/include/oox/ppt/pptimport.hxx
+index e96f04b70374..f04cb632e574 100644
+--- a/include/oox/ppt/pptimport.hxx
++++ b/include/oox/ppt/pptimport.hxx
+@@ -77,8 +77,6 @@ public:
+
+ ::Color getSchemeColor( sal_Int32 nToken ) const;
+
+- static std::vector< PPTShape* > maPPTShapes;
+-
+ #if OSL_DEBUG_LEVEL > 0
+ static XmlFilterBase* mpDebugFilterBase;
+ #endif
+diff --git a/include/oox/ppt/pptshape.hxx b/include/oox/ppt/pptshape.hxx
+index e67a77635111..f452e585abcf 100644
+--- a/include/oox/ppt/pptshape.hxx
++++ b/include/oox/ppt/pptshape.hxx
+@@ -66,8 +66,7 @@ public:
+ const oox::drawingml::Theme* pTheme,
+ const css::uno::Reference< css::drawing::XShapes >& rxShapes,
+ basegfx::B2DHomMatrix& aTransformation,
+- ::oox::drawingml::ShapeIdMap* pShapeMap,
+- bool bhasSameSubTypeIndex = false );
++ ::oox::drawingml::ShapeIdMap* pShapeMap );
+
+ ShapeLocation getShapeLocation() const { return meShapeLocation; };
+ void setReferenced( bool bReferenced ){ mbReferenced = bReferenced; };
+diff --git a/include/svl/solar.hrc b/include/svl/solar.hrc
+index 317d45a84bc1..521e24365f9f 100644
+--- a/include/svl/solar.hrc
++++ b/include/svl/solar.hrc
+@@ -23,7 +23,7 @@
+ // defines ------------------------------------------------------------------
+
+ #define OWN_ATTR_VALUE_START 3900
+-#define OWN_ATTR_VALUE_END 4005
++#define OWN_ATTR_VALUE_END 4006
+
+ #define RID_LIB_START 10000
+ #define RID_LIB_END 19999
+diff --git a/include/svx/SvxXTextColumns.hxx b/include/svx/SvxXTextColumns.hxx
+new file mode 100644
+index 000000000000..0dbc92ba9611
+--- /dev/null
++++ b/include/svx/SvxXTextColumns.hxx
+@@ -0,0 +1,22 @@
++/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
++/*
++ * This file is part of the LibreOffice project.
++ *
++ * This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
++ */
++
++#pragma once
++
++#include
++
++#include
++#include
++
++#include
++
++SVXCORE_DLLPUBLIC css::uno::Reference
++SvxXTextColumns_createInstance() noexcept;
++
++/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
+diff --git a/include/svx/dialogs.hrc b/include/svx/dialogs.hrc
+index 5dc3fc6faa58..f03600a74fe0 100644
+--- a/include/svx/dialogs.hrc
++++ b/include/svx/dialogs.hrc
+@@ -45,6 +45,7 @@
+ #define RID_SVXPAGE_TRANSPARENCE (RID_SVX_START + 54)
+ #define RID_SVXPAGE_TEXTATTR (RID_SVX_START + 153)
+ #define RID_SVXPAGE_TEXTANIMATION (RID_SVX_START + 184)
++#define RID_SVXPAGE_TEXTCOLUMNS (RID_SVX_START + 154)
+ #define RID_SVXPAGE_MEASURE (RID_SVX_START + 161)
+ #define RID_SVXPAGE_CONNECTION (RID_SVX_START + 191)
+ #define RID_SVXPAGE_LINE_DEF (RID_SVX_START + 52)
+diff --git a/include/svx/strings.hrc b/include/svx/strings.hrc
+index 95e9690b0b9e..50231f03bcc4 100644
+--- a/include/svx/strings.hrc
++++ b/include/svx/strings.hrc
+@@ -401,6 +401,8 @@
+ #define SIP_SA_TEXT_ANIDELAY NC_("SIP_SA_TEXT_ANIDELAY", "Speed of ticker")
+ #define SIP_SA_TEXT_ANIAMOUNT NC_("SIP_SA_TEXT_ANIAMOUNT", "Ticker step size")
+ #define SIP_SA_TEXT_CONTOURFRAME NC_("SIP_SA_TEXT_CONTOURFRAME", "Outline text flow")
++#define SIP_SA_TEXTCOLUMNS_NUMBER NC_("SIP_SA_TEXTCOLUMNS_NUMBER", "Columns number")
++#define SIP_SA_TEXTCOLUMNS_SPACING NC_("SIP_SA_TEXTCOLUMNS_SPACING", "Columns spacing")
+ #define SIP_SA_XMLATTRIBUTES NC_("SIP_SA_XMLATTRIBUTES", "User-defined attributes")
+ #define SIP_SA_TEXT_USEFIXEDCELLHEIGHT NC_("SIP_SA_TEXT_USEFIXEDCELLHEIGHT", "Use font-independent line spacing")
+ #define SIP_SA_WORDWRAP NC_("SIP_SA_WORDWRAP", "Word wrap text in shape")
+diff --git a/include/svx/svddef.hxx b/include/svx/svddef.hxx
+index 30031e20dde2..f4b5c0986d7c 100644
+--- a/include/svx/svddef.hxx
++++ b/include/svx/svddef.hxx
+@@ -138,6 +138,7 @@ class SdrVertShearAllItem;
+ class SdrVertShearOneItem;
+ class SdrYesNoItem;
+ class SfxBoolItem;
++class SfxInt16Item;
+ class SfxUInt16Item;
+ class SfxUInt32Item;
+ class SfxStringItem;
+@@ -426,7 +427,12 @@ constexpr sal_uInt16 SDRATTR_SOFTEDGE_FIRST(SDRATTR_GLOW_LAST +
+ constexpr TypedWhichId SDRATTR_SOFTEDGE_RADIUS(SDRATTR_SOFTEDGE_FIRST + 0);
+ constexpr sal_uInt16 SDRATTR_SOFTEDGE_LAST(SDRATTR_SOFTEDGE_RADIUS);
+
+-constexpr sal_uInt16 SDRATTR_END (SDRATTR_SOFTEDGE_LAST); /* 1357 */ /* 1333 V4+++*/ /* 1243 V4+++*/ /*1213*/ /*1085*/ /*1040*/ /*Pool V2: 1123,V1: 1065 */
++constexpr sal_uInt16 SDRATTR_TEXTCOLUMNS_FIRST(SDRATTR_SOFTEDGE_LAST + 1);
++constexpr TypedWhichId SDRATTR_TEXTCOLUMNS_NUMBER(SDRATTR_TEXTCOLUMNS_FIRST + 0);
++constexpr TypedWhichId SDRATTR_TEXTCOLUMNS_SPACING(SDRATTR_TEXTCOLUMNS_FIRST + 1);
++constexpr sal_uInt16 SDRATTR_TEXTCOLUMNS_LAST(SDRATTR_TEXTCOLUMNS_SPACING);
++
++constexpr sal_uInt16 SDRATTR_END (SDRATTR_TEXTCOLUMNS_LAST); /* 1357 */ /* 1333 V4+++*/ /* 1243 V4+++*/ /*1213*/ /*1085*/ /*1040*/ /*Pool V2: 1123,V1: 1065 */
+
+ #endif // INCLUDED_SVX_SVDDEF_HXX
+
+diff --git a/include/svx/svdotext.hxx b/include/svx/svdotext.hxx
+index 9d74bffec93e..26ed2d4c6e2c 100644
+--- a/include/svx/svdotext.hxx
++++ b/include/svx/svdotext.hxx
+@@ -389,7 +389,7 @@ public:
+ // FitToSize and Fontwork are not taken into account in GetTextSize()!
+ virtual const Size& GetTextSize() const;
+ void FitFrameToTextSize();
+- double GetFontScaleY() const;
++ sal_uInt16 GetFontScaleY() const;
+
+ // Simultaneously sets the text into the Outliner (possibly
+ // the one of the EditOutliner) and sets the PaperSize.
+@@ -436,6 +436,13 @@ public:
+ SdrTextAniKind GetTextAniKind() const;
+ SdrTextAniDirection GetTextAniDirection() const;
+
++ bool HasTextColumnsNumber() const;
++ sal_Int16 GetTextColumnsNumber() const;
++ void SetTextColumnsNumber(sal_Int16 nColumns);
++ bool HasTextColumnsSpacing() const;
++ sal_Int32 GetTextColumnsSpacing() const;
++ void SetTextColumnsSpacing(sal_Int32 nSpacing);
++
+ // react on model/page change
+ virtual void handlePageChange(SdrPage* pOldPage, SdrPage* pNewPage) override;
+
+diff --git a/include/svx/unoshprp.hxx b/include/svx/unoshprp.hxx
+index 20a49f1f4d62..98bea3cd1c09 100644
+--- a/include/svx/unoshprp.hxx
++++ b/include/svx/unoshprp.hxx
+@@ -60,6 +60,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -193,7 +194,9 @@
+ #define OWN_ATTR_SIGNATURELINE_IS_SIGNED (OWN_ATTR_VALUE_START+103)
+ #define OWN_ATTR_QRCODE (OWN_ATTR_VALUE_START+104)
+ #define OWN_ATTR_TEXTFITTOSIZESCALE (OWN_ATTR_VALUE_START+105)
+-// ATTENTION: maximum is OWN_ATTR_VALUE_START+105 svx, see include/svl/solar.hrc
++#define OWN_ATTR_TEXTCOLUMNS (OWN_ATTR_VALUE_START+106)
++// ATTENTION: current maximum is OWN_ATTR_VALUE_START+106 svx; wnen adding values, update
++// OWN_ATTR_VALUE_END in include/svl/solar.hrc accordingly
+
+ // #FontWork#
+ #define FONTWORK_PROPERTIES \
+@@ -316,6 +319,7 @@
+ { u"" UNO_NAME_TEXT_VERTADJUST, SDRATTR_TEXT_VERTADJUST, cppu::UnoType::get(), 0, 0},\
+ { u"" UNO_NAME_TEXT_WORDWRAP, SDRATTR_TEXT_WORDWRAP, cppu::UnoType::get(), 0, 0}, \
+ { u"" UNO_NAME_TEXT_CHAINNEXTNAME, SDRATTR_TEXT_CHAINNEXTNAME, ::cppu::UnoType::get(), 0, 0}, \
++ { u"TextColumns", OWN_ATTR_TEXTCOLUMNS, cppu::UnoType::get(), 0, 0 }, \
+ SVX_UNOEDIT_CHAR_PROPERTIES, \
+ SVX_UNOEDIT_PARA_PROPERTIES,
+
+diff --git a/offapi/com/sun/star/drawing/TextProperties.idl b/offapi/com/sun/star/drawing/TextProperties.idl
+index 4516c9829781..eb045624aba0 100644
+--- a/offapi/com/sun/star/drawing/TextProperties.idl
++++ b/offapi/com/sun/star/drawing/TextProperties.idl
+@@ -40,6 +40,7 @@
+ #include
+ #include
+ #include
++#include
+
+
+ module com { module sun { module star { module drawing {
+@@ -249,6 +250,13 @@ published service TextProperties
+ /** This value selects the writing mode for the text.
+ */
+ [property] ::com::sun::star::text::WritingMode TextWritingMode;
++
++
++ /** Column layout properties for the text.
++
++ @since LibreOffice 7.2
++ */
++ [optional, property] ::com::sun::star::text::XTextColumns TextColumns;
+ };
+
+
+diff --git a/oox/inc/drawingml/table/tableproperties.hxx b/oox/inc/drawingml/table/tableproperties.hxx
+index ec8b3c4c5b60..34e361b18add 100644
+--- a/oox/inc/drawingml/table/tableproperties.hxx
++++ b/oox/inc/drawingml/table/tableproperties.hxx
+@@ -58,9 +58,6 @@ public:
+ const css::uno::Reference < css::beans::XPropertySet > & xPropSet,
+ const ::oox::drawingml::TextListStylePtr& pMasterTextListStyle );
+
+- /// Distributes text body with multiple columns in table cells.
+- void pullFromTextBody(oox::drawingml::TextBodyPtr pTextBody, sal_Int32 nShapeWidth, bool bhasSameSubTypeIndex, bool bMaster);
+-
+ private:
+
+ const TableStyle& getUsedTableStyle(const ::oox::core::XmlFilterBase& rFilterBase, std::unique_ptr& rTableStyleToDelete);
+diff --git a/oox/inc/drawingml/textbodyproperties.hxx b/oox/inc/drawingml/textbodyproperties.hxx
+index 8a51c2bb906e..41fbb832a5d8 100644
+--- a/oox/inc/drawingml/textbodyproperties.hxx
++++ b/oox/inc/drawingml/textbodyproperties.hxx
+@@ -42,8 +42,6 @@ struct TextBodyProperties
+ std::optional< sal_Int32 > moTextOffRight;
+ css::drawing::TextVerticalAdjust meVA;
+ OUString msPrst;
+- /// Number of requested columns.
+- sal_Int32 mnNumCol = 1;
+ /// Normal autofit: font scale (default: 100%).
+ sal_Int32 mnFontScale = 100000;
+ OUString msHorzOverflow;
+diff --git a/oox/source/drawingml/table/tableproperties.cxx b/oox/source/drawingml/table/tableproperties.cxx
+index 1c12c10eda47..69117123dca8 100644
+--- a/oox/source/drawingml/table/tableproperties.cxx
++++ b/oox/source/drawingml/table/tableproperties.cxx
+@@ -145,7 +145,7 @@ void TableProperties::pushToPropSet(const ::oox::core::XmlFilterBase& rFilterBas
+ for (auto& tableRow : mvTableRows)
+ {
+ sal_Int32 nColumn = 0;
+- sal_Int32 nColumnSize = mvTableGrid.size();
++ sal_Int32 nColumnSize = tableRow.getTableCells().size();
+ sal_Int32 nRemovedColumn = 0; //
+
+ for (sal_Int32 nColIndex = 0; nColIndex < nColumnSize; nColIndex++)
+@@ -206,65 +206,6 @@ void TableProperties::pushToPropSet(const ::oox::core::XmlFilterBase& rFilterBas
+
+ xTableStyleToDelete.reset();
+ }
+-
+-void TableProperties::pullFromTextBody(oox::drawingml::TextBodyPtr pTextBody, sal_Int32 nShapeWidth, bool bhasSameSubTypeIndex, bool bMaster)
+-{
+- // Create table grid and a single row.
+- sal_Int32 nNumCol = pTextBody->getTextProperties().mnNumCol;
+- std::vector& rTableGrid(getTableGrid());
+- std::vector& rTableRows(getTableRows());
+- sal_Int32 nColWidth = nShapeWidth / nNumCol;
+-
+- if(!bhasSameSubTypeIndex)
+- {
+- for (sal_Int32 nCol = 0; nCol < nNumCol; ++nCol)
+- rTableGrid.push_back(nColWidth);
+-
+- rTableRows.emplace_back();
+- }
+-
+- if(rTableRows.empty())
+- rTableRows.emplace_back();
+-
+- oox::drawingml::table::TableRow& rTableRow = rTableRows.back();
+- std::vector& rTableCells = rTableRow.getTableCells();
+-
+- // Create the cells and distribute the paragraphs from pTextBody.
+- sal_Int32 nNumPara = pTextBody->getParagraphs().size();
+- sal_Int32 nParaPerCol = std::ceil(double(nNumPara) / nNumCol);
+- // Font scale of text body will be applied at a text run level.
+- sal_Int32 nFontScale = pTextBody->getTextProperties().mnFontScale;
+- size_t nPara = 0;
+- for (sal_Int32 nCol = 0; nCol < nNumCol; ++nCol)
+- {
+- rTableCells.emplace_back();
+- oox::drawingml::table::TableCell& rTableCell = rTableCells.at(nCol);
+- TextBodyPtr pCellTextBody = std::make_shared();
+- rTableCell.setTextBody(pCellTextBody);
+-
+- // Copy properties provided by .
+- pCellTextBody->getTextListStyle() = pTextBody->getTextListStyle();
+-
+- if (bMaster)
+- continue;
+-
+- for (sal_Int32 nParaInCol = 0; nParaInCol < nParaPerCol; ++nParaInCol)
+- {
+- if (nPara < pTextBody->getParagraphs().size())
+- {
+- std::shared_ptr pParagraph
+- = pTextBody->getParagraphs()[nPara];
+- if (nFontScale != 100000)
+- {
+- for (auto& pRun : pParagraph->getRuns())
+- pRun->getTextCharacterProperties().moFontScale = nFontScale;
+- }
+- pCellTextBody->appendParagraph(pParagraph);
+- }
+- ++nPara;
+- }
+- }
+-}
+ }
+
+ /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
+diff --git a/oox/source/drawingml/textbodypropertiescontext.cxx b/oox/source/drawingml/textbodypropertiescontext.cxx
+index 4d94191dcfc7..e2811d6667ba 100644
+--- a/oox/source/drawingml/textbodypropertiescontext.cxx
++++ b/oox/source/drawingml/textbodypropertiescontext.cxx
+@@ -22,6 +22,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -32,6 +33,7 @@
+ #include
+ #include
+ #include
++#include
+
+ using namespace ::oox::core;
+ using namespace ::com::sun::star;
+@@ -85,7 +87,17 @@ TextBodyPropertiesContext::TextBodyPropertiesContext( ContextHandler2Helper cons
+ mrTextBodyProp.msVertOverflow = rAttribs.getString(XML_vertOverflow, "");
+
+ // ST_TextColumnCount
+- mrTextBodyProp.mnNumCol = rAttribs.getInteger( XML_numCol, 1 );
++ if (const sal_Int32 nColumns = rAttribs.getInteger(XML_numCol, 0); nColumns > 0)
++ {
++ css::uno::Reference xCols(SvxXTextColumns_createInstance(),
++ css::uno::UNO_QUERY_THROW);
++ xCols->setColumnCount(nColumns);
++ css::uno::Reference xProps(xCols, css::uno::UNO_QUERY_THROW);
++ // ST_PositiveCoordinate32
++ const sal_Int32 nSpacing = convertEmuToHmm(rAttribs.getInteger(XML_spcCol, 0));
++ xProps->setPropertyValue("AutomaticDistance", css::uno::Any(nSpacing));
++ mrTextBodyProp.maPropertyMap.setAnyProperty(PROP_TextColumns, css::uno::Any(xCols));
++ }
+
+ // ST_Angle
+ mrTextBodyProp.moRotation = rAttribs.getInteger( XML_rot );
+diff --git a/oox/source/export/drawingml.cxx b/oox/source/export/drawingml.cxx
+index 8cfd4a6dabc5..263cd5c06b67 100644
+--- a/oox/source/export/drawingml.cxx
++++ b/oox/source/export/drawingml.cxx
+@@ -85,6 +85,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -3060,6 +3061,22 @@ void DrawingML::WriteText(const Reference& rXIface, bool bBodyPr, bo
+ sal_Int32 nShapeTextRotateAngle = 0;
+ if (GetProperty(xTextSet, "RotateAngle"))
+ nShapeTextRotateAngle = rXPropSet->getPropertyValue("RotateAngle").get() / 300;
++ sal_Int16 nCols = 0;
++ sal_Int32 nColSpacing = -1;
++ if (GetProperty(rXPropSet, "TextColumns"))
++ {
++ if (css::uno::Reference xCols{ mAny, css::uno::UNO_QUERY })
++ {
++ nCols = xCols->getColumnCount();
++ if (css::uno::Reference xProps{ mAny,
++ css::uno::UNO_QUERY })
++ {
++ if (GetProperty(xProps, "AutomaticDistance"))
++ mAny >>= nColSpacing;
++ }
++ }
++ }
++
+ std::optional isUpright;
+ if (GetProperty(rXPropSet, "InteropGrabBag"))
+ {
+@@ -3114,6 +3131,8 @@ void DrawingML::WriteText(const Reference& rXIface, bool bBodyPr, bo
+ }
+
+ mpFS->startElementNS( (nXmlNamespace ? nXmlNamespace : XML_a), XML_bodyPr,
++ XML_numCol, sax_fastparser::UseIf(OString::number(nCols), nCols > 0),
++ XML_spcCol, sax_fastparser::UseIf(OString::number(oox::drawingml::convertHmmToEmu(nColSpacing)), nCols > 0 && nColSpacing >= 0),
+ XML_wrap, pWrap,
+ XML_horzOverflow, sHorzOverflow,
+ XML_vertOverflow, sVertOverflow,
+@@ -3229,10 +3248,7 @@ void DrawingML::WriteText(const Reference& rXIface, bool bBodyPr, bo
+ {
+ SdrTextObj* pTextObject = dynamic_cast(pTextShape->GetSdrObject());
+ if (pTextObject)
+- {
+- double fScaleY = pTextObject->GetFontScaleY();
+- nFontScale = static_cast(fScaleY * 100) * 1000;
+- }
++ nFontScale = pTextObject->GetFontScaleY() * 1000;
+ }
+
+ mpFS->singleElementNS(XML_a, XML_normAutofit, XML_fontScale,
+diff --git a/oox/source/ppt/pptimport.cxx b/oox/source/ppt/pptimport.cxx
+index f7d00920705e..dea04a98e9a3 100644
+--- a/oox/source/ppt/pptimport.cxx
++++ b/oox/source/ppt/pptimport.cxx
+@@ -70,7 +70,6 @@ PowerPointImport::PowerPointImport( const Reference< XComponentContext >& rxCont
+
+ PowerPointImport::~PowerPointImport()
+ {
+- maPPTShapes.clear();
+ }
+
+ /// Visits the relations from pRelations which are of type rType.
+diff --git a/oox/source/ppt/pptshape.cxx b/oox/source/ppt/pptshape.cxx
+index 96ca319f2a8e..e326cba50464 100644
+--- a/oox/source/ppt/pptshape.cxx
++++ b/oox/source/ppt/pptshape.cxx
+@@ -114,8 +114,7 @@ void PPTShape::addShape(
+ const oox::drawingml::Theme* pTheme,
+ const Reference< XShapes >& rxShapes,
+ basegfx::B2DHomMatrix& aTransformation,
+- ::oox::drawingml::ShapeIdMap* pShapeMap,
+- bool bhasSameSubTypeIndex )
++ ::oox::drawingml::ShapeIdMap* pShapeMap )
+ {
+ SAL_INFO("oox.ppt","add shape id: " << msId << " location: " << ((meShapeLocation == Master) ? "master" : ((meShapeLocation == Slide) ? "slide" : ((meShapeLocation == Layout) ? "layout" : "other"))) << " subtype: " << mnSubType << " service: " << msServiceName);
+ // only placeholder from layout are being inserted
+@@ -225,36 +224,6 @@ void PPTShape::addShape(
+ }
+ }
+
+- if (sServiceName != "com.sun.star.drawing.TableShape")
+- {
+- if (TextBodyPtr pTextBody = getTextBody())
+- {
+- // If slide shape has not numCol but placeholder has we should inherit from placeholder.
+- if (pTextBody->getTextProperties().mnNumCol == 1 &&
+- mnSubType &&
+- getSubTypeIndex().has() &&
+- rSlidePersist.getMasterPersist())
+- {
+- oox::drawingml::ShapePtr pPlaceholder = PPTShape::findPlaceholderByIndex(
+- getSubTypeIndex().get(),
+- rSlidePersist.getMasterPersist()->getShapes()->getChildren());
+- if (pPlaceholder && pPlaceholder->getTableProperties())
+- pTextBody->getTextProperties().mnNumCol = pPlaceholder->getTableProperties()->getTableGrid().size();
+- }
+-
+- sal_Int32 nNumCol = pTextBody->getTextProperties().mnNumCol;
+- if (nNumCol > 1)
+- {
+- // This shape is not a table, but has multiple columns,
+- // represent that as a table.
+- sServiceName = "com.sun.star.drawing.TableShape";
+- oox::drawingml::table::TablePropertiesPtr pTableProperties = getTableProperties();
+- pTableProperties->pullFromTextBody(pTextBody, maSize.Width, bhasSameSubTypeIndex, meShapeLocation == Layout);
+- setTextBody(nullptr);
+- }
+- }
+- }
+-
+ SAL_INFO("oox.ppt","shape service: " << sServiceName);
+
+ if (mnSubType && getSubTypeIndex().has() && meShapeLocation == Layout)
+diff --git a/oox/source/ppt/slidepersist.cxx b/oox/source/ppt/slidepersist.cxx
+index dc18ec06e128..74bd8165bc7a 100644
+--- a/oox/source/ppt/slidepersist.cxx
++++ b/oox/source/ppt/slidepersist.cxx
+@@ -23,7 +23,6 @@
+ #include
+ #include
+ #include
+-#include
+ #include
+ #include
+ #include
+@@ -35,7 +34,6 @@
+ #include
+ #include
+ #include
+-#include
+
+ #include
+
+@@ -54,8 +52,6 @@ using namespace ::com::sun::star::animations;
+
+ namespace oox::ppt {
+
+-std::vector< PPTShape* > PowerPointImport::maPPTShapes;
+-
+ SlidePersist::SlidePersist( XmlFilterBase& rFilter, bool bMaster, bool bNotes,
+ const css::uno::Reference< css::drawing::XDrawPage >& rxPage,
+ oox::drawingml::ShapePtr const & pShapesPtr, const drawingml::TextListStylePtr & pDefaultTextStyle )
+@@ -132,29 +128,12 @@ sal_Int16 SlidePersist::getLayoutFromValueToken() const
+ return nLayout;
+ }
+
+-static bool hasSameSubTypeIndex(sal_Int32 checkSubTypeIndex)
+-{
+- sal_Int32 nSubTypeIndex = -1;
+- for(PPTShape* pPPTShape : PowerPointImport::maPPTShapes)
+- {
+- if(!pPPTShape->getSubTypeIndex().has())
+- continue;
+-
+- nSubTypeIndex = pPPTShape->getSubTypeIndex().get();
+-
+- if( nSubTypeIndex == checkSubTypeIndex )
+- return true;
+- }
+- return false;
+-}
+ void SlidePersist::createXShapes( XmlFilterBase& rFilterBase )
+ {
+ applyTextStyles( rFilterBase );
+
+ Reference< XShapes > xShapes( getPage() );
+ std::vector< oox::drawingml::ShapePtr >& rShapes( maShapesPtr->getChildren() );
+- bool bhasSameSubTypeIndex = false;
+- sal_Int32 nNumCol = 1;
+
+ for (auto const& shape : rShapes)
+ {
+@@ -164,17 +143,7 @@ void SlidePersist::createXShapes( XmlFilterBase& rFilterBase )
+ PPTShape* pPPTShape = dynamic_cast< PPTShape* >( child.get() );
+ basegfx::B2DHomMatrix aTransformation;
+ if ( pPPTShape )
+- {
+- bhasSameSubTypeIndex = hasSameSubTypeIndex( pPPTShape->getSubTypeIndex().get());
+-
+- if(pPPTShape->getTextBody())
+- nNumCol = pPPTShape->getTextBody()->getTextProperties().mnNumCol;
+-
+- if(pPPTShape->getSubTypeIndex().has() && nNumCol > 1 )
+- PowerPointImport::maPPTShapes.push_back(pPPTShape);
+-
+- pPPTShape->addShape( rFilterBase, *this, getTheme().get(), xShapes, aTransformation, &getShapeMap(), bhasSameSubTypeIndex );
+- }
++ pPPTShape->addShape( rFilterBase, *this, getTheme().get(), xShapes, aTransformation, &getShapeMap() );
+ else
+ child->addShape( rFilterBase, getTheme().get(), xShapes, aTransformation, maShapesPtr->getFillProperties(), &getShapeMap() );
+ }
+diff --git a/oox/source/token/properties.txt b/oox/source/token/properties.txt
+index d50cd4bb124d..980c1bb8c0f2 100644
+--- a/oox/source/token/properties.txt
++++ b/oox/source/token/properties.txt
+@@ -528,6 +528,7 @@ TextBox
+ TextBreak
+ TextCameraZRotateAngle
+ TextColor
++TextColumns
+ TextFitToSize
+ TextFrames
+ TextHorizontalAdjust
+diff --git a/sc/qa/uitest/chart/chartLegend.py b/sc/qa/uitest/chart/chartLegend.py
+index 7e2e085d135b..7ee844008d40 100644
+--- a/sc/qa/uitest/chart/chartLegend.py
++++ b/sc/qa/uitest/chart/chartLegend.py
+@@ -13,6 +13,7 @@ from libreoffice.calc.document import get_cell_by_position
+ from libreoffice.uno.propertyvalue import mkPropertyValues
+ from uitest.uihelper.common import get_state_as_dict, type_text
+ from uitest.debug import sleep
++from uitest.uihelper import guarded
+ import org.libreoffice.unotest
+ import pathlib
+
+@@ -23,119 +24,91 @@ def get_url_for_data_file(file_name):
+
+ class chartLegend(UITestCase):
+ def test_chart_display_legend_dialog(self):
+- calc_doc = self.ui_test.load_file(get_url_for_data_file("tdf98390.ods"))
+- xCalcDoc = self.xUITest.getTopFocusWindow()
+- gridwin = xCalcDoc.getChild("grid_window")
+- document = self.ui_test.get_component()
+-
+- gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
+- gridwin.executeAction("ACTIVATE", tuple())
+- xChartMainTop = self.xUITest.getTopFocusWindow()
+- xChartMain = xChartMainTop.getChild("chart_window")
+- xSeriesObj = xChartMain.getChild("CID/D=0:CS=0:CT=0:Series=0")
+- self.ui_test.execute_dialog_through_action(xSeriesObj, "COMMAND", mkPropertyValues({"COMMAND": "InsertMenuLegend"}))
+- xDialog = self.xUITest.getTopFocusWindow()
+-
+- left = xDialog.getChild("left")
+- right = xDialog.getChild("right")
+- top = xDialog.getChild("top")
+- bottom = xDialog.getChild("bottom")
+-
+- left.executeAction("CLICK", tuple())
+-
+- xOKBtn = xDialog.getChild("ok")
+- self.ui_test.close_dialog_through_button(xOKBtn)
+-
+- #reopen and verify InsertMenuLegend dialog
+- gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
+- gridwin.executeAction("ACTIVATE", tuple())
+- xChartMainTop = self.xUITest.getTopFocusWindow()
+- xChartMain = xChartMainTop.getChild("chart_window")
+- xSeriesObj = xChartMain.getChild("CID/D=0:CS=0:CT=0:Series=0")
+- self.ui_test.execute_dialog_through_action(xSeriesObj, "COMMAND", mkPropertyValues({"COMMAND": "InsertMenuLegend"}))
+- xDialog = self.xUITest.getTopFocusWindow()
+-
+- left = xDialog.getChild("left")
+- right = xDialog.getChild("right")
+- top = xDialog.getChild("top")
+- bottom = xDialog.getChild("bottom")
+- show = xDialog.getChild("show")
+-
+- self.assertEqual(get_state_as_dict(left)["Checked"], "true")
+- self.assertEqual(get_state_as_dict(right)["Checked"], "false")
+- self.assertEqual(get_state_as_dict(top)["Checked"], "false")
+- self.assertEqual(get_state_as_dict(bottom)["Checked"], "false")
+-
+- show.executeAction("CLICK", tuple())
+-
+- xOKBtn = xDialog.getChild("ok")
+- self.ui_test.close_dialog_through_button(xOKBtn)
+-
+- #reopen and verify InsertMenuLegend dialog
+- gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
+- gridwin.executeAction("ACTIVATE", tuple())
+- xChartMainTop = self.xUITest.getTopFocusWindow()
+- xChartMain = xChartMainTop.getChild("chart_window")
+- xSeriesObj = xChartMain.getChild("CID/D=0:CS=0:CT=0:Series=0")
+- self.ui_test.execute_dialog_through_action(xSeriesObj, "COMMAND", mkPropertyValues({"COMMAND": "InsertMenuLegend"}))
+- xDialog = self.xUITest.getTopFocusWindow()
+-
+- left = xDialog.getChild("left")
+- right = xDialog.getChild("right")
+- top = xDialog.getChild("top")
+- bottom = xDialog.getChild("bottom")
+- show = xDialog.getChild("show")
+-
+- self.assertEqual(get_state_as_dict(left)["Checked"], "true")
+- self.assertEqual(get_state_as_dict(right)["Checked"], "false")
+- self.assertEqual(get_state_as_dict(top)["Checked"], "false")
+- self.assertEqual(get_state_as_dict(bottom)["Checked"], "false")
+-
+- self.assertEqual(get_state_as_dict(show)["Selected"], "false")
+-
+- xOKBtn = xDialog.getChild("ok")
+- self.ui_test.close_dialog_through_button(xOKBtn)
+- self.ui_test.close_doc()
++ with guarded.load_file(self, get_url_for_data_file("tdf98390.ods")) as calc_doc:
++ xCalcDoc = self.xUITest.getTopFocusWindow()
++ gridwin = xCalcDoc.getChild("grid_window")
++ document = self.ui_test.get_component()
++
++ gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
++ gridwin.executeAction("ACTIVATE", tuple())
++ xChartMainTop = self.xUITest.getTopFocusWindow()
++ xChartMain = xChartMainTop.getChild("chart_window")
++ xSeriesObj = xChartMain.getChild("CID/D=0:CS=0:CT=0:Series=0")
++ with guarded.execute_dialog_through_action(self, xSeriesObj, "COMMAND", mkPropertyValues({"COMMAND": "InsertMenuLegend"})) as xDialog:
++ left = xDialog.getChild("left")
++ right = xDialog.getChild("right")
++ top = xDialog.getChild("top")
++ bottom = xDialog.getChild("bottom")
++
++ left.executeAction("CLICK", tuple())
++
++ #reopen and verify InsertMenuLegend dialog
++ gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
++ gridwin.executeAction("ACTIVATE", tuple())
++ xChartMainTop = self.xUITest.getTopFocusWindow()
++ xChartMain = xChartMainTop.getChild("chart_window")
++ xSeriesObj = xChartMain.getChild("CID/D=0:CS=0:CT=0:Series=0")
++ with guarded.execute_dialog_through_action(self, xSeriesObj, "COMMAND", mkPropertyValues({"COMMAND": "InsertMenuLegend"})) as xDialog:
++ left = xDialog.getChild("left")
++ right = xDialog.getChild("right")
++ top = xDialog.getChild("top")
++ bottom = xDialog.getChild("bottom")
++ show = xDialog.getChild("show")
++
++ self.assertEqual(get_state_as_dict(left)["Checked"], "true")
++ self.assertEqual(get_state_as_dict(right)["Checked"], "false")
++ self.assertEqual(get_state_as_dict(top)["Checked"], "false")
++ self.assertEqual(get_state_as_dict(bottom)["Checked"], "false")
++
++ show.executeAction("CLICK", tuple())
++
++ #reopen and verify InsertMenuLegend dialog
++ gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
++ gridwin.executeAction("ACTIVATE", tuple())
++ xChartMainTop = self.xUITest.getTopFocusWindow()
++ xChartMain = xChartMainTop.getChild("chart_window")
++ xSeriesObj = xChartMain.getChild("CID/D=0:CS=0:CT=0:Series=0")
++ with guarded.execute_dialog_through_action(self, xSeriesObj, "COMMAND", mkPropertyValues({"COMMAND": "InsertMenuLegend"})) as xDialog:
++ left = xDialog.getChild("left")
++ right = xDialog.getChild("right")
++ top = xDialog.getChild("top")
++ bottom = xDialog.getChild("bottom")
++ show = xDialog.getChild("show")
++
++ self.assertEqual(get_state_as_dict(left)["Checked"], "true")
++ self.assertEqual(get_state_as_dict(right)["Checked"], "false")
++ self.assertEqual(get_state_as_dict(top)["Checked"], "false")
++ self.assertEqual(get_state_as_dict(bottom)["Checked"], "false")
++
++ self.assertEqual(get_state_as_dict(show)["Selected"], "false")
+
+ def test_legends_move_with_arrows_keys(self):
+
+- calc_doc = self.ui_test.load_file(get_url_for_data_file("dataLabels.ods"))
+- xCalcDoc = self.xUITest.getTopFocusWindow()
+- gridwin = xCalcDoc.getChild("grid_window")
++ with guarded.load_file(self, get_url_for_data_file("dataLabels.ods")) as calc_doc:
++ xCalcDoc = self.xUITest.getTopFocusWindow()
++ gridwin = xCalcDoc.getChild("grid_window")
+
+- change_measurement_unit(self, "Centimeter")
++ change_measurement_unit(self, "Centimeter")
+
+- gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
+- gridwin.executeAction("ACTIVATE", tuple())
+- xChartMainTop = self.xUITest.getTopFocusWindow()
+- xChartMain = xChartMainTop.getChild("chart_window")
++ gridwin.executeAction("SELECT", mkPropertyValues({"OBJECT": "Object 1"}))
++ gridwin.executeAction("ACTIVATE", tuple())
++ xChartMainTop = self.xUITest.getTopFocusWindow()
++ xChartMain = xChartMainTop.getChild("chart_window")
+
+- # Select the legends
+- xLegends = xChartMain.getChild("CID/D=0:Legend=")
+- xLegends.executeAction("SELECT", tuple())
++ # Select the legends
++ xLegends = xChartMain.getChild("CID/D=0:Legend=")
++ xLegends.executeAction("SELECT", tuple())
+
+- self.ui_test.execute_dialog_through_action(xLegends, "COMMAND", mkPropertyValues({"COMMAND": "TransformDialog"}))
++ with guarded.execute_dialog_through_action(self, xLegends, "COMMAND", mkPropertyValues({"COMMAND": "TransformDialog"})) as xDialog:
++ self.assertEqual("4.61", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_X"))['Value'])
++ self.assertEqual("1.53", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_Y"))['Value'])
+
+- xDialog = self.xUITest.getTopFocusWindow()
+- self.assertEqual("4.61", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_X"))['Value'])
+- self.assertEqual("1.54", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_Y"))['Value'])
++ xChartMain.executeAction("TYPE", mkPropertyValues({"KEYCODE": "UP"}))
++ xChartMain.executeAction("TYPE", mkPropertyValues({"KEYCODE": "LEFT"}))
+
+- xOkBtn = xDialog.getChild("ok")
+- xOkBtn.executeAction("CLICK", tuple())
+-
+- xChartMain.executeAction("TYPE", mkPropertyValues({"KEYCODE": "UP"}))
+- xChartMain.executeAction("TYPE", mkPropertyValues({"KEYCODE": "LEFT"}))
+-
+- self.ui_test.execute_dialog_through_action(xLegends, "COMMAND", mkPropertyValues({"COMMAND": "TransformDialog"}))
+-
+- # Check the position has changed after moving the label using the arrows keys
+- xDialog = self.xUITest.getTopFocusWindow()
+- self.assertEqual("4.51", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_X"))['Value'])
+- self.assertEqual("1.44", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_Y"))['Value'])
+-
+- xOkBtn = xDialog.getChild("ok")
+- xOkBtn.executeAction("CLICK", tuple())
+-
+- self.ui_test.close_doc()
++ # Check the position has changed after moving the label using the arrows keys
++ with guarded.execute_dialog_through_action(self, xLegends, "COMMAND", mkPropertyValues({"COMMAND": "TransformDialog"})) as xDialog:
++ self.assertEqual("4.51", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_X"))['Value'])
++ self.assertEqual("1.43", get_state_as_dict(xDialog.getChild("MTR_FLD_POS_Y"))['Value'])
+
+ # vim: set shiftwidth=4 softtabstop=4 expandtab:
+diff --git a/sc/qa/unit/filters-test.cxx b/sc/qa/unit/filters-test.cxx
+index a994297ff2c0..30ef7f12aa3e 100644
+--- a/sc/qa/unit/filters-test.cxx
++++ b/sc/qa/unit/filters-test.cxx
+@@ -514,7 +514,7 @@ void ScFiltersTest::testCommentSize()
+
+ const tools::Rectangle& rNewRect = pCaption->GetLogicRect();
+ CPPUNIT_ASSERT_EQUAL(rOldRect.getWidth(), rNewRect.getWidth());
+- CPPUNIT_ASSERT_EQUAL(tools::Long(1605), rNewRect.getHeight());
++ CPPUNIT_ASSERT_EQUAL(tools::Long(1606), rNewRect.getHeight());
+
+ rDoc.GetUndoManager()->Undo();
+
+diff --git a/sd/qa/uitest/impress_tests/tdf91762.py b/sd/qa/uitest/impress_tests/tdf91762.py
+index 2487bc2a4726..b05670c4af63 100644
+--- a/sd/qa/uitest/impress_tests/tdf91762.py
++++ b/sd/qa/uitest/impress_tests/tdf91762.py
+@@ -26,9 +26,9 @@ class tdf91762(UITestCase):
+ self.ui_test.close_dialog_through_button(xOkBtn)
+
+ document = self.ui_test.get_component()
+- self.assertEqual(1929, document.DrawPages[0].getByIndex(1).BoundRect.Height)
++ self.assertEqual(1931, document.DrawPages[0].getByIndex(1).BoundRect.Height)
+ self.assertEqual(25198, document.DrawPages[0].getByIndex(1).Size.Width)
+- self.assertEqual(1923, document.DrawPages[0].getByIndex(1).Size.Height)
++ self.assertEqual(1925, document.DrawPages[0].getByIndex(1).Size.Height)
+
+ self.assertEqual(1400, document.DrawPages[0].getByIndex(1).Position.X)
+ self.assertEqual(3685, document.DrawPages[0].getByIndex(1).Position.Y)
+@@ -40,8 +40,8 @@ class tdf91762(UITestCase):
+ xEdit.executeAction("TYPE", mkPropertyValues({"KEYCODE": "RETURN"}))
+
+ # tdf#138011: Without the fix in place, this test would have failed with
+- # AssertionError: 5494 != 3559
+- self.assertEqual(5494, document.DrawPages[0].getByIndex(1).BoundRect.Height)
++ # AssertionError: 5496 != 3559
++ self.assertEqual(5496, document.DrawPages[0].getByIndex(1).BoundRect.Height)
+
+ self.ui_test.close_doc()
+
+diff --git a/sd/qa/uitest/impress_tests/textColumnsDialog.py b/sd/qa/uitest/impress_tests/textColumnsDialog.py
+new file mode 100644
+index 000000000000..266c55bada6b
+--- /dev/null
++++ b/sd/qa/uitest/impress_tests/textColumnsDialog.py
+@@ -0,0 +1,53 @@
++# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
++#
++# This file is part of the LibreOffice project.
++#
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0. If a copy of the MPL was not distributed with this
++# file, You can obtain one at http://mozilla.org/MPL/2.0/.
++#
++
++from uitest.uihelper.common import get_state_as_dict
++from libreoffice.uno.propertyvalue import mkPropertyValues
++from uitest.uihelper.common import change_measurement_unit, select_pos
++from uitest.framework import UITestCase
++from uitest.uihelper import guarded
++
++class textColumnsDialog(UITestCase):
++
++ def test_textColumnsDialog(self):
++ with guarded.create_doc_in_start_center(self, "impress") as document:
++
++ xTemplateDlg = self.xUITest.getTopFocusWindow()
++ xCancelBtn = xTemplateDlg.getChild("cancel")
++ self.ui_test.close_dialog_through_button(xCancelBtn)
++
++ xImpressDoc = self.xUITest.getTopFocusWindow()
++
++ xEditWin = xImpressDoc.getChild("impress_win")
++ xEditWin.executeAction("SELECT", mkPropertyValues({"OBJECT":"Unnamed Drawinglayer object 1"}))
++ self.assertEqual("com.sun.star.drawing.SvxShapeCollection", document.CurrentSelection.getImplementationName())
++
++ # Test defaults and set some values
++ with guarded.execute_dialog_through_command(self, ".uno:TextAttributes") as xDialog:
++ xTabs = xDialog.getChild("tabcontrol")
++ select_pos(xTabs, "2")
++ colNumber = xDialog.getChild('FLD_COL_NUMBER')
++ colSpacing = xDialog.getChild('MTR_FLD_COL_SPACING')
++ self.assertEqual('1', get_state_as_dict(colNumber)['Text'])
++ self.assertEqual('0.00″', get_state_as_dict(colSpacing)['Text'])
++ colNumber.executeAction("TYPE", mkPropertyValues({"KEYCODE": "BACKSPACE"}))
++ colNumber.executeAction("TYPE", mkPropertyValues({"TEXT": "3"}))
++ colSpacing.executeAction("TYPE", mkPropertyValues({"KEYCODE": "BACKSPACE"}))
++ colSpacing.executeAction("TYPE", mkPropertyValues({"TEXT": "1.5"}))
++
++ # Test that settings persist
++ with guarded.execute_dialog_through_command(self, ".uno:TextAttributes") as xDialog:
++ xTabs = xDialog.getChild("tabcontrol")
++ select_pos(xTabs, "2")
++ colNumber = xDialog.getChild('FLD_COL_NUMBER')
++ colSpacing = xDialog.getChild('MTR_FLD_COL_SPACING')
++ self.assertEqual('3', get_state_as_dict(colNumber)['Text'])
++ self.assertEqual('1.50″', get_state_as_dict(colSpacing)['Text'])
++
++# vim: set shiftwidth=4 softtabstop=4 expandtab:
+diff --git a/sd/qa/unit/data/xml/n593612_0.xml b/sd/qa/unit/data/xml/n593612_0.xml
+index 7c93f494e3ca..51fb75eba050 100644
+--- a/sd/qa/unit/data/xml/n593612_0.xml
++++ b/sd/qa/unit/data/xml/n593612_0.xml
+@@ -1,6 +1,6 @@
+
+
+-
++
+
+
+
+@@ -10,7 +10,7 @@
+
+
+
+-
++
+
+
+
+diff --git a/sd/qa/unit/data/xml/n758621_1.xml b/sd/qa/unit/data/xml/n758621_1.xml
+index 556c18673065..92de100a09f1 100644
+--- a/sd/qa/unit/data/xml/n758621_1.xml
++++ b/sd/qa/unit/data/xml/n758621_1.xml
+@@ -1,6 +1,6 @@
+
+
+-
++
+
+
+
+@@ -10,7 +10,7 @@
+
+
+
+-
++
+
+
+
+diff --git a/sd/qa/unit/import-tests.cxx b/sd/qa/unit/import-tests.cxx
+index e140c41c126d..5e09f0a2996f 100644
+--- a/sd/qa/unit/import-tests.cxx
++++ b/sd/qa/unit/import-tests.cxx
+@@ -89,6 +89,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+
+ #include
+@@ -225,7 +226,6 @@ public:
+ void testPatternImport();
+ void testPptCrop();
+ void testTdf120028();
+- void testTdf120028b();
+ void testDescriptionImport();
+ void testTdf83247();
+ void testTdf47365();
+@@ -335,7 +335,6 @@ public:
+ CPPUNIT_TEST(testTdf116266);
+ CPPUNIT_TEST(testPptCrop);
+ CPPUNIT_TEST(testTdf120028);
+- CPPUNIT_TEST(testTdf120028b);
+ CPPUNIT_TEST(testDescriptionImport);
+ CPPUNIT_TEST(testTdf83247);
+ CPPUNIT_TEST(testTdf47365);
+@@ -990,26 +989,20 @@ void SdImportTest::testMultiColTexts()
+ sd::DrawDocShellRef xDocShRef = loadURL( m_directories.getURLFromSrc("/sd/qa/unit/data/pptx/multicol.pptx"), PPTX );
+ const SdrPage *pPage = GetPage( 1, xDocShRef );
+
+- sdr::table::SdrTableObj *pTableObj = dynamic_cast(pPage->GetObj(0));
+- CPPUNIT_ASSERT( pTableObj );
++ auto pTextObj = dynamic_cast(pPage->GetObj(0));
++ CPPUNIT_ASSERT(pTextObj);
+
+- CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pTableObj->getRowCount());
+- CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pTableObj->getColumnCount());
++ CPPUNIT_ASSERT_EQUAL(sal_Int16(2), pTextObj->GetTextColumnsNumber());
++ CPPUNIT_ASSERT_EQUAL(sal_Int32(1000), pTextObj->GetTextColumnsSpacing());
+
+- sdr::table::SdrTableObj *pMasterTableObj = dynamic_cast(pPage->TRG_GetMasterPage().GetObj(0));
+- CPPUNIT_ASSERT( pMasterTableObj );
++ auto pMasterTextObj = dynamic_cast(pPage->TRG_GetMasterPage().GetObj(0));
++ CPPUNIT_ASSERT(pMasterTextObj);
+
+- CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pMasterTableObj->getRowCount());
+- CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pMasterTableObj->getColumnCount());
++ CPPUNIT_ASSERT_EQUAL(sal_Int16(2), pMasterTextObj->GetTextColumnsNumber());
++ CPPUNIT_ASSERT_EQUAL(sal_Int32(1000), pMasterTextObj->GetTextColumnsSpacing());
+
+- uno::Reference< table::XCellRange > xTable(pMasterTableObj->getTable(), uno::UNO_QUERY_THROW);
+- uno::Reference< beans::XPropertySet > xCell;
+- xCell.set(xTable->getCellByPosition(0, 0), uno::UNO_QUERY_THROW);
+- uno::Reference xParagraph(getParagraphFromShape(0, xCell));
+- uno::Reference xRun( getRunFromParagraph (0, xParagraph ) );
+- OUString sText = xRun->getString();
+-
+- CPPUNIT_ASSERT_EQUAL(OUString(""), sText); //We don't import master table text for multicolumn case.
++ uno::Reference xText(pMasterTextObj->getUnoShape(), uno::UNO_QUERY_THROW);
++ CPPUNIT_ASSERT_EQUAL(OUString("mastershape1\nmastershape2"), xText->getString());
+ }
+
+ void SdImportTest::testPredefinedTableStyle()
+@@ -3023,7 +3016,7 @@ void SdImportTest::testTdf116266()
+
+ void SdImportTest::testTdf120028()
+ {
+- // Check that the table shape has 4 columns.
++ // Check that the text shape has 4 columns.
+ ::sd::DrawDocShellRef xDocShRef
+ = loadURL(m_directories.getURLFromSrc("/sd/qa/unit/data/pptx/tdf120028.pptx"), PPTX);
+ uno::Reference xDoc(xDocShRef->GetDoc()->getUnoModel(),
+@@ -3033,63 +3026,22 @@ void SdImportTest::testTdf120028()
+ uno::Reference xPage(xDoc->getDrawPages()->getByIndex(0), uno::UNO_QUERY);
+ CPPUNIT_ASSERT(xPage.is());
+
+- // This failed, shape was not a table, all text was rendered in a single
+- // column.
+ uno::Reference xShape(getShape(0, xPage));
+- uno::Reference xModel(xShape->getPropertyValue("Model"),
+- uno::UNO_QUERY);
+- CPPUNIT_ASSERT(xModel.is());
+-
+- uno::Reference xColumns = xModel->getColumns();
+- CPPUNIT_ASSERT_EQUAL(static_cast(4), xColumns->getCount());
+-
+- // Check font size in the A1 cell.
+- uno::Reference xCells(xModel, uno::UNO_QUERY);
+- uno::Reference xCell(xCells->getCellByPosition(0, 0), uno::UNO_QUERY);
+- uno::Reference xParagraph(getParagraphFromShape(0, xCell));
++ uno::Reference xCols(xShape->getPropertyValue("TextColumns"),
++ uno::UNO_QUERY_THROW);
++ CPPUNIT_ASSERT_EQUAL(static_cast(4), xCols->getColumnCount());
++ uno::Reference xColProps(xCols, uno::UNO_QUERY_THROW);
++ CPPUNIT_ASSERT_EQUAL(uno::Any(sal_Int32(0)), xColProps->getPropertyValue("AutomaticDistance"));
++
++ // Check font size in the shape.
++ uno::Reference xParagraph(getParagraphFromShape(0, xShape));
+ uno::Reference xRun(getRunFromParagraph(0, xParagraph));
+- uno::Reference xPropSet(xRun, uno::UNO_QUERY);
++ uno::Reference xPropSet(xRun, uno::UNO_QUERY_THROW);
+ double fCharHeight = 0;
+ xPropSet->getPropertyValue("CharHeight") >>= fCharHeight;
+- // This failed, non-scaled height was 13.5.
+- CPPUNIT_ASSERT_DOUBLES_EQUAL(11.5, fCharHeight, 1E-12);
+-
+- xDocShRef->DoClose();
+-}
+-
+-void SdImportTest::testTdf120028b()
+-{
+- // Check that the table shape has 4 columns.
+- ::sd::DrawDocShellRef xDocShRef
+- = loadURL(m_directories.getURLFromSrc("/sd/qa/unit/data/pptx/tdf120028b.pptx"), PPTX);
+- uno::Reference xDoc(xDocShRef->GetDoc()->getUnoModel(),
+- uno::UNO_QUERY);
+- CPPUNIT_ASSERT(xDoc.is());
+-
+- uno::Reference xPage(xDoc->getDrawPages()->getByIndex(0), uno::UNO_QUERY);
+- CPPUNIT_ASSERT(xPage.is());
+-
+- uno::Reference xShape(getShape(0, xPage));
+- CPPUNIT_ASSERT(xShape.is());
+-
+- uno::Reference xModel(xShape->getPropertyValue("Model"),
+- uno::UNO_QUERY);
+- CPPUNIT_ASSERT(xModel.is());
+-
+- uno::Reference xColumns = xModel->getColumns();
+- CPPUNIT_ASSERT_EQUAL(static_cast(4), xColumns->getCount());
+-
+- // Check font color in the A1 cell.
+- uno::Reference xCells(xModel, uno::UNO_QUERY);
+- uno::Reference xCell(xCells->getCellByPosition(0, 0), uno::UNO_QUERY);
+- uno::Reference xParagraph(getParagraphFromShape(0, xCell));
+- uno::Reference xRun(getRunFromParagraph(0, xParagraph));
+- uno::Reference xPropSet(xRun, uno::UNO_QUERY);
+- sal_Int32 nCharColor = 0;
+- xPropSet->getPropertyValue("CharColor") >>= nCharColor;
+- // This was 0x1f497d, not white: text list style from placeholder shape
+- // from slide layout was ignored.
+- CPPUNIT_ASSERT_EQUAL(static_cast(0xffffff), nCharColor);
++ CPPUNIT_ASSERT_DOUBLES_EQUAL(13.5, fCharHeight, 1E-12);
++ // 13.5 * 86% is approx. 11.6 (the correct scaled font size)
++ CPPUNIT_ASSERT_EQUAL(uno::Any(sal_Int16(86)), xShape->getPropertyValue("TextFitToSizeScale"));
+
+ xDocShRef->DoClose();
+ }
+diff --git a/sd/qa/unit/misc-tests.cxx b/sd/qa/unit/misc-tests.cxx
+index 75818805295a..82734af9eb73 100644
+--- a/sd/qa/unit/misc-tests.cxx
++++ b/sd/qa/unit/misc-tests.cxx
+@@ -59,6 +59,8 @@
+ #include
+ #include
+ #include
++#include
++#include
+
+ using namespace ::com::sun::star;
+
+@@ -81,6 +83,7 @@ public:
+ void testTdf67248();
+ void testTdf119956();
+ void testTdf120527();
++ void testTextColumns();
+ void testTdf98839_ShearVFlipH();
+ void testTdf130988();
+ void testTdf131033();
+@@ -103,6 +106,7 @@ public:
+ CPPUNIT_TEST(testTdf67248);
+ CPPUNIT_TEST(testTdf119956);
+ CPPUNIT_TEST(testTdf120527);
++ CPPUNIT_TEST(testTextColumns);
+ CPPUNIT_TEST(testTdf98839_ShearVFlipH);
+ CPPUNIT_TEST(testTdf130988);
+ CPPUNIT_TEST(testTdf131033);
+@@ -492,6 +496,51 @@ void SdMiscTest::testTdf120527()
+ xDocShRef->DoClose();
+ }
+
++// Testing document model part of editengine-columns
++void SdMiscTest::testTextColumns()
++{
++ ::sd::DrawDocShellRef xDocShRef
++ = new ::sd::DrawDocShell(SfxObjectCreateMode::EMBEDDED, false, DocumentType::Impress);
++ uno::Reference xDrawPagesSupplier = getDoc(xDocShRef);
++ uno::Reference xDrawPages = xDrawPagesSupplier->getDrawPages();
++ // Insert a new page.
++ uno::Reference xDrawPage(xDrawPages->insertNewByIndex(0),
++ uno::UNO_SET_THROW);
++ uno::Reference xShapes(xDrawPage, uno::UNO_QUERY_THROW);
++ uno::Reference const xDoc(xDocShRef->GetDoc()->getUnoModel(),
++ uno::UNO_QUERY);
++
++ {
++ // Create a text shape
++ uno::Reference xShape(
++ xDoc->createInstance("com.sun.star.drawing.TextShape"), uno::UNO_QUERY_THROW);
++ uno::Reference xPropSet(xShape, uno::UNO_QUERY_THROW);
++
++ // Add the shape to the page.
++ xShapes->add(xShape);
++
++ // Set up columns
++ auto pTextObj = dynamic_cast(GetSdrObjectFromXShape(xShape));
++ CPPUNIT_ASSERT(pTextObj);
++ pTextObj->SetMergedItem(SfxInt16Item(SDRATTR_TEXTCOLUMNS_NUMBER, 2));
++ pTextObj->SetMergedItem(SdrMetricItem(SDRATTR_TEXTCOLUMNS_SPACING, 1000));
++ }
++
++ {
++ // Retrieve the shape and check columns
++ uno::Reference xIndexAccess(xDrawPage, uno::UNO_QUERY_THROW);
++ uno::Reference xShape(xIndexAccess->getByIndex(0), uno::UNO_QUERY_THROW);
++
++ auto pTextObj = dynamic_cast(GetSdrObjectFromXShape(xShape));
++ CPPUNIT_ASSERT(pTextObj);
++
++ CPPUNIT_ASSERT_EQUAL(sal_Int16(2), pTextObj->GetTextColumnsNumber());
++ CPPUNIT_ASSERT_EQUAL(sal_Int32(1000), pTextObj->GetTextColumnsSpacing());
++ }
++
++ xDocShRef->DoClose();
++}
++
+ /// Draw miscellaneous tests.
+
+ // Since LO 6.2 the visible/printable/locked information for layers is always
+diff --git a/sd/qa/unit/tiledrendering/LOKitSearchTest.cxx b/sd/qa/unit/tiledrendering/LOKitSearchTest.cxx
+index beba53b67078..18b923de4461 100644
+--- a/sd/qa/unit/tiledrendering/LOKitSearchTest.cxx
++++ b/sd/qa/unit/tiledrendering/LOKitSearchTest.cxx
+@@ -653,7 +653,7 @@ void LOKitSearchTest::testSearchIn2MixedObjects()
+ CPPUNIT_ASSERT_EQUAL(size_t(1), mpCallbackRecorder->m_aSearchResultSelection.size());
+ CPPUNIT_ASSERT_EQUAL(size_t(1), mpCallbackRecorder->m_aSearchResultPart.size());
+
+- CPPUNIT_ASSERT_EQUAL(OString("3546, 3174, 738, 402"),
++ CPPUNIT_ASSERT_EQUAL(OString("3546, 3173, 738, 401"),
+ mpCallbackRecorder->m_aSearchResultSelection[0]);
+
+ // Search next
+@@ -679,7 +679,7 @@ void LOKitSearchTest::testSearchIn2MixedObjects()
+ CPPUNIT_ASSERT_EQUAL(size_t(1), mpCallbackRecorder->m_aSearchResultSelection.size());
+ CPPUNIT_ASSERT_EQUAL(size_t(1), mpCallbackRecorder->m_aSearchResultPart.size());
+
+- CPPUNIT_ASSERT_EQUAL(OString("3546, 3174, 738, 402"),
++ CPPUNIT_ASSERT_EQUAL(OString("3546, 3173, 738, 401"),
+ mpCallbackRecorder->m_aSearchResultSelection[0]);
+ #endif
+ }
+diff --git a/sd/source/core/stlsheet.cxx b/sd/source/core/stlsheet.cxx
+index 52d05351b3b9..3f6b0b905db9 100644
+--- a/sd/source/core/stlsheet.cxx
++++ b/sd/source/core/stlsheet.cxx
+@@ -1186,6 +1186,19 @@ PropertyState SAL_CALL SdStyleSheet::getPropertyState( const OUString& PropertyN
+ return PropertyState_AMBIGUOUS_VALUE;
+ }
+ }
++ else if (pEntry->nWID == OWN_ATTR_TEXTCOLUMNS)
++ {
++ const SfxItemSet& rSet = GetItemSet();
++
++ const auto eState1 = rSet.GetItemState(SDRATTR_TEXTCOLUMNS_NUMBER, false);
++ const auto eState2 = rSet.GetItemState(SDRATTR_TEXTCOLUMNS_SPACING, false);
++ if (eState1 == SfxItemState::SET || eState2 == SfxItemState::SET)
++ return PropertyState_DIRECT_VALUE;
++ else if (eState1 == SfxItemState::DEFAULT && eState2 == SfxItemState::DEFAULT)
++ return PropertyState_DEFAULT_VALUE;
++ else
++ return PropertyState_AMBIGUOUS_VALUE;
++ }
+ else
+ {
+ SfxItemSet &rStyleSet = GetItemSet();
+diff --git a/svx/Library_svxcore.mk b/svx/Library_svxcore.mk
+index 3d4590d216a1..e8cbb606b086 100644
+--- a/svx/Library_svxcore.mk
++++ b/svx/Library_svxcore.mk
+@@ -391,6 +391,7 @@ $(eval $(call gb_Library_add_exception_objects,svxcore,\
+ svx/source/toolbars/fontworkbar \
+ svx/source/unodraw/gluepts \
+ svx/source/unodraw/shapepropertynotifier \
++ svx/source/unodraw/SvxXTextColumns \
+ svx/source/unodraw/tableshape \
+ svx/source/unodraw/unobrushitemhelper \
+ svx/source/unodraw/unobtabl \
+diff --git a/svx/source/sdr/properties/attributeproperties.cxx b/svx/source/sdr/properties/attributeproperties.cxx
+index 55dbf646210e..c79ccbd75c52 100644
+--- a/svx/source/sdr/properties/attributeproperties.cxx
++++ b/svx/source/sdr/properties/attributeproperties.cxx
+@@ -128,7 +128,8 @@ namespace sdr::properties
+ // ranges from SdrAttrObj
+ svl::Items{});
++ SDRATTR_TEXTDIRECTION, SDRATTR_TEXTDIRECTION,
++ SDRATTR_TEXTCOLUMNS_FIRST, SDRATTR_TEXTCOLUMNS_LAST>{});
+ }
+
+ AttributeProperties::AttributeProperties(SdrObject& rObj)
+diff --git a/svx/source/sdr/properties/captionproperties.cxx b/svx/source/sdr/properties/captionproperties.cxx
+index 0afd70af5234..b657b1879fe9 100644
+--- a/svx/source/sdr/properties/captionproperties.cxx
++++ b/svx/source/sdr/properties/captionproperties.cxx
+@@ -38,6 +38,7 @@ namespace sdr::properties
+ // Ranges from SdrAttrObj, SdrCaptionObj:
+ SDRATTR_START, SDRATTR_MISC_LAST,
+ SDRATTR_TEXTDIRECTION, SDRATTR_TEXTDIRECTION,
++ SDRATTR_TEXTCOLUMNS_FIRST, SDRATTR_TEXTCOLUMNS_LAST,
+ // Range from SdrTextObj:
+ EE_ITEMS_START, EE_ITEMS_END>{});
+ }
+diff --git a/svx/source/sdr/properties/circleproperties.cxx b/svx/source/sdr/properties/circleproperties.cxx
+index 895029ca7939..c0a9e6aec14e 100644
+--- a/svx/source/sdr/properties/circleproperties.cxx
++++ b/svx/source/sdr/properties/circleproperties.cxx
+@@ -42,6 +42,7 @@ namespace sdr::properties
+ SDRATTR_MISC_FIRST, SDRATTR_MISC_LAST,
+ SDRATTR_CIRC_FIRST, SDRATTR_CIRC_LAST,
+ SDRATTR_TEXTDIRECTION, SDRATTR_TEXTDIRECTION,
++ SDRATTR_TEXTCOLUMNS_FIRST, SDRATTR_TEXTCOLUMNS_LAST,
+ // Range from SdrTextObj:
+ EE_ITEMS_START, EE_ITEMS_END>{});
+ }
+diff --git a/svx/source/sdr/properties/connectorproperties.cxx b/svx/source/sdr/properties/connectorproperties.cxx
+index 29a2b7edeb5f..f5e50cb53f83 100644
+--- a/svx/source/sdr/properties/connectorproperties.cxx
++++ b/svx/source/sdr/properties/connectorproperties.cxx
+@@ -39,6 +39,7 @@ namespace sdr::properties
+ SDRATTR_START, SDRATTR_SHADOW_LAST,
+ SDRATTR_MISC_FIRST, SDRATTR_EDGE_LAST,
+ SDRATTR_TEXTDIRECTION, SDRATTR_TEXTDIRECTION,
++ SDRATTR_TEXTCOLUMNS_FIRST, SDRATTR_TEXTCOLUMNS_LAST,
+ // Range from SdrTextObj:
+ EE_ITEMS_START, EE_ITEMS_END>{});
+ }
+diff --git a/svx/source/sdr/properties/customshapeproperties.cxx b/svx/source/sdr/properties/customshapeproperties.cxx
+index 0d1443081910..ba028bd29f8f 100644
+--- a/svx/source/sdr/properties/customshapeproperties.cxx
++++ b/svx/source/sdr/properties/customshapeproperties.cxx
+@@ -73,7 +73,7 @@ namespace sdr::properties
+ // Graphic attributes, 3D properties, CustomShape
+ // properties:
+ SDRATTR_GRAF_FIRST, SDRATTR_CUSTOMSHAPE_LAST,
+- SDRATTR_GLOW_FIRST, SDRATTR_SOFTEDGE_LAST,
++ SDRATTR_GLOW_FIRST, SDRATTR_TEXTCOLUMNS_LAST,
+ // Range from SdrTextObj:
+ EE_ITEMS_START, EE_ITEMS_END>{});
+ }
+diff --git a/svx/source/sdr/properties/graphicproperties.cxx b/svx/source/sdr/properties/graphicproperties.cxx
+index 2819826caad5..966cb68ab7ff 100644
+--- a/svx/source/sdr/properties/graphicproperties.cxx
++++ b/svx/source/sdr/properties/graphicproperties.cxx
+@@ -67,7 +67,7 @@ namespace sdr::properties
+ // range from SdrGrafObj
+ SDRATTR_GRAF_FIRST, SDRATTR_GRAF_LAST,
+
+- SDRATTR_GLOW_FIRST, SDRATTR_SOFTEDGE_LAST,
++ SDRATTR_GLOW_FIRST, SDRATTR_TEXTCOLUMNS_LAST,
+
+ // range from SdrTextObj
+ EE_ITEMS_START, EE_ITEMS_END>{});
+diff --git a/svx/source/sdr/properties/measureproperties.cxx b/svx/source/sdr/properties/measureproperties.cxx
+index 5519930e3dda..67b33192d890 100644
+--- a/svx/source/sdr/properties/measureproperties.cxx
++++ b/svx/source/sdr/properties/measureproperties.cxx
+@@ -48,6 +48,7 @@ namespace sdr::properties
+ SDRATTR_MISC_FIRST, SDRATTR_MISC_LAST,
+ SDRATTR_MEASURE_FIRST, SDRATTR_MEASURE_LAST,
+ SDRATTR_TEXTDIRECTION, SDRATTR_TEXTDIRECTION,
++ SDRATTR_TEXTCOLUMNS_FIRST, SDRATTR_TEXTCOLUMNS_LAST,
+ // Range from SdrTextObj:
+ EE_ITEMS_START, EE_ITEMS_END>{});
+ }
+diff --git a/svx/source/sdr/properties/textproperties.cxx b/svx/source/sdr/properties/textproperties.cxx
+index 8a90de5c848b..3a3335b6c0e5 100644
+--- a/svx/source/sdr/properties/textproperties.cxx
++++ b/svx/source/sdr/properties/textproperties.cxx
+@@ -54,6 +54,7 @@ namespace sdr::properties
+ svl::Items{});
+@@ -87,6 +88,12 @@ namespace sdr::properties
+ // #i101556# ItemSet has changed -> new version
+ maVersion++;
+
++ if (auto pOutliner = rObj.GetTextEditOutliner())
++ {
++ pOutliner->SetTextColumns(rObj.GetTextColumnsNumber(),
++ rObj.GetTextColumnsSpacing());
++ }
++
+ const svx::ITextProvider& rTextProvider(getTextProvider());
+ sal_Int32 nText = rTextProvider.getTextCount();
+ while (nText--)
+diff --git a/svx/source/svdraw/svdattr.cxx b/svx/source/svdraw/svdattr.cxx
+index 28bbfe39afce..a0e8887da5a0 100644
+--- a/svx/source/svdraw/svdattr.cxx
++++ b/svx/source/svdraw/svdattr.cxx
+@@ -335,6 +335,9 @@ SdrItemPool::SdrItemPool(
+
+ rPoolDefaults[SDRATTR_SOFTEDGE_RADIUS - SDRATTR_START] = new SdrMetricItem(SDRATTR_SOFTEDGE_RADIUS, 0);
+
++ rPoolDefaults[SDRATTR_TEXTCOLUMNS_NUMBER - SDRATTR_START] = new SfxInt16Item(SDRATTR_TEXTCOLUMNS_NUMBER, 1);
++ rPoolDefaults[SDRATTR_TEXTCOLUMNS_SPACING - SDRATTR_START] = new SdrMetricItem(SDRATTR_TEXTCOLUMNS_SPACING, 0);
++
+ // set own ItemInfos
+ mpLocalItemInfos[SDRATTR_SHADOW-SDRATTR_START]._nSID=SID_ATTR_FILL_SHADOW;
+ mpLocalItemInfos[SDRATTR_SHADOWCOLOR-SDRATTR_START]._nSID=SID_ATTR_SHADOW_COLOR;
+@@ -356,6 +359,9 @@ SdrItemPool::SdrItemPool(
+
+ mpLocalItemInfos[SDRATTR_SOFTEDGE_RADIUS - SDRATTR_START]._nSID = SID_ATTR_SOFTEDGE_RADIUS;
+
++ mpLocalItemInfos[SDRATTR_TEXTCOLUMNS_NUMBER - SDRATTR_START]._nSID = 0 /*TODO*/;
++ mpLocalItemInfos[SDRATTR_TEXTCOLUMNS_SPACING - SDRATTR_START]._nSID = 0 /*TODO*/;
++
+ // it's my own creation level, set Defaults and ItemInfos
+ SetDefaults(mpLocalPoolDefaults);
+ SetItemInfos(mpLocalItemInfos.get());
+@@ -622,6 +628,9 @@ OUString SdrItemPool::GetItemName(sal_uInt16 nWhich)
+ case EE_FEATURE_LINEBR : pResId = SIP_EE_FEATURE_LINEBR;break;
+ case EE_FEATURE_NOTCONV : pResId = SIP_EE_FEATURE_NOTCONV;break;
+ case EE_FEATURE_FIELD : pResId = SIP_EE_FEATURE_FIELD;break;
++
++ case SDRATTR_TEXTCOLUMNS_NUMBER: pResId = SIP_SA_TEXTCOLUMNS_NUMBER; break;
++ case SDRATTR_TEXTCOLUMNS_SPACING: pResId = SIP_SA_TEXTCOLUMNS_SPACING; break;
+ } // switch
+
+ return SvxResId(pResId);
+diff --git a/svx/source/svdraw/svdotext.cxx b/svx/source/svdraw/svdotext.cxx
+index 0daeb7cc9a5d..855e07d4c4af 100644
+--- a/svx/source/svdraw/svdotext.cxx
++++ b/svx/source/svdraw/svdotext.cxx
+@@ -51,6 +51,7 @@
+ #include
+ #include
+ #include
++#include
+
+ using namespace com::sun::star;
+
+@@ -682,11 +683,13 @@ void SdrTextObj::TakeTextRect( SdrOutliner& rOutliner, tools::Rectangle& rTextRe
+ if(SDRTEXTHORZADJUST_BLOCK == eHAdj && !IsVerticalWriting())
+ {
+ rOutliner.SetMinAutoPaperSize(Size(nAnkWdt, 0));
++ rOutliner.SetMinColumnWrapHeight(nAnkHgt);
+ }
+
+ if(SDRTEXTVERTADJUST_BLOCK == eVAdj && IsVerticalWriting())
+ {
+ rOutliner.SetMinAutoPaperSize(Size(0, nAnkHgt));
++ rOutliner.SetMinColumnWrapHeight(nAnkWdt);
+ }
+ }
+
+@@ -1188,67 +1191,15 @@ void SdrTextObj::ImpSetupDrawOutlinerForPaint( bool bContourFrame,
+ }
+ }
+
+-double SdrTextObj::GetFontScaleY() const
++sal_uInt16 SdrTextObj::GetFontScaleY() const
+ {
+- SdrText* pText = getActiveText();
+- if (pText == nullptr || !pText->GetOutlinerParaObject())
+- return 1.0;
+-
+ SdrOutliner& rOutliner = ImpGetDrawOutliner();
+- const Size aShapeSize = GetSnapRect().GetSize();
+- const Size aSize(aShapeSize.Width() - GetTextLeftDistance() - GetTextRightDistance(),
+- aShapeSize.Height() - GetTextUpperDistance() - GetTextLowerDistance());
+-
+- rOutliner.SetPaperSize(aSize);
+- rOutliner.SetUpdateMode(true);
+- rOutliner.SetText(*pText->GetOutlinerParaObject());
+- bool bIsVerticalWriting = IsVerticalWriting();
+-
+- // Algorithm from SdrTextObj::ImpAutoFitText
+-
+- sal_uInt16 nMinStretchX = 0, nMinStretchY = 0;
+- sal_uInt16 nCurrStretchX = 100, nCurrStretchY = 100;
+- sal_uInt16 aOldStretchXVals[] = { 0,0,0 };
+- const size_t aStretchArySize = SAL_N_ELEMENTS(aOldStretchXVals);
+- for (unsigned int i = 0; i= 1.0)
+- {
+- nMinStretchX = std::max(nMinStretchX, nCurrStretchX);
+- nMinStretchY = std::max(nMinStretchY, nCurrStretchY);
+- }
+-
+- aOldStretchXVals[i] = nCurrStretchX;
+- if (std::find(aOldStretchXVals, aOldStretchXVals + i, nCurrStretchX) != aOldStretchXVals + i)
+- break; // same value already attained once; algo is looping, exit
++ // This eventually calls ImpAutoFitText
++ UpdateOutlinerFormatting(rOutliner, o3tl::temporary(tools::Rectangle()));
+
+- if (fFactor < 1.0 || nCurrStretchX != 100)
+- {
+- nCurrStretchX = sal::static_int_cast(nCurrStretchX*fFactor);
+- nCurrStretchY = sal::static_int_cast(nCurrStretchY*fFactor);
+- rOutliner.SetGlobalCharStretching(std::min(sal_uInt16(100), nCurrStretchX),
+- std::min(sal_uInt16(100), nCurrStretchY));
+- }
+- }
+-
+- return std::min(sal_uInt16(100), nCurrStretchY) / 100.0;
++ sal_uInt16 nStretchY;
++ rOutliner.GetGlobalCharStretching(o3tl::temporary(sal_uInt16()), nStretchY);
++ return nStretchY;
+ }
+
+ void SdrTextObj::ImpAutoFitText( SdrOutliner& rOutliner ) const
+@@ -1777,6 +1728,36 @@ SdrTextAniDirection SdrTextObj::GetTextAniDirection() const
+ return GetObjectItemSet().Get(SDRATTR_TEXT_ANIDIRECTION).GetValue();
+ }
+
++bool SdrTextObj::HasTextColumnsNumber() const
++{
++ return GetObjectItemSet().HasItem(SDRATTR_TEXTCOLUMNS_NUMBER);
++}
++
++sal_Int16 SdrTextObj::GetTextColumnsNumber() const
++{
++ return GetObjectItemSet().Get(SDRATTR_TEXTCOLUMNS_NUMBER).GetValue();
++}
++
++void SdrTextObj::SetTextColumnsNumber(sal_Int16 nColumns)
++{
++ SetObjectItem(SfxInt16Item(SDRATTR_TEXTCOLUMNS_NUMBER, nColumns));
++}
++
++bool SdrTextObj::HasTextColumnsSpacing() const
++{
++ return GetObjectItemSet().HasItem(SDRATTR_TEXTCOLUMNS_SPACING);
++}
++
++sal_Int32 SdrTextObj::GetTextColumnsSpacing() const
++{
++ return GetObjectItemSet().Get(SDRATTR_TEXTCOLUMNS_SPACING).GetValue();
++}
++
++void SdrTextObj::SetTextColumnsSpacing(sal_Int32 nSpacing)
++{
++ SetObjectItem(SdrMetricItem(SDRATTR_TEXTCOLUMNS_SPACING, nSpacing));
++}
++
+ // Get necessary data for text scroll animation. ATM base it on a Text-Metafile and a
+ // painting rectangle. Rotation is excluded from the returned values.
+ GDIMetaFile* SdrTextObj::GetTextScrollMetaFileAndRectangle(
+diff --git a/svx/source/svdraw/svdotextdecomposition.cxx b/svx/source/svdraw/svdotextdecomposition.cxx
+index de362466fe03..b54202c51847 100644
+--- a/svx/source/svdraw/svdotextdecomposition.cxx
++++ b/svx/source/svdraw/svdotextdecomposition.cxx
+@@ -770,11 +770,13 @@ void SdrTextObj::impDecomposeAutoFitTextPrimitive(
+ if(SDRTEXTHORZADJUST_BLOCK == eHAdj && !bVerticalWriting)
+ {
+ rOutliner.SetMinAutoPaperSize(Size(nAnchorTextWidth, 0));
++ rOutliner.SetMinColumnWrapHeight(nAnchorTextHeight);
+ }
+
+ if(SDRTEXTVERTADJUST_BLOCK == eVAdj && bVerticalWriting)
+ {
+ rOutliner.SetMinAutoPaperSize(Size(0, nAnchorTextHeight));
++ rOutliner.SetMinColumnWrapHeight(nAnchorTextWidth);
+ }
+
+ rOutliner.SetPaperSize(aNullSize);
+@@ -983,10 +985,12 @@ void SdrTextObj::impDecomposeBlockTextPrimitive(
+ if(bHorizontalIsBlock)
+ {
+ rOutliner.SetMinAutoPaperSize(Size(nAnchorTextWidth, 0));
++ rOutliner.SetMinColumnWrapHeight(nAnchorTextHeight);
+ }
+ else if(bVerticalIsBlock)
+ {
+ rOutliner.SetMinAutoPaperSize(Size(0, nAnchorTextHeight));
++ rOutliner.SetMinColumnWrapHeight(nAnchorTextWidth);
+ }
+
+ if((rSdrBlockTextPrimitive.getWordWrap() || IsTextFrame()) && !rSdrBlockTextPrimitive.getUnlimitedPage())
+diff --git a/svx/source/svdraw/svdotxed.cxx b/svx/source/svdraw/svdotxed.cxx
+index 6e423029f582..f62bd8d913cc 100644
+--- a/svx/source/svdraw/svdotxed.cxx
++++ b/svx/source/svdraw/svdotxed.cxx
+@@ -349,6 +349,7 @@ void SdrTextObj::ImpSetTextEditParams() const
+ pEdtOutl->SetMinAutoPaperSize(aPaperMin);
+ pEdtOutl->SetMaxAutoPaperSize(aPaperMax);
+ pEdtOutl->SetPaperSize(Size());
++ pEdtOutl->SetTextColumns(GetTextColumnsNumber(), GetTextColumnsSpacing());
+ if (bContourFrame) {
+ tools::Rectangle aAnchorRect;
+ TakeTextAnchorRect(aAnchorRect);
+diff --git a/svx/source/svdraw/svdoutl.cxx b/svx/source/svdraw/svdoutl.cxx
+index fed5e1c82f25..738309182fd7 100644
+--- a/svx/source/svdraw/svdoutl.cxx
++++ b/svx/source/svdraw/svdoutl.cxx
+@@ -57,6 +57,7 @@ void SdrOutliner::SetTextObj( const SdrTextObj* pObj )
+ SetMinAutoPaperSize( Size() );
+ SetMaxAutoPaperSize( aMaxSize );
+ SetPaperSize( aMaxSize );
++ SetTextColumns(pObj->GetTextColumnsNumber(), pObj->GetTextColumnsSpacing());
+ ClearPolygon();
+ }
+
+diff --git a/svx/source/unodraw/SvxXTextColumns.cxx b/svx/source/unodraw/SvxXTextColumns.cxx
+new file mode 100644
+index 000000000000..3d4732cc9817
+--- /dev/null
++++ b/svx/source/unodraw/SvxXTextColumns.cxx
+@@ -0,0 +1,334 @@
++/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
++/*
++ * This file is part of the LibreOffice project.
++ *
++ * This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
++ *
++ * This file incorporates work covered by the following license notice:
++ *
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements. See the NOTICE file distributed
++ * with this work for additional information regarding copyright
++ * ownership. The ASF licenses this file to you under the Apache
++ * License, Version 2.0 (the "License"); you may not use this file
++ * except in compliance with the License. You may obtain a copy of
++ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
++ */
++
++#include
++
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++
++#include
++#include
++#include
++#include
++#include
++#include