[graphics/krita] /: Implement bulk action interface for KoSvgTextShape

Dmitry Kazakov null at kde.org
Thu Nov 20 10:21:30 GMT 2025


Git commit a069089e2871a5a5c4d0081a26717f7a4df77cf7 by Dmitry Kazakov, on behalf of Wolthera van Hövell tot Westerflier.
Committed on 19/11/2025 at 08:03.
Pushed by dkazakov into branch 'master'.

Implement bulk action interface for KoSvgTextShape

Basically all updates for shapes should now be handled
by KoShapeBulkActionLock, it handles both, shapes and
its dependees:

Before:

```cpp
const QRectF oldDirtyRect = shape->boundingRect();
shape->setAbsolutePosition(d->newPositions.at(i), d->anchor);
shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
```

After:

```cpp
KoShapeBulkActionLock lock(d->shapes);

for (int i = 0; i < d->shapes.count(); i++) {
    KoShape *shape = d->shapes.at(i);
    shape->setAbsolutePosition(d->newPositions.at(i), d->anchor);
}

KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
```

I.e. all the shape updates (and its dependees) are now handled
by KoShapeBulkActionLock, don't try to do that manually.

CC:kimageshop at kde.org

M  +13   -6    libs/flake/KoShapeBulkActionLock.cpp
M  +8    -1    libs/flake/KoShapeBulkActionLock.h
M  +4    -2    libs/flake/commands/KoPathControlPointMoveCommand.cpp
M  +6    -6    libs/flake/commands/KoPathPointMoveCommand.cpp
M  +12   -5    libs/flake/commands/KoPathShapeMarkerCommand.cpp
M  +9    -4    libs/flake/commands/KoShapeMergeTextPropertiesCommand.cpp
M  +11   -6    libs/flake/commands/KoShapeMoveCommand.cpp
M  +16   -34   libs/flake/commands/KoShapeResizeCommand.cpp
M  +4    -3    libs/flake/commands/KoShapeResizeCommand.h
M  +11   -4    libs/flake/commands/KoShapeShadowCommand.cpp
M  +11   -4    libs/flake/commands/KoShapeStrokeCommand.cpp
M  +9    -4    libs/flake/commands/KoShapeTransformCommand.cpp
M  +6    -4    libs/flake/commands/KoSvgConvertTextTypeCommand.cpp
M  +9    -12   libs/flake/commands/KoSvgTextAddRemoveShapeCommands.cpp
M  +10   -6    libs/flake/commands/KoSvgTextFlipShapeContourTypeCommand.cpp
M  +7    -6    libs/flake/commands/KoSvgTextReorderShapeInsideCommand.cpp
M  +5    -1    libs/flake/text/KoSvgTextLoader.cpp
M  +69   -19   libs/flake/text/KoSvgTextShape.cpp
M  +5    -1    libs/flake/text/KoSvgTextShape.h
M  +14   -2    libs/flake/text/KoSvgTextShape_p.h
M  +1    -3    libs/flake/text/KoSvgTextShape_p_layout.cpp
M  +5    -2    plugins/tools/defaulttool/defaulttool/ShapeMoveStrategy.cpp
M  +6    -3    plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
M  +6    -2    plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
M  +3    -2    plugins/tools/svgtexttool/SvgInlineSizeChangeCommand.cpp
M  +3    -3    plugins/tools/svgtexttool/SvgMoveTextCommand.cpp
M  +8    -6    plugins/tools/svgtexttool/SvgTextChangeTransformsOnRange.cpp
M  +1    -0    plugins/tools/svgtexttool/SvgTextCursor.cpp
M  +5    -4    plugins/tools/svgtexttool/SvgTextInsertCommand.cpp
M  +5    -4    plugins/tools/svgtexttool/SvgTextInsertRichCommand.cpp
M  +5    -4    plugins/tools/svgtexttool/SvgTextMergePropertiesRangeCommand.cpp
M  +5    -6    plugins/tools/svgtexttool/SvgTextPathInfoChangeCommand.cpp
M  +7    -4    plugins/tools/svgtexttool/SvgTextRemoveCommand.cpp
M  +5    -6    plugins/tools/svgtexttool/SvgTextRemoveTransformsFromRange.cpp
M  +1    -0    plugins/tools/svgtexttool/SvgTextTypeSettingStrategy.cpp

https://invent.kde.org/graphics/krita/-/commit/a069089e2871a5a5c4d0081a26717f7a4df77cf7

diff --git a/libs/flake/KoShapeBulkActionLock.cpp b/libs/flake/KoShapeBulkActionLock.cpp
index ef6ee1720b6..e6728d40b7a 100644
--- a/libs/flake/KoShapeBulkActionLock.cpp
+++ b/libs/flake/KoShapeBulkActionLock.cpp
@@ -14,14 +14,15 @@
 KoShapeBulkActionLockAdapter::KoShapeBulkActionLockAdapter(const QList<KoShape*> &shapes)
 {
     Q_FOREACH(KoShape *shape, shapes) {
-        if (!tryAddBulkInterfaceShape(shape)) {
-            m_normalUpdateShapes.append(shape);
-        }
+        // explicitly called shaped will be "updated" in both
+        // ways, using normal updates and dependent bulk updates
+        m_normalUpdateShapes.append(shape);
+        tryAddBulkInterfaceShape(shape);
         addBulkInterfaceDependees(shape->dependees());
     }
 }
 
-bool KoShapeBulkActionLockAdapter::tryAddBulkInterfaceShape(KoShape *shape)
+void KoShapeBulkActionLockAdapter::tryAddBulkInterfaceShape(KoShape *shape)
 {
     KoShapeBulkActionInterface *iface = dynamic_cast<KoShapeBulkActionInterface *>(shape);
     if (iface) {
@@ -31,9 +32,7 @@ bool KoShapeBulkActionLockAdapter::tryAddBulkInterfaceShape(KoShape *shape)
             m_bulkInterfaceShapes.removeOne(iface);
         }
         m_bulkInterfaceShapes.append(iface);
-        return true;
     }
-    return false;
 };
 
 void KoShapeBulkActionLockAdapter::addBulkInterfaceDependees(const QList<KoShape*> dependees)
@@ -91,4 +90,12 @@ KoShapeBulkActionLock::~KoShapeBulkActionLock()
     if (owns_lock()) {
         qWarning() << "WARNING: KoShapeBulkActionLock is destroyed while being locked. Update rect will be lost!";
     }
+}
+
+void KoShapeBulkActionLock::bulkShapesUpdate(const UpdatesList &updates)
+{
+    // TODO: implement optimization for shape managers merging
+    for (auto it = updates.begin(); it != updates.end(); ++it) {
+        it->first->updateAbsolute(it->second);
+    }
 }
\ No newline at end of file
diff --git a/libs/flake/KoShapeBulkActionLock.h b/libs/flake/KoShapeBulkActionLock.h
index 2a1de493b25..571d4feb916 100644
--- a/libs/flake/KoShapeBulkActionLock.h
+++ b/libs/flake/KoShapeBulkActionLock.h
@@ -28,7 +28,7 @@ public:
     UpdatesList takeFinalUpdatesList();
 
 private:
-    bool tryAddBulkInterfaceShape(KoShape *shape);
+    void tryAddBulkInterfaceShape(KoShape *shape);
     void addBulkInterfaceDependees(const QList<KoShape*> dependees);
 
 private:
@@ -43,6 +43,11 @@ public:
     using BaseClass = KisAdaptedLock<KoShapeBulkActionLockAdapter>;
     using BaseClass::BaseClass;
 
+    template <typename T, typename = std::enable_if_t<std::is_base_of_v<KoShape, T>>>
+    KoShapeBulkActionLock(T *shape)
+        : BaseClass(QList<KoShape*>{shape})
+    {}
+
     ~KoShapeBulkActionLock();
 
     using BaseClass::Update;
@@ -59,6 +64,8 @@ public:
     using BaseClass::swap;
     using BaseClass::release;
     using BaseClass::operator bool;
+
+    static void bulkShapesUpdate(const UpdatesList &updates);
 };
 
 #endif /* KOSHAPEBULKACTIONLOCK_H */
diff --git a/libs/flake/commands/KoPathControlPointMoveCommand.cpp b/libs/flake/commands/KoPathControlPointMoveCommand.cpp
index 3c80e4581fa..714673ebb6f 100644
--- a/libs/flake/commands/KoPathControlPointMoveCommand.cpp
+++ b/libs/flake/commands/KoPathControlPointMoveCommand.cpp
@@ -9,6 +9,7 @@
 #include <klocalizedstring.h>
 #include <math.h>
 #include "kis_command_ids.h"
+#include <KoShapeBulkActionLock.h>
 
 KoPathControlPointMoveCommand::KoPathControlPointMoveCommand(
     const KoPathPointData &pointData,
@@ -35,7 +36,7 @@ void KoPathControlPointMoveCommand::redo()
     KoPathShape * pathShape = m_pointData.pathShape;
     KoPathPoint * point = pathShape->pointByIndex(m_pointData.pointIndex);
     if (point) {
-        const QRectF oldDirtyRect = pathShape->boundingRect();
+        KoShapeBulkActionLock lock(pathShape);
 
         if (m_pointType == KoPathPoint::ControlPoint1) {
             point->setControlPoint1(point->controlPoint1() + m_offset);
@@ -70,7 +71,8 @@ void KoPathControlPointMoveCommand::redo()
         }
 
         pathShape->normalize();
-        pathShape->updateAbsolute(oldDirtyRect | pathShape->boundingRect());
+
+        KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
     }
 }
 
diff --git a/libs/flake/commands/KoPathPointMoveCommand.cpp b/libs/flake/commands/KoPathPointMoveCommand.cpp
index 5ad5d4bf4f0..938d48105d9 100644
--- a/libs/flake/commands/KoPathPointMoveCommand.cpp
+++ b/libs/flake/commands/KoPathPointMoveCommand.cpp
@@ -11,6 +11,7 @@
 #include <klocalizedstring.h>
 #include "kis_command_ids.h"
 #include "krita_container_utils.h"
+#include <KoShapeBulkActionLock.h>
 
 class KoPathPointMoveCommandPrivate
 {
@@ -99,11 +100,10 @@ bool KoPathPointMoveCommand::mergeWith(const KUndo2Command *command)
 
 void KoPathPointMoveCommandPrivate::applyOffset(qreal factor)
 {
-    QMap<KoShape*, QRectF> oldDirtyRects;
+    QList<KoShape*> shapes;
+    std::copy(paths.begin(), paths.end(), std::back_inserter(shapes));
 
-    foreach (KoPathShape *path, paths) {
-        oldDirtyRects[path] = path->boundingRect();
-    }
+    KoShapeBulkActionLock lock(shapes);
 
     QMap<KoPathPointData, QPointF>::iterator it(points.begin());
     for (; it != points.end(); ++it) {
@@ -120,7 +120,7 @@ void KoPathPointMoveCommandPrivate::applyOffset(qreal factor)
 
     foreach (KoPathShape *path, paths) {
         path->normalize();
-        // repaint new bounding rect
-        path->updateAbsolute(oldDirtyRects[path] | path->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/libs/flake/commands/KoPathShapeMarkerCommand.cpp b/libs/flake/commands/KoPathShapeMarkerCommand.cpp
index fe45532d5ef..44e0e2a83ca 100644
--- a/libs/flake/commands/KoPathShapeMarkerCommand.cpp
+++ b/libs/flake/commands/KoPathShapeMarkerCommand.cpp
@@ -9,6 +9,8 @@
 #include "KoMarker.h"
 #include "KoPathShape.h"
 #include <QExplicitlySharedDataPointer>
+#include <KoShapeBulkActionLock.h>
+#include <kis_pointer_utils.h>
 
 #include "kis_command_ids.h"
 
@@ -45,30 +47,35 @@ KoPathShapeMarkerCommand::~KoPathShapeMarkerCommand()
 void KoPathShapeMarkerCommand::redo()
 {
     KUndo2Command::redo();
+
+    KoShapeBulkActionLock lock(implicitCastList<KoShape*>(m_d->shapes));
+
     Q_FOREACH (KoPathShape *shape, m_d->shapes) {
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setMarker(m_d->marker.data(), m_d->position);
 
         // we have no GUI for selection auto-filling yet! So just enable it!
         shape->setAutoFillMarkers(true);
-
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoPathShapeMarkerCommand::undo()
 {
     KUndo2Command::undo();
+
+    KoShapeBulkActionLock lock(implicitCastList<KoShape*>(m_d->shapes));
+
     auto markerIt = m_d->oldMarkers.begin();
     auto autoFillIt = m_d->oldAutoFillMarkers.begin();
     Q_FOREACH (KoPathShape *shape, m_d->shapes) {
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setMarker((*markerIt).data(), m_d->position);
         shape->setAutoFillMarkers(*autoFillIt);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
         ++markerIt;
         ++autoFillIt;
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int KoPathShapeMarkerCommand::id() const
diff --git a/libs/flake/commands/KoShapeMergeTextPropertiesCommand.cpp b/libs/flake/commands/KoShapeMergeTextPropertiesCommand.cpp
index 45f6fef7330..ab84db93025 100644
--- a/libs/flake/commands/KoShapeMergeTextPropertiesCommand.cpp
+++ b/libs/flake/commands/KoShapeMergeTextPropertiesCommand.cpp
@@ -9,6 +9,7 @@
 #include <KoSvgTextProperties.h>
 #include <kis_command_ids.h>
 #include <krita_container_utils.h>
+#include <KoShapeBulkActionLock.h>
 
 struct KoShapeMergeTextPropertiesCommand::Private {
     Private(const QList<KoShape*> &list) : shapes(list) { }
@@ -30,28 +31,32 @@ KoShapeMergeTextPropertiesCommand::KoShapeMergeTextPropertiesCommand(const QList
 
 void KoShapeMergeTextPropertiesCommand::redo()
 {
+    KoShapeBulkActionLock lock(d->shapes);
+
     for(auto it = d->shapes.begin(); it!= d->shapes.end(); it++) {
         KoSvgTextShape *shape = dynamic_cast<KoSvgTextShape*>(*it);
         if (shape) {
             d->mementos.insert(*it, shape->getMemento());
-            const QRectF oldDirtyRect = shape->boundingRect();
             shape->mergePropertiesIntoRange(-1, -1, d->newProperties, d->removeProperties);
-            shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
         }
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoShapeMergeTextPropertiesCommand::undo()
 {
+    KoShapeBulkActionLock lock(d->shapes);
+
     for(auto it = d->shapes.begin(); it!= d->shapes.end(); it++) {
         KoSvgTextShape *shape = dynamic_cast<KoSvgTextShape*>(*it);
         if (shape && d->mementos.contains(*it)) {
-            const QRectF oldDirtyRect = shape->boundingRect();
             shape->setMemento(d->mementos.value(*it));
-            shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
         }
     }
     d->mementos.clear();
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int KoShapeMergeTextPropertiesCommand::id() const
diff --git a/libs/flake/commands/KoShapeMoveCommand.cpp b/libs/flake/commands/KoShapeMoveCommand.cpp
index fdb638eb579..6a59fb85c8c 100644
--- a/libs/flake/commands/KoShapeMoveCommand.cpp
+++ b/libs/flake/commands/KoShapeMoveCommand.cpp
@@ -11,6 +11,8 @@
 #include <klocalizedstring.h>
 #include "kis_command_ids.h"
 #include <kis_assert.h>
+#include <KoShapeBulkActionLock.h>
+
 
 class Q_DECL_HIDDEN KoShapeMoveCommand::Private
 {
@@ -56,25 +58,28 @@ void KoShapeMoveCommand::redo()
 {
     KUndo2Command::redo();
 
+    KoShapeBulkActionLock lock(d->shapes);
+
     for (int i = 0; i < d->shapes.count(); i++) {
         KoShape *shape = d->shapes.at(i);
-
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setAbsolutePosition(d->newPositions.at(i), d->anchor);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoShapeMoveCommand::undo()
 {
     KUndo2Command::undo();
+
+    KoShapeBulkActionLock lock(d->shapes);
+
     for (int i = 0; i < d->shapes.count(); i++) {
         KoShape *shape = d->shapes.at(i);
-
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setAbsolutePosition(d->previousPositions.at(i), d->anchor);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int KoShapeMoveCommand::id() const
diff --git a/libs/flake/commands/KoShapeResizeCommand.cpp b/libs/flake/commands/KoShapeResizeCommand.cpp
index 40dd42ed87c..68b03510dae 100644
--- a/libs/flake/commands/KoShapeResizeCommand.cpp
+++ b/libs/flake/commands/KoShapeResizeCommand.cpp
@@ -8,7 +8,7 @@
 
 #include <KoShape.h>
 #include "kis_command_ids.h"
-#include "kis_assert.h"
+#include <KoShapeBulkActionLock.h>
 
 
 struct Q_DECL_HIDDEN KoShapeResizeCommand::Private
@@ -56,57 +56,42 @@ KoShapeResizeCommand::~KoShapeResizeCommand()
 
 void KoShapeResizeCommand::redoImpl()
 {
-    QMap<KoShape*, QRectF> updates = redoNoUpdate();
+    KoShapeBulkActionLock lock(m_d->shapes);
 
-    for (auto it = updates.begin(); it != updates.end(); ++it) {
-        it.key()->updateAbsolute(it.value());
-    }
+    redoNoUpdate();
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoShapeResizeCommand::undoImpl()
 {
-    QMap<KoShape*, QRectF> updates = undoNoUpdate();
+    KoShapeBulkActionLock lock(m_d->shapes);
 
-    for (auto it = updates.begin(); it != updates.end(); ++it) {
-        it.key()->updateAbsolute(it.value());
-    }
+    undoNoUpdate();
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
-QMap<KoShape*, QRectF> KoShapeResizeCommand::redoNoUpdate()
+void KoShapeResizeCommand::redoNoUpdate()
 {
-    QMap<KoShape*,QRectF> updates;
-
     Q_FOREACH (KoShape *shape, m_d->shapes) {
-        const QRectF oldDirtyRect = shape->boundingRect();
-
         KoFlake::resizeShapeCommon(shape,
                              m_d->scaleX, m_d->scaleY,
                              m_d->absoluteStillPoint,
                              m_d->useGlobalMode,
                              m_d->usePostScaling,
                              m_d->postScalingCoveringTransform);
-
-        updates[shape] = oldDirtyRect | shape->boundingRect();
     }
-
-    return updates;
 }
 
-QMap<KoShape*, QRectF> KoShapeResizeCommand::undoNoUpdate()
+void KoShapeResizeCommand::undoNoUpdate()
 {
-    QMap<KoShape*,QRectF> updates;
-
     for (int i = 0; i < m_d->shapes.size(); i++) {
         KoShape *shape = m_d->shapes[i];
 
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setSize(m_d->oldSizes[i]);
         shape->setTransformation(m_d->oldTransforms[i]);
-
-        updates[shape] = oldDirtyRect | shape->boundingRect();
     }
-
-    return updates;
 }
 
 int KoShapeResizeCommand::id() const
@@ -144,18 +129,15 @@ bool KoShapeResizeCommand::mergeWith(const KUndo2Command *command)
 
 void KoShapeResizeCommand::replaceResizeAction(qreal scaleX, qreal scaleY, const QPointF &absoluteStillPoint)
 {
-    const QMap<KoShape*, QRectF> undoUpdates = undoNoUpdate();
+    KoShapeBulkActionLock lock(m_d->shapes);
+
+    undoNoUpdate();
 
     m_d->scaleX = scaleX;
     m_d->scaleY = scaleY;
     m_d->absoluteStillPoint = absoluteStillPoint;
 
-    const QMap<KoShape*, QRectF> redoUpdates = redoNoUpdate();
-
-    KIS_SAFE_ASSERT_RECOVER_NOOP(undoUpdates.size() == redoUpdates.size());
+    redoNoUpdate();
 
-    for (auto it = undoUpdates.begin(); it != undoUpdates.end(); ++it) {
-        KIS_SAFE_ASSERT_RECOVER_NOOP(redoUpdates.contains(it.key()));
-        it.key()->updateAbsolute(it.value() | redoUpdates[it.key()]);
-    }
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/libs/flake/commands/KoShapeResizeCommand.h b/libs/flake/commands/KoShapeResizeCommand.h
index 3e0d530e665..e8916741af3 100644
--- a/libs/flake/commands/KoShapeResizeCommand.h
+++ b/libs/flake/commands/KoShapeResizeCommand.h
@@ -33,15 +33,16 @@ public:
     void redoImpl() override;
     void undoImpl() override;
 
-    QMap<KoShape*, QRectF> redoNoUpdate();
-    QMap<KoShape*, QRectF> undoNoUpdate();
-
     int id() const override;
     bool mergeWith(const KUndo2Command *command) override;
 
     void replaceResizeAction(qreal scaleX, qreal scaleY,
                              const QPointF &absoluteStillPoint);
 
+private:
+    void redoNoUpdate();
+    void undoNoUpdate();
+
 private:
     struct Private;
     QScopedPointer<Private> const m_d;
diff --git a/libs/flake/commands/KoShapeShadowCommand.cpp b/libs/flake/commands/KoShapeShadowCommand.cpp
index c3b60364c1d..23f5f4c977a 100644
--- a/libs/flake/commands/KoShapeShadowCommand.cpp
+++ b/libs/flake/commands/KoShapeShadowCommand.cpp
@@ -7,6 +7,7 @@
 #include "KoShapeShadowCommand.h"
 #include "KoShape.h"
 #include "KoShapeShadow.h"
+#include <KoShapeBulkActionLock.h>
 
 #include <klocalizedstring.h>
 
@@ -90,6 +91,9 @@ KoShapeShadowCommand::~KoShapeShadowCommand()
 void KoShapeShadowCommand::redo()
 {
     KUndo2Command::redo();
+
+    KoShapeBulkActionLock lock(d->shapes);
+
     int shapeCount = d->shapes.count();
     for (int i = 0; i < shapeCount; ++i) {
         KoShape *shape = d->shapes[i];
@@ -97,16 +101,19 @@ void KoShapeShadowCommand::redo()
         // TODO: implement comparison operator for KoShapeShadow
         //       (or just deprecate it entirely)
         if (shape->shadow() || d->newShadows[i]) {
-            const QRectF oldBoundingRect = shape->boundingRect();
             shape->setShadow(d->newShadows[i]);
-            shape->updateAbsolute(oldBoundingRect | shape->boundingRect());
         }
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoShapeShadowCommand::undo()
 {
     KUndo2Command::undo();
+
+    KoShapeBulkActionLock lock(d->shapes);
+
     int shapeCount = d->shapes.count();
     for (int i = 0; i < shapeCount; ++i) {
         KoShape *shape = d->shapes[i];
@@ -114,9 +121,9 @@ void KoShapeShadowCommand::undo()
         // TODO: implement comparison operator for KoShapeShadow
         //       (or just deprecate it entirely)
         if (shape->shadow() || d->oldShadows[i]) {
-            const QRectF oldBoundingRect = shape->boundingRect();
             shape->setShadow(d->oldShadows[i]);
-            shape->updateAbsolute(oldBoundingRect | shape->boundingRect());
         }
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/libs/flake/commands/KoShapeStrokeCommand.cpp b/libs/flake/commands/KoShapeStrokeCommand.cpp
index eb5cb19af8c..945c0e6fcd0 100644
--- a/libs/flake/commands/KoShapeStrokeCommand.cpp
+++ b/libs/flake/commands/KoShapeStrokeCommand.cpp
@@ -10,6 +10,7 @@
 #include "KoShapeStrokeCommand.h"
 #include "KoShape.h"
 #include "KoShapeStrokeModel.h"
+#include <KoShapeBulkActionLock.h>
 
 #include <klocalizedstring.h>
 
@@ -92,25 +93,31 @@ KoShapeStrokeCommand::~KoShapeStrokeCommand()
 void KoShapeStrokeCommand::redo()
 {
     KUndo2Command::redo();
+
+    KoShapeBulkActionLock lock(d->shapes);
+
     QList<KoShapeStrokeModelSP>::iterator strokeIt = d->newStrokes.begin();
     Q_FOREACH (KoShape *shape, d->shapes) {
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setStroke(*strokeIt);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
         ++strokeIt;
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoShapeStrokeCommand::undo()
 {
     KUndo2Command::undo();
+
+    KoShapeBulkActionLock lock(d->shapes);
+
     QList<KoShapeStrokeModelSP>::iterator strokeIt = d->oldStrokes.begin();
     Q_FOREACH (KoShape *shape, d->shapes) {
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setStroke(*strokeIt);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
         ++strokeIt;
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int KoShapeStrokeCommand::id() const
diff --git a/libs/flake/commands/KoShapeTransformCommand.cpp b/libs/flake/commands/KoShapeTransformCommand.cpp
index 1f92da43ead..2b2ee352fa9 100644
--- a/libs/flake/commands/KoShapeTransformCommand.cpp
+++ b/libs/flake/commands/KoShapeTransformCommand.cpp
@@ -7,6 +7,7 @@
 
 #include "kis_command_ids.h"
 
+#include <KoShapeBulkActionLock.h>
 #include "KoShapeTransformCommand.h"
 #include "KoShape.h"
 
@@ -43,26 +44,30 @@ void KoShapeTransformCommand::redo()
 {
     KUndo2Command::redo();
 
+    KoShapeBulkActionLock lock(d->shapes);
+
     const int shapeCount = d->shapes.count();
     for (int i = 0; i < shapeCount; ++i) {
         KoShape * shape = d->shapes[i];
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setTransformation(d->newState[i]);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoShapeTransformCommand::undo()
 {
     KUndo2Command::undo();
 
+    KoShapeBulkActionLock lock(d->shapes);
+
     const int shapeCount = d->shapes.count();
     for (int i = 0; i < shapeCount; ++i) {
         KoShape * shape = d->shapes[i];
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setTransformation(d->oldState[i]);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int KoShapeTransformCommand::id() const
diff --git a/libs/flake/commands/KoSvgConvertTextTypeCommand.cpp b/libs/flake/commands/KoSvgConvertTextTypeCommand.cpp
index ffe01a0ff66..b7b78d988a6 100644
--- a/libs/flake/commands/KoSvgConvertTextTypeCommand.cpp
+++ b/libs/flake/commands/KoSvgConvertTextTypeCommand.cpp
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 #include "KoSvgConvertTextTypeCommand.h"
+#include <KoShapeBulkActionLock.h>
 
 KoSvgConvertTextTypeCommand::KoSvgConvertTextTypeCommand(KoSvgTextShape *shape, KoSvgTextShape::TextType type, int pos, KUndo2Command *parent)
     : KUndo2Command(parent)
@@ -16,7 +17,8 @@ KoSvgConvertTextTypeCommand::KoSvgConvertTextTypeCommand(KoSvgTextShape *shape,
 
 void KoSvgConvertTextTypeCommand::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
+
     m_textData = m_shape->getMemento();
     const int oldIndex = qMax(0, m_shape->indexForPos(m_pos));
 
@@ -28,14 +30,14 @@ void KoSvgConvertTextTypeCommand::redo()
         m_shape->setCharacterTransformsFromLayout();
     }
 
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
     const int pos = m_shape->posForIndex(oldIndex, false, false);
     m_shape->notifyCursorPosChanged(pos, pos);
 }
 
 void KoSvgConvertTextTypeCommand::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->setMemento(m_textData, m_pos, m_pos);
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/libs/flake/commands/KoSvgTextAddRemoveShapeCommands.cpp b/libs/flake/commands/KoSvgTextAddRemoveShapeCommands.cpp
index ca32c3df451..90dcfc85f00 100644
--- a/libs/flake/commands/KoSvgTextAddRemoveShapeCommands.cpp
+++ b/libs/flake/commands/KoSvgTextAddRemoveShapeCommands.cpp
@@ -8,6 +8,7 @@
 #include <optional>
 #include "kis_debug.h"
 
+#include <KoShapeBulkActionLock.h>
 #include <KoSvgTextShape.h>
 #include <KoShapeContainer.h>
 
@@ -70,8 +71,8 @@ void KoSvgTextAddRemoveShapeCommandImpl::partB()
         d->memento = d->textShape->getMemento();
         d->oldTextPaths = d->textShape->textPathsAtRange(d->startPos, d->endPos);
     }
-    QRectF updateRectText = d->textShape->boundingRect();
-    QRectF updateRectShape = d->shape->boundingRect();
+
+    KoShapeBulkActionLock lock(QList<KoShape*>({d->textShape, d->shape}));
 
     KoShapeContainer *newParent = d->originalShapeParent.has_value() ? d->originalShapeParent.value() : d->textShape->parent();
     const QTransform newParentTransform = newParent ? newParent->absoluteTransformation() : QTransform();
@@ -96,10 +97,8 @@ void KoSvgTextAddRemoveShapeCommandImpl::partB()
         d->textShape->relayout();
     }
     d->shape->applyAbsoluteTransformation(absoluteTf);
-    updateRectText |= d->textShape->boundingRect();
-    updateRectShape |= d->shape->boundingRect();
-    d->textShape->updateAbsolute(updateRectText);
-    d->shape->updateAbsolute(updateRectShape);
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 // Add shape to text.
@@ -110,8 +109,8 @@ void KoSvgTextAddRemoveShapeCommandImpl::partA()
         d->oldTextPaths = d->textShape->textPathsAtRange(d->startPos, d->endPos);
         d->originalShapeParent = d->shape->parent();
     }
-    QRectF updateRect = d->textShape->boundingRect();
-    updateRect |= d->shape->boundingRect();
+
+    KoShapeBulkActionLock lock(QList<KoShape*>({d->textShape, d->shape}));
 
     KoShapeContainer *oldParent = d->shape->parent();
     const QTransform oldParentTransform = oldParent ? oldParent->absoluteTransformation() : QTransform();
@@ -136,10 +135,8 @@ void KoSvgTextAddRemoveShapeCommandImpl::partA()
         d->textShape->relayout();
     }
     d->shape->applyAbsoluteTransformation(absoluteTf);
-    updateRect |= d->textShape->boundingRect();
-    updateRect |= d->shape->boundingRect();
-    d->shape->updateAbsolute(updateRect);
-    d->textShape->updateAbsolute(updateRect);
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 KoSvgTextAddShapeCommand::KoSvgTextAddShapeCommand(KoSvgTextShape *textShape, KoShape *shape, bool inside, KUndo2Command *parentCommand)
diff --git a/libs/flake/commands/KoSvgTextFlipShapeContourTypeCommand.cpp b/libs/flake/commands/KoSvgTextFlipShapeContourTypeCommand.cpp
index cfe5760aaaa..41de2590bda 100644
--- a/libs/flake/commands/KoSvgTextFlipShapeContourTypeCommand.cpp
+++ b/libs/flake/commands/KoSvgTextFlipShapeContourTypeCommand.cpp
@@ -6,6 +6,8 @@
 #include "KoSvgTextFlipShapeContourTypeCommand.h"
 
 #include <KoSvgTextShape.h>
+#include <KoShapeBulkActionLock.h>
+
 
 struct KoSvgTextFlipShapeContourTypeCommand::Private {
     Private(KoSvgTextShape *_text, KoShape *_shape)
@@ -34,16 +36,18 @@ KoSvgTextFlipShapeContourTypeCommand::~KoSvgTextFlipShapeContourTypeCommand()
 
 void KoSvgTextFlipShapeContourTypeCommand::redo()
 {
-    QRectF updateRect = d->textShape->boundingRect();
+    KoShapeBulkActionLock lock(d->textShape);
+
     d->textShape->addShapeContours({d->shape}, !(d->isInside));
-    updateRect |= d->textShape->boundingRect();
-    d->textShape->updateAbsolute(updateRect);
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoSvgTextFlipShapeContourTypeCommand::undo()
 {
-    QRectF updateRect = d->textShape->boundingRect();
+    KoShapeBulkActionLock lock(d->textShape);
+
     d->textShape->addShapeContours({d->shape}, d->isInside);
-    updateRect |= d->textShape->boundingRect();
-    d->textShape->updateAbsolute(updateRect);
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/libs/flake/commands/KoSvgTextReorderShapeInsideCommand.cpp b/libs/flake/commands/KoSvgTextReorderShapeInsideCommand.cpp
index d9d3a9252d8..496562c74a5 100644
--- a/libs/flake/commands/KoSvgTextReorderShapeInsideCommand.cpp
+++ b/libs/flake/commands/KoSvgTextReorderShapeInsideCommand.cpp
@@ -5,6 +5,7 @@
  */
 #include "KoSvgTextReorderShapeInsideCommand.h"
 #include <KoSvgTextShape.h>
+#include <KoShapeBulkActionLock.h>
 
 
 struct KoSvgTextReorderShapeInsideCommand::Private {
@@ -44,7 +45,7 @@ KoSvgTextReorderShapeInsideCommand::~KoSvgTextReorderShapeInsideCommand()
 
 void KoSvgTextReorderShapeInsideCommand::redo()
 {
-    QRectF updateRect = d->textShape->boundingRect();
+    KoShapeBulkActionLock lock(d->textShape);
 
     int newIndex = d->textShape->shapesInside().indexOf(d->shapes.first());
     const int max = (d->textShape->shapesInside().size() -1);
@@ -81,13 +82,13 @@ void KoSvgTextReorderShapeInsideCommand::redo()
         }
     }
 
-    updateRect |= d->textShape->boundingRect();
-    d->textShape->updateAbsolute(updateRect);
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void KoSvgTextReorderShapeInsideCommand::undo()
 {
-    QRectF updateRect = d->textShape->boundingRect();
+    KoShapeBulkActionLock lock(d->textShape);
+
     if (d->type == MoveEarlier || d->type == BringToFront) {
         for (int i = d->oldIndices.size() -1 ; i>= 0; i--) {
             d->textShape->moveShapeInsideToIndex(d->shapes.at(i), d->oldIndices.at(i));
@@ -98,6 +99,6 @@ void KoSvgTextReorderShapeInsideCommand::undo()
         }
     }
     d->textShape->setMemento(d->memento);
-    updateRect |= d->textShape->boundingRect();
-    d->textShape->updateAbsolute(updateRect);
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/libs/flake/text/KoSvgTextLoader.cpp b/libs/flake/text/KoSvgTextLoader.cpp
index fc0c54a6700..be2259e06cb 100644
--- a/libs/flake/text/KoSvgTextLoader.cpp
+++ b/libs/flake/text/KoSvgTextLoader.cpp
@@ -34,7 +34,11 @@ KoSvgTextLoader::~KoSvgTextLoader()
     d->shape->d->updateShapeGroup();
     d->shape->cleanUp();
     d->shape->d->isLoading = false;
-    d->shape->relayout();
+
+    // finish loading by synchronizing the internal group
+    d->shape->d->shapeGroup->setTransformation(d->shape->absoluteTransformation());
+    d->shape->d->updateTextWrappingAreas();
+    d->shape->notifyChanged();
 }
 
 void KoSvgTextLoader::enterNodeSubtree()
diff --git a/libs/flake/text/KoSvgTextShape.cpp b/libs/flake/text/KoSvgTextShape.cpp
index 06c696616db..4cf417c5026 100644
--- a/libs/flake/text/KoSvgTextShape.cpp
+++ b/libs/flake/text/KoSvgTextShape.cpp
@@ -177,18 +177,19 @@ KoShape *KoSvgTextShape::cloneShape() const
 
 void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape)
 {
+    KoShape::shapeChanged(type, shape);
+
     if (d->isLoading) {
         return;
     }
-    KoShape::shapeChanged(type, shape);
 
     const QVector<ChangeType> transformationTypes = {PositionChanged, RotationChanged, ScaleChanged, ShearChanged, SizeChanged, GenericMatrixChange};
 
-    qDebug() << shape << type;
     if ((d->shapesInside.contains(shape) || d->shapesSubtract.contains(shape) || d->textPaths.contains(shape))
             && (transformationTypes.contains(type)
                 || type == ParameterChanged
                 || type == ParentChanged
+                || type == ContentChanged // for KoPathShape modifications
                 || type == Deleted)) {
         if (type == Deleted) {
             if (d->shapesInside.contains(shape)) {
@@ -209,28 +210,38 @@ void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape)
 
         // Updates the contours and calls relayout.
         // Would be great if we could compress the updates here somehow...
-        d->updateTextWrappingAreas();
-        // NotifyChanged ensures that boundingRect() is called on this shape.
-        this->notifyChanged();
-        if (d->shapesSubtract.contains(shape)) {
-            // Shape subtract will otherwise only
-            // update it's own bounding rect.
-            this->update();
+        if (d->bulkActionState) {
+            d->bulkActionState->contourHasChanged = true;
+        } else {
+            d->updateTextWrappingAreas();
         }
-        qDebug() << "child updated";
+
+        // NotifyChanged ensures that boundingRect() is called on this shape;
+        // it is NOT compressed by the bulk action, since boundingRect() is
+        // guaranteed to be valid during the whole bulk action
+        this->notifyChanged();
     }
     if ((!shape || shape == this)) {
-        qDebug() << "text updated";
-        if ((type == ContentChanged)) {
-            relayout();
+        if (type == ContentChanged) {
+            if (d->bulkActionState) {
+                d->bulkActionState->layoutHasChanged = true;
+            } else {
+                relayout();
+            }
         } else if (transformationTypes.contains(type) || type == ParentChanged) {
-            qDebug() << "update transform" << this->absoluteTransformation();
             d->shapeGroup->setTransformation(this->absoluteTransformation());
         } else if (type == TextRunAroundChanged) {
             // Hack: we don't use runaround else where, so we're using it for padding and margin.
-            qDebug() << "update wrapping areas";
-            d->updateTextWrappingAreas();
+            if (d->bulkActionState) {
+                d->bulkActionState->contourHasChanged = true;
+            } else {
+                d->updateTextWrappingAreas();
+            }
         }
+
+        // when calling shapeChangedImpl() on `this` shape, we call
+        // notifyChanged() manually at the calling site, so we shouldn't
+        // do that again here
     }
 }
 
@@ -1936,6 +1947,33 @@ QList<QPainterPath> KoSvgTextShape::generateTextAreas(const QList<KoShape *> sha
     return Private::generateShapes(shapesInside, shapesSubtract, props);
 }
 
+void KoSvgTextShape::startBulkAction()
+{
+    KIS_SAFE_ASSERT_RECOVER_RETURN(!d->bulkActionState);
+    d->bulkActionState.emplace(boundingRect());
+}
+
+QRectF KoSvgTextShape::endBulkAction()
+{
+    KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(d->bulkActionState, boundingRect());
+
+    QRectF updateRect;
+
+    if (d->bulkActionState->changed()) {
+        if (d->bulkActionState->contourHasChanged) {
+            d->updateTextWrappingAreas();
+        } else if (d->bulkActionState->layoutHasChanged) {
+            // updateTextWrappingAreas() already includes a call to relayout()
+            relayout();
+        }
+
+        updateRect = d->bulkActionState->originalBoundingRect | boundingRect();
+    }
+
+    d->bulkActionState = std::nullopt;
+    return updateRect;
+}
+
 void KoSvgTextShape::paint(QPainter &painter) const
 {
     painter.save();
@@ -1982,6 +2020,7 @@ QPainterPath KoSvgTextShape::outline() const {
             result.addPath(shape->transformation().map(shape->outline()));
         }
     }
+
     if ((d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
         for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
             result.addPath(it->associatedOutline);
@@ -2074,6 +2113,8 @@ void KoSvgTextShape::setSize(const QSizeF &size)
         notifyChanged();
         shapeChangedPriv(ScaleChanged);
     } else {
+        if (!d->textPaths.isEmpty()) return;
+
         const bool allInternalShapeAreTranslatedOnly = [this] () {
             Q_FOREACH(KoShape *shape, d->internalShapes()) {
                 if (shape->transformation().type() > QTransform::TxTranslate) {
@@ -2282,8 +2323,13 @@ void KoSvgTextShape::addShapeContours(QList<KoShape *> shapes, const bool inside
             shape->addDependee(this);
         }
     }
+
     notifyChanged(); // notify shape manager that our geometry has changed
-    d->updateTextWrappingAreas();
+
+    if (!d->isLoading) {
+        d->updateTextWrappingAreas();
+    }
+
     d->updateInternalShapesList();
     shapeChangedPriv(ContentChanged);
     update();
@@ -2312,7 +2358,9 @@ void KoSvgTextShape::removeShapesFromContours(QList<KoShape *> shapes, bool call
     }
     if (callUpdate) {
         notifyChanged(); // notify shape manager that our geometry has changed
-        d->updateTextWrappingAreas();
+        if (!d->isLoading) {
+            d->updateTextWrappingAreas();
+        }
         d->updateInternalShapesList();
         shapeChangedPriv(ContentChanged);
         update();
@@ -2326,7 +2374,9 @@ void KoSvgTextShape::moveShapeInsideToIndex(KoShape *shapeInside, const int inde
 
     // Update.
     d->shapesInside.move(oldIndex, index);
-    d->updateTextWrappingAreas();
+    if (!d->isLoading) {
+        d->updateTextWrappingAreas();
+    }
     d->updateInternalShapesList();
     shapeChangedPriv(ContentChanged);
     update();
diff --git a/libs/flake/text/KoSvgTextShape.h b/libs/flake/text/KoSvgTextShape.h
index 90beadc2e4b..df4eb6b5e29 100644
--- a/libs/flake/text/KoSvgTextShape.h
+++ b/libs/flake/text/KoSvgTextShape.h
@@ -13,6 +13,7 @@
 #include <KoShapeFactoryBase.h>
 #include <SvgShape.h>
 #include <KoShape.h>
+#include <KoShapeBulkActionInterface.h>
 #include <KoSvgText.h>
 #include "html/HtmlSavingContext.h"
 #include <QFlags>
@@ -28,7 +29,7 @@ typedef QSharedPointer<KoSvgTextShapeMemento> KoSvgTextShapeMementoSP;
 /**
  * KoSvgTextShape is a root chunk of the \<text\> element subtree.
  */
-class KRITAFLAKE_EXPORT KoSvgTextShape : public KoShape, public SvgShape
+class KRITAFLAKE_EXPORT KoSvgTextShape : public KoShape, public SvgShape, public KoShapeBulkActionInterface
 {
 public:
     KoSvgTextShape();
@@ -656,6 +657,9 @@ public:
      */
     static QList<QPainterPath> generateTextAreas(const QList<KoShape*> shapesInside, const QList<KoShape*> shapesSubtract, const KoSvgTextProperties &props);
 
+    void startBulkAction() override;
+    QRectF endBulkAction() override;
+
 protected:
 
     void shapeChanged(ChangeType type, KoShape *shape) override;
diff --git a/libs/flake/text/KoSvgTextShape_p.h b/libs/flake/text/KoSvgTextShape_p.h
index c22accbf12a..63ea0e81d7c 100644
--- a/libs/flake/text/KoSvgTextShape_p.h
+++ b/libs/flake/text/KoSvgTextShape_p.h
@@ -531,17 +531,29 @@ public:
         Q_FOREACH(KoShape *shape, textPaths) {
             shapeGroup->addShape(shape);
         }
-        qDebug() << "updateing contours";
         updateTextWrappingAreas();
         updateInternalShapesList();
     }
     void updateInternalShapesList() {
         if (shapeGroup) {
-            qDebug() << Q_FUNC_INFO << shapesInside.size() << shapeGroup->shapes().size();
             internalShapesPainter->setShapes(shapeGroup->shapes());
         }
     }
 
+    struct BulkActionState {
+        BulkActionState(QRectF originalBoundingRectArg) : originalBoundingRect(originalBoundingRectArg) {}
+
+        QRectF originalBoundingRect;
+        bool contourHasChanged = false;
+        bool layoutHasChanged = false;
+
+        bool changed() const {
+            return contourHasChanged || layoutHasChanged;
+        }
+    };
+
+    std::optional<BulkActionState> bulkActionState;
+
     QList<KoShape*> shapesInside;
     QList<KoShape*> shapesSubtract;
     QList<KoShape*> textPaths;
diff --git a/libs/flake/text/KoSvgTextShape_p_layout.cpp b/libs/flake/text/KoSvgTextShape_p_layout.cpp
index 0e0247a6998..719db97fb65 100644
--- a/libs/flake/text/KoSvgTextShape_p_layout.cpp
+++ b/libs/flake/text/KoSvgTextShape_p_layout.cpp
@@ -114,9 +114,7 @@ void KoSvgTextShape::Private::updateTextWrappingAreas()
 {
     KoSvgTextProperties rootProperties = textData.empty()? KoSvgTextProperties::defaultProperties(): textData.childBegin()->properties;
     currentTextWrappingAreas = getShapes(shapesInside, shapesSubtract, rootProperties);
-    if (!isLoading) {
-        relayout();
-    }
+    relayout();
 }
 
 QList<QPainterPath> KoSvgTextShape::Private::generateShapes(const QList<KoShape *> shapesInside, const QList<KoShape *> shapesSubtract, const KoSvgTextProperties &properties)
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeMoveStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeMoveStrategy.cpp
index 649c59a8f0d..d2dabdfdac8 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeMoveStrategy.cpp
+++ b/plugins/tools/defaulttool/defaulttool/ShapeMoveStrategy.cpp
@@ -15,6 +15,7 @@
 #include <KoShapeContainerModel.h>
 #include <KoCanvasResourceProvider.h>
 #include <commands/KoShapeMoveCommand.h>
+#include <KoShapeBulkActionLock.h>
 #include <KoSnapGuide.h>
 #include <KoPointerEvent.h>
 #include <KoToolBase.h>
@@ -71,6 +72,8 @@ void ShapeMoveStrategy::moveSelection(const QPointF &diff)
 {
     Q_ASSERT(m_newPositions.count());
 
+    KoShapeBulkActionLock lock(m_selectedShapes);
+
     int i = 0;
     Q_FOREACH (KoShape *shape, m_selectedShapes) {
         QPointF delta = m_previousPositions.at(i) + diff - shape->absolutePosition(KoFlake::Center);
@@ -81,11 +84,11 @@ void ShapeMoveStrategy::moveSelection(const QPointF &diff)
         QPointF newPos(shape->absolutePosition(KoFlake::Center) + delta);
         m_newPositions[i] = newPos;
 
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->setAbsolutePosition(newPos, KoFlake::Center);
-        shape->updateAbsolute(oldDirtyRect | oldDirtyRect.translated(delta));
         i++;
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 KUndo2Command *ShapeMoveStrategy::createCommand()
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
index 5987407577c..62dc0ebbaff 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
+++ b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
@@ -15,6 +15,7 @@
 #include <KoShapeManager.h>
 #include <KoCanvasResourceProvider.h>
 #include <commands/KoShapeTransformCommand.h>
+#include <KoShapeBulkActionLock.h>
 
 #include <QPointF>
 #include <math.h>
@@ -73,12 +74,14 @@ void ShapeRotateStrategy::rotateBy(qreal angle)
 
     QTransform applyMatrix = matrix * m_rotationMatrix.inverted();
     m_rotationMatrix = matrix;
+
+    KoShapeBulkActionLock lock(m_transformedShapesAndSelection);
+
     Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
-        QRectF dirtyRect = shape->boundingRect();
         shape->applyAbsoluteTransformation(applyMatrix);
-        dirtyRect |= shape->boundingRect();
-        shape->updateAbsolute(dirtyRect);
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void ShapeRotateStrategy::paint(QPainter &painter, const KoViewConverter &converter)
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
index dd997ad9efe..efc5faf7f13 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
+++ b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
@@ -16,6 +16,7 @@
 #include <commands/KoShapeShearCommand.h>
 #include <commands/KoShapeMoveCommand.h>
 #include <commands/KoShapeTransformCommand.h>
+#include <KoShapeBulkActionLock.h>
 
 #include <KoSelection.h>
 #include <QPointF>
@@ -147,11 +148,14 @@ void ShapeShearStrategy::handleMouseMove(const QPointF &point, Qt::KeyboardModif
 
     QTransform applyMatrix = matrix * m_shearMatrix.inverted();
 
+    KoShapeBulkActionLock lock(m_transformedShapesAndSelection);
+
     Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
-        const QRectF oldDirtyRect = shape->boundingRect();
         shape->applyAbsoluteTransformation(applyMatrix);
-        shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
     }
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
+
     m_shearMatrix = matrix;
 }
 
diff --git a/plugins/tools/svgtexttool/SvgInlineSizeChangeCommand.cpp b/plugins/tools/svgtexttool/SvgInlineSizeChangeCommand.cpp
index 79db66fd5c8..40c03edfd13 100644
--- a/plugins/tools/svgtexttool/SvgInlineSizeChangeCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgInlineSizeChangeCommand.cpp
@@ -15,6 +15,7 @@
 #include "KoSvgTextProperties.h"
 #include "KoSvgTextShape.h"
 #include "KoSvgTextShapeMarkupConverter.h"
+#include <KoShapeBulkActionLock.h>
 
 #include "kis_assert.h"
 #include "kis_command_ids.h"
@@ -46,7 +47,7 @@ SvgInlineSizeChangeCommand::SvgInlineSizeChangeCommand(KoSvgTextShape *shape,
 
 void SvgInlineSizeChangeCommand::applyInlineSize(double inlineSize, int anchor, QPointF pos, bool undo)
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
 
     KoSvgTextProperties properties = m_shape->propertiesForPos(-1);
     KoSvgText::AutoValue inlineSizeProp = properties.propertyOrDefault(KoSvgTextProperties::InlineSizeId).value<KoSvgText::AutoValue>();
@@ -63,7 +64,7 @@ void SvgInlineSizeChangeCommand::applyInlineSize(double inlineSize, int anchor,
         m_shape->setPropertiesAtPos(-1, properties);
     }
 
-    m_shape->updateAbsolute(updateRect | m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void SvgInlineSizeChangeCommand::redo()
diff --git a/plugins/tools/svgtexttool/SvgMoveTextCommand.cpp b/plugins/tools/svgtexttool/SvgMoveTextCommand.cpp
index fcc55bb98ea..1da79be71a9 100644
--- a/plugins/tools/svgtexttool/SvgMoveTextCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgMoveTextCommand.cpp
@@ -10,6 +10,7 @@
 
 #include "KoSvgTextShape.h"
 #include "kis_command_ids.h"
+#include <KoShapeBulkActionLock.h>
 
 SvgMoveTextCommand::SvgMoveTextCommand(KoSvgTextShape *shape,
                                        const QPointF &newPosition,
@@ -25,10 +26,9 @@ SvgMoveTextCommand::SvgMoveTextCommand(KoSvgTextShape *shape,
 
 static void moveShape(KoSvgTextShape *shape, const QPointF &position)
 {
-    QRectF updateRect = shape->boundingRect();
+    KoShapeBulkActionLock lock(shape);
     shape->setAbsolutePosition(position);
-    updateRect |= shape->boundingRect();
-    shape->updateAbsolute(updateRect);
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 void SvgMoveTextCommand::redo()
diff --git a/plugins/tools/svgtexttool/SvgTextChangeTransformsOnRange.cpp b/plugins/tools/svgtexttool/SvgTextChangeTransformsOnRange.cpp
index 33a7cb48860..7b25c586f44 100644
--- a/plugins/tools/svgtexttool/SvgTextChangeTransformsOnRange.cpp
+++ b/plugins/tools/svgtexttool/SvgTextChangeTransformsOnRange.cpp
@@ -5,6 +5,7 @@
  */
 #include "SvgTextChangeTransformsOnRange.h"
 #include "kis_command_ids.h"
+#include <KoShapeBulkActionLock.h>
 
 SvgTextChangeTransformsOnRange::SvgTextChangeTransformsOnRange(KoSvgTextShape *shape, int startPos, int endPos, QVector<QPointF> positions, QVector<qreal> rotations, bool calculateDeltaPositions, KUndo2Command *parentCommand)
     : KUndo2Command(parentCommand)
@@ -70,27 +71,28 @@ SvgTextChangeTransformsOnRange::SvgTextChangeTransformsOnRange(KoSvgTextShape *s
 
 void SvgTextChangeTransformsOnRange::undo()
 {
-    QRectF updateRect = m_textShape->boundingRect();
+    KoShapeBulkActionLock lock(m_textShape);
 
     m_textShape->setMemento(m_textData);
 
-    updateRect |= m_textShape->boundingRect();
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
+
     m_textShape->notifyCursorPosChanged(m_startPos, m_endPos);
-    m_textShape->updateAbsolute(updateRect);
+
 }
 
 void SvgTextChangeTransformsOnRange::redo()
 {
-    QRectF updateRect = m_textShape->boundingRect();
+    KoShapeBulkActionLock lock(m_textShape);
 
     const int posIndex = m_textShape->indexForPos(m_startPos);
     const int posAnchor = m_textShape->indexForPos(m_endPos);
 
     m_textShape->setCharacterTransformsOnRange(m_startPos, m_endPos, m_positions, m_rotations, m_calculateDeltaPositions);
 
-    updateRect |= m_textShape->boundingRect();
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
+
     m_textShape->notifyCursorPosChanged(m_textShape->posForIndex(posIndex), m_textShape->posForIndex(posAnchor));
-    m_textShape->updateAbsolute(updateRect);
 }
 
 int SvgTextChangeTransformsOnRange::id() const
diff --git a/plugins/tools/svgtexttool/SvgTextCursor.cpp b/plugins/tools/svgtexttool/SvgTextCursor.cpp
index 696d55e56eb..6fac31a52a7 100644
--- a/plugins/tools/svgtexttool/SvgTextCursor.cpp
+++ b/plugins/tools/svgtexttool/SvgTextCursor.cpp
@@ -1142,6 +1142,7 @@ void SvgTextCursor::inputMethodEvent(QInputMethodEvent *event)
     d->blockQueryUpdates = false;
     qApp->inputMethod()->update(Qt::ImQueryInput);
     updateRect |= d->shape->boundingRect();
+    // TODO: replace with KoShapeBulkActionLock
     d->shape->updateAbsolute(updateRect);
     d->styleMap = styleMap;
     updateIMEDecoration();
diff --git a/plugins/tools/svgtexttool/SvgTextInsertCommand.cpp b/plugins/tools/svgtexttool/SvgTextInsertCommand.cpp
index 0ca15dd8610..f3a12fa27db 100644
--- a/plugins/tools/svgtexttool/SvgTextInsertCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgTextInsertCommand.cpp
@@ -6,6 +6,7 @@
 #include "SvgTextInsertCommand.h"
 #include "KoSvgTextShape.h"
 #include "KoSvgTextShapeMarkupConverter.h"
+#include <KoShapeBulkActionLock.h>
 
 #include <QRegExp>
 
@@ -40,14 +41,14 @@ SvgTextInsertCommand::SvgTextInsertCommand(KoSvgTextShape *shape, int pos, int a
 
 void SvgTextInsertCommand::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     // Index defaults to -1 when there's no text in the shape.
     int oldIndex = qMax(0, m_shape->indexForPos(m_pos));
 
     m_textData = m_shape->getMemento();
     m_shape->insertText(m_pos, m_text);
     m_shape->cleanUp();
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 
     int pos = m_shape->posForIndex(oldIndex + m_text.size(), false, false);
     m_shape->notifyCursorPosChanged(pos, pos);
@@ -56,9 +57,9 @@ void SvgTextInsertCommand::redo()
 
 void SvgTextInsertCommand::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->setMemento(m_textData, m_pos, m_anchor);
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int SvgTextInsertCommand::id() const
diff --git a/plugins/tools/svgtexttool/SvgTextInsertRichCommand.cpp b/plugins/tools/svgtexttool/SvgTextInsertRichCommand.cpp
index 6353a5a67fe..c39c0b52e9d 100644
--- a/plugins/tools/svgtexttool/SvgTextInsertRichCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgTextInsertRichCommand.cpp
@@ -5,6 +5,7 @@
  */
 #include "SvgTextInsertRichCommand.h"
 #include "KoSvgTextShapeMarkupConverter.h"
+#include <KoShapeBulkActionLock.h>
 
 SvgTextInsertRichCommand::SvgTextInsertRichCommand(KoSvgTextShape *shape, KoSvgTextShape *insert, int pos, int anchor, KUndo2Command *parent)
     : KUndo2Command(parent)
@@ -18,14 +19,14 @@ SvgTextInsertRichCommand::SvgTextInsertRichCommand(KoSvgTextShape *shape, KoSvgT
 
 void SvgTextInsertRichCommand::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     // Index defaults to -1 when there's no text in the shape.
     int oldIndex = qMax(0, m_shape->indexForPos(m_pos));
 
     m_textData = m_shape->getMemento();
     m_shape->insertRichText(m_pos, m_insert);
     m_shape->cleanUp();
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 
     int pos = m_shape->posForIndex(oldIndex + m_insert->plainText().size(), false, false);
     m_shape->notifyCursorPosChanged(pos, pos);
@@ -34,7 +35,7 @@ void SvgTextInsertRichCommand::redo()
 
 void SvgTextInsertRichCommand::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->setMemento(m_textData, m_pos, m_anchor);
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
diff --git a/plugins/tools/svgtexttool/SvgTextMergePropertiesRangeCommand.cpp b/plugins/tools/svgtexttool/SvgTextMergePropertiesRangeCommand.cpp
index d46825d02dd..477ac64fd1a 100644
--- a/plugins/tools/svgtexttool/SvgTextMergePropertiesRangeCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgTextMergePropertiesRangeCommand.cpp
@@ -7,6 +7,7 @@
 
 #include "KoSvgTextShape.h"
 #include "kis_command_ids.h"
+#include <KoShapeBulkActionLock.h>
 
 SvgTextMergePropertiesRangeCommand::SvgTextMergePropertiesRangeCommand(KoSvgTextShape *shape,
                                                                        const KoSvgTextProperties props,
@@ -31,18 +32,18 @@ SvgTextMergePropertiesRangeCommand::SvgTextMergePropertiesRangeCommand(KoSvgText
 
 void SvgTextMergePropertiesRangeCommand::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->mergePropertiesIntoRange(qMin(m_pos, m_anchor), qMax(m_pos, m_anchor), m_props, m_removeProperties);
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 
     m_shape->notifyMarkupChanged();
 }
 
 void SvgTextMergePropertiesRangeCommand::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->setMemento(m_textData, m_pos, m_anchor);
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int SvgTextMergePropertiesRangeCommand::id() const
diff --git a/plugins/tools/svgtexttool/SvgTextPathInfoChangeCommand.cpp b/plugins/tools/svgtexttool/SvgTextPathInfoChangeCommand.cpp
index f78ea07745b..f21312eb494 100644
--- a/plugins/tools/svgtexttool/SvgTextPathInfoChangeCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgTextPathInfoChangeCommand.cpp
@@ -5,6 +5,7 @@
  */
 #include "SvgTextPathInfoChangeCommand.h"
 #include "kis_command_ids.h"
+#include <KoShapeBulkActionLock.h>
 
 SvgTextPathInfoChangeCommand::SvgTextPathInfoChangeCommand(KoSvgTextShape *shape, int pos, KoSvgText::TextOnPathInfo textPathInfo, KUndo2Command *parent)
     : KUndo2Command(parent)
@@ -18,7 +19,7 @@ SvgTextPathInfoChangeCommand::SvgTextPathInfoChangeCommand(KoSvgTextShape *shape
 
 void SvgTextPathInfoChangeCommand::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     KoSvgTextNodeIndex index = m_shape->topLevelNodeForPos(m_pos);
 
 
@@ -37,17 +38,15 @@ void SvgTextPathInfoChangeCommand::redo()
 
     m_shape->relayout();
     m_shape->notifyChanged();
-    updateRect |= m_shape->boundingRect();
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
     m_shape->notifyCursorPosChanged(m_pos, m_pos);
-    m_shape->updateAbsolute(updateRect);
 }
 
 void SvgTextPathInfoChangeCommand::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->setMemento(m_textData, m_pos, m_pos);
-    updateRect |= m_shape->boundingRect();
-    m_shape->updateAbsolute(updateRect);
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int SvgTextPathInfoChangeCommand::id() const
diff --git a/plugins/tools/svgtexttool/SvgTextRemoveCommand.cpp b/plugins/tools/svgtexttool/SvgTextRemoveCommand.cpp
index d9b4f0c2b1e..cd653bbe5fc 100644
--- a/plugins/tools/svgtexttool/SvgTextRemoveCommand.cpp
+++ b/plugins/tools/svgtexttool/SvgTextRemoveCommand.cpp
@@ -10,6 +10,8 @@
 
 #include "KoSvgTextShape.h"
 #include "KoSvgTextShapeMarkupConverter.h"
+#include <KoShapeBulkActionLock.h>
+
 SvgTextRemoveCommand::SvgTextRemoveCommand(KoSvgTextShape *shape,
                                            int endIndex, int pos,
                                            int anchor,
@@ -30,7 +32,7 @@ SvgTextRemoveCommand::SvgTextRemoveCommand(KoSvgTextShape *shape,
 
 void SvgTextRemoveCommand::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
 
     m_textData = m_shape->getMemento();
 
@@ -41,7 +43,8 @@ void SvgTextRemoveCommand::redo()
     if (m_allowCleanUp) {
         m_shape->cleanUp();
     }
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 
     int pos = qMax(0, m_shape->posForIndex(idx, false, false));
     m_shape->notifyCursorPosChanged(pos, pos);
@@ -49,9 +52,9 @@ void SvgTextRemoveCommand::redo()
 
 void SvgTextRemoveCommand::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
     m_shape->setMemento(m_textData, m_originalPos, m_anchor);
-    m_shape->updateAbsolute( updateRect| m_shape->boundingRect());
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
 }
 
 int SvgTextRemoveCommand::id() const
diff --git a/plugins/tools/svgtexttool/SvgTextRemoveTransformsFromRange.cpp b/plugins/tools/svgtexttool/SvgTextRemoveTransformsFromRange.cpp
index 13bcee79ce1..18af8c50f39 100644
--- a/plugins/tools/svgtexttool/SvgTextRemoveTransformsFromRange.cpp
+++ b/plugins/tools/svgtexttool/SvgTextRemoveTransformsFromRange.cpp
@@ -5,6 +5,7 @@
  */
 
 #include "SvgTextRemoveTransformsFromRange.h"
+#include <KoShapeBulkActionLock.h>
 
 SvgTextRemoveTransformsFromRange::SvgTextRemoveTransformsFromRange(KoSvgTextShape *shape, int pos, int anchor, KUndo2Command *parent)
     : KUndo2Command(parent)
@@ -18,25 +19,23 @@ SvgTextRemoveTransformsFromRange::SvgTextRemoveTransformsFromRange(KoSvgTextShap
 
 void SvgTextRemoveTransformsFromRange::undo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
 
     m_shape->setMemento(m_textData);
 
-    updateRect |= m_shape->boundingRect();
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
     m_shape->notifyCursorPosChanged(m_pos, m_anchor);
-    m_shape->updateAbsolute(updateRect);
 }
 
 void SvgTextRemoveTransformsFromRange::redo()
 {
-    QRectF updateRect = m_shape->boundingRect();
+    KoShapeBulkActionLock lock(m_shape);
 
     const int indexPos = m_shape->indexForPos(m_pos);
     const int indexAnchor = m_shape->indexForPos(m_anchor);
 
     m_shape->removeTransformsFromRange(m_pos, m_anchor);
 
-    updateRect |= m_shape->boundingRect();
+    KoShapeBulkActionLock::bulkShapesUpdate(lock.unlock());
     m_shape->notifyCursorPosChanged(m_shape->posForIndex(indexPos), m_shape->posForIndex(indexAnchor));
-    m_shape->updateAbsolute(updateRect);
 }
diff --git a/plugins/tools/svgtexttool/SvgTextTypeSettingStrategy.cpp b/plugins/tools/svgtexttool/SvgTextTypeSettingStrategy.cpp
index 71a926f81d2..ffdb604488a 100644
--- a/plugins/tools/svgtexttool/SvgTextTypeSettingStrategy.cpp
+++ b/plugins/tools/svgtexttool/SvgTextTypeSettingStrategy.cpp
@@ -50,6 +50,7 @@ void SvgTextTypeSettingStrategy::handleMouseMove(const QPointF &mouseLocation, Q
 
     if (m_editingType != int(SvgTextCursor::NoHandle)) {
         SvgTextShapeManagerBlocker blocker(tool()->canvas()->shapeManager());
+        // TODO: replace with KoShapeBulkActionLock (recursive locking is not supported right now)
         QRectF updateRect = m_shape->boundingRect();
         if (m_previousCmd) {
             m_previousCmd->undo();



More information about the kimageshop mailing list