[krita] /: Implemented moving multiple selected layers at once

Dmitry Kazakov dimula73 at gmail.com
Tue Apr 5 11:51:32 UTC 2016


Git commit 28d985bcb91b5ca2e4cd410e7f01db14d30ec748 by Dmitry Kazakov.
Committed on 05/04/2016 at 11:48.
Pushed by dkazakov into branch 'master'.

Implemented moving multiple selected layers at once

Please test if it breaks anything in the Move tool!
(it shouldn't though :) )

Ref T2082
BUG:359559
CC:kimageshop at kde.org

M  +25   -2    libs/image/kis_layer_utils.cpp
M  +4    -2    libs/image/kis_layer_utils.h
M  +11   -0    libs/image/krita_utils.h
M  +8    -0    libs/ui/tool/kis_tool.cc
M  +1    -0    libs/ui/tool/kis_tool.h
M  +65   -85   plugins/tools/basictools/kis_tool_move.cc
M  +3    -1    plugins/tools/basictools/kis_tool_move.h
M  +31   -28   plugins/tools/basictools/strokes/move_stroke_strategy.cpp
M  +3    -12   plugins/tools/basictools/strokes/move_stroke_strategy.h
M  +1    -1    plugins/tools/basictools/tests/move_stroke_test.cpp

http://commits.kde.org/krita/28d985bcb91b5ca2e4cd410e7f01db14d30ec748

diff --git a/libs/image/kis_layer_utils.cpp b/libs/image/kis_layer_utils.cpp
index 46f11f0..7fd6f02 100644
--- a/libs/image/kis_layer_utils.cpp
+++ b/libs/image/kis_layer_utils.cpp
@@ -377,7 +377,7 @@ namespace KisLayerUtils {
         }
     }
 
-    bool RemoveNodeHelper::checkIsSourceForClone(KisNodeSP src, const QList<KisNodeSP> &nodes) {
+    bool RemoveNodeHelper::checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes) {
         foreach (KisNodeSP node, nodes) {
             if (node == src) continue;
 
@@ -696,7 +696,7 @@ namespace KisLayerUtils {
         return false;
     }
 
-    void filterMergableNodes(QList<KisNodeSP> &nodes, bool allowMasks)
+    void filterMergableNodes(KisNodeList &nodes, bool allowMasks)
     {
         QList<KisNodeSP>::iterator it = nodes.begin();
 
@@ -744,6 +744,29 @@ namespace KisLayerUtils {
         return result;
     }
 
+    KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks)
+    {
+        KIS_ASSERT_RECOVER(!nodes.isEmpty()) { return nodes; }
+
+        KisNodeSP root;
+        Q_FOREACH(KisNodeSP node, nodes) {
+            KisNodeSP localRoot = node;
+            while (localRoot->parent()) {
+                localRoot = localRoot->parent();
+            }
+
+            if (!root) {
+                root = localRoot;
+            }
+            KIS_ASSERT_RECOVER(root == localRoot) { return nodes; }
+        }
+
+        KisNodeList result;
+        sortMergableNodes(root, nodes, result);
+        filterMergableNodes(result, allowMasks);
+        return result;
+    }
+
     void addCopyOfNameTag(KisNodeSP node)
     {
         const QString prefix = i18n("Copy of");
diff --git a/libs/image/kis_layer_utils.h b/libs/image/kis_layer_utils.h
index 47a1871..e733953 100644
--- a/libs/image/kis_layer_utils.h
+++ b/libs/image/kis_layer_utils.h
@@ -37,9 +37,11 @@ namespace KisLayerUtils
 {
     KRITAIMAGE_EXPORT void sortMergableNodes(KisNodeSP root, QList<KisNodeSP> &inputNodes, QList<KisNodeSP> &outputNodes);
     KRITAIMAGE_EXPORT KisNodeList sortMergableNodes(KisNodeSP root, KisNodeList nodes);
-    KRITAIMAGE_EXPORT void filterMergableNodes(QList<KisNodeSP> &nodes, bool allowMasks = false);
+    KRITAIMAGE_EXPORT void filterMergableNodes(KisNodeList &nodes, bool allowMasks = false);
     KRITAIMAGE_EXPORT bool checkIsChildOf(KisNodeSP node, const QList<KisNodeSP> &parents);
 
+    KRITAIMAGE_EXPORT KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks = false);
+
     KRITAIMAGE_EXPORT void mergeDown(KisImageSP image, KisLayerSP layer, const KisMetaData::MergeStrategy* strategy);
 
     KRITAIMAGE_EXPORT QSet<int> fetchLayerFrames(KisNodeSP node);
@@ -116,7 +118,7 @@ namespace KisLayerUtils
         virtual void addCommandImpl(KUndo2Command *cmd) = 0;
         void safeRemoveMultipleNodes(QList<KisNodeSP> nodes, KisImageSP image);
     private:
-        bool checkIsSourceForClone(KisNodeSP src, const QList<KisNodeSP> &nodes);
+        bool checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes);
         static bool scanForLastLayer(KisImageWSP image, KisNodeList nodesToRemove);
     };
 
diff --git a/libs/image/krita_utils.h b/libs/image/krita_utils.h
index 00bb08a..a1f19b3 100644
--- a/libs/image/krita_utils.h
+++ b/libs/image/krita_utils.h
@@ -31,6 +31,8 @@ class QPainter;
 #include <QVector>
 #include "kritaimage_export.h"
 #include "kis_types.h"
+#include <functional>
+
 
 namespace KritaUtils
 {
@@ -93,6 +95,15 @@ namespace KritaUtils
         }
     }
 
+    template <class C>
+        void filterContainer(C &container, std::function<bool(typename C::reference)> keepIf) {
+
+        auto newEnd = std::remove_if(container.begin(), container.end(), std::unary_negate<decltype(keepIf)>(keepIf));
+        while (newEnd != container.end()) {
+            newEnd = container.erase(newEnd);
+        }
+    }
+
     /**
      * When drawing a rect Qt uses quite a weird algorithm. It
      * draws 4 lines:
diff --git a/libs/ui/tool/kis_tool.cc b/libs/ui/tool/kis_tool.cc
index 4719f22..c95ea01 100644
--- a/libs/ui/tool/kis_tool.cc
+++ b/libs/ui/tool/kis_tool.cc
@@ -41,6 +41,7 @@
 #include <KoSnapGuide.h>
 
 #include <KisViewManager.h>
+#include "kis_node_manager.h"
 #include <kis_selection.h>
 #include <kis_image.h>
 #include <kis_group_layer.h>
@@ -412,6 +413,13 @@ KisNodeSP KisTool::currentNode()
     return node;
 }
 
+KisNodeList KisTool::selectedNodes() const
+{
+    KisCanvas2 * kiscanvas = static_cast<KisCanvas2*>(canvas());
+    KisViewManager* viewManager = kiscanvas->viewManager();
+    return viewManager->nodeManager()->selectedNodes();
+}
+
 KoColor KisTool::currentFgColor()
 {
     return d->currentFgColor;
diff --git a/libs/ui/tool/kis_tool.h b/libs/ui/tool/kis_tool.h
index a65665b..2e66071 100644
--- a/libs/ui/tool/kis_tool.h
+++ b/libs/ui/tool/kis_tool.h
@@ -286,6 +286,7 @@ protected:
     KoPattern* currentPattern();
     KoAbstractGradient *currentGradient();
     KisNodeSP currentNode();
+    KisNodeList selectedNodes() const;
     KoColor currentFgColor();
     KoColor currentBgColor();
     KisPaintOpPresetSP currentPaintOpPreset();
diff --git a/plugins/tools/basictools/kis_tool_move.cc b/plugins/tools/basictools/kis_tool_move.cc
index 638a9df..c1c2a8b 100644
--- a/plugins/tools/basictools/kis_tool_move.cc
+++ b/plugins/tools/basictools/kis_tool_move.cc
@@ -37,6 +37,8 @@
 #include "strokes/move_selection_stroke_strategy.h"
 #include "kis_resources_snapshot.h"
 #include "kis_action_registry.h"
+#include "krita_utils.h"
+
 
 KisToolMove::KisToolMove(KoCanvasBase * canvas)
         :  KisTool(canvas, KisCursor::moveCursor())
@@ -93,57 +95,83 @@ void KisToolMove::resetCursorStyle()
     overrideCursorIfNotEditable();
 }
 
-void KisToolMove::moveDiscrete(MoveDirection direction, bool big)
+bool KisToolMove::startStrokeImpl(MoveToolMode mode, const QPoint *pos)
 {
-    if (mode() == KisTool::PAINT_MODE) return;  // Don't interact with dragging
-
-    setMode(KisTool::PAINT_MODE);
-
     KisNodeSP node;
+    KisNodeList nodes;
     KisImageSP image = this->image();
 
     KisResourcesSnapshotSP resources =
         new KisResourcesSnapshot(image, currentNode(), 0, this->canvas()->resourceManager());
     KisSelectionSP selection = resources->activeSelection();
 
-    // Move current layer by default.
-    if ((!node && !(node = resources->currentNode())) || !node->isEditable()) {
-        return;
+    if (mode != MoveSelectedLayer && pos) {
+        bool wholeGroup = !selection &&  mode == MoveGroup;
+        node = KisToolUtils::findNode(image->root(), *pos, wholeGroup);
+        nodes = {node};
     }
 
+    if (nodes.isEmpty()) {
+        nodes = this->selectedNodes();
 
-    // If node has changed, end current stroke.
-    if (m_strokeId && (node != m_currentlyProcessingNode)) {
-        endStroke();
+        KritaUtils::filterContainer(nodes,
+                                    [](KisNodeSP node) {
+                                        return node->isEditable();
+                                    });
     }
 
+    if (nodes.size() == 1) {
+        node = nodes.first();
+    }
+
+    if (nodes.isEmpty()) {
+        return false;
+    }
 
     /**
-     * Begin a new stroke if necessary.
+     * If the target node has changed, the stroke should be
+     * restarted. Otherwise just continue processing current node.
      */
-    if (!m_strokeId) {
-        KisStrokeStrategy *strategy;
+    if (m_strokeId) {
+        if (KritaUtils::compareListsUnordered(nodes, m_currentlyProcessingNodes)) {
+            return true;
+        } else {
+            endStroke();
+        }
+    }
 
-        KisPaintLayerSP paintLayer =
-            dynamic_cast<KisPaintLayer*>(node.data());
+    KisStrokeStrategy *strategy;
 
-        if (paintLayer && selection &&
-            !selection->isTotallyUnselected(image->bounds())) {
+    KisPaintLayerSP paintLayer = node ?
+        dynamic_cast<KisPaintLayer*>(node.data()) : 0;
 
-            strategy =
-                new MoveSelectionStrokeStrategy(paintLayer,
-                                                selection,
-                                                image.data(),
-                                                image->postExecutionUndoAdapter());
-        } else {
-            strategy =
-                new MoveStrokeStrategy(node, image.data(),
-                                       image->postExecutionUndoAdapter());
-        }
+    if (paintLayer && selection &&
+        !selection->isTotallyUnselected(image->bounds())) {
 
-        m_strokeId = image->startStroke(strategy);
-        m_currentlyProcessingNode = node;
-        m_accumulatedOffset = QPoint();
+        strategy =
+            new MoveSelectionStrokeStrategy(paintLayer,
+                                            selection,
+                                            image.data(),
+                                            image->postExecutionUndoAdapter());
+    } else {
+        strategy =
+            new MoveStrokeStrategy(nodes, image.data(),
+                                   image->postExecutionUndoAdapter());
+    }
+
+    m_strokeId = image->startStroke(strategy);
+    m_currentlyProcessingNodes = nodes;
+    m_accumulatedOffset = QPoint();
+
+    return true;
+}
+
+void KisToolMove::moveDiscrete(MoveDirection direction, bool big)
+{
+    if (mode() == KisTool::PAINT_MODE) return;  // Don't interact with dragging
+
+    if (startStrokeImpl(MoveSelectedLayer, 0)) {
+        setMode(KisTool::PAINT_MODE);
     }
 
     // Larger movement if "shift" key is pressed.
@@ -155,7 +183,7 @@ void KisToolMove::moveDiscrete(MoveDirection direction, bool big)
                     direction == Left ? QPoint(-moveStep,  0) :
                                         QPoint( moveStep,  0) ;
 
-    image->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset + offset));
+    image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset + offset));
     m_accumulatedOffset += offset;
 
 
@@ -252,59 +280,11 @@ void KisToolMove::startAction(KoPointerEvent *event, MoveToolMode mode)
     m_moveInProgress = true;
     emit moveInProgressChanged();
 
-    KisNodeSP node;
-    KisImageSP image = this->image();
-
-    KisResourcesSnapshotSP resources =
-        new KisResourcesSnapshot(image, currentNode(), 0, this->canvas()->resourceManager());
-    KisSelectionSP selection = resources->activeSelection();
-
-    if (mode != MoveSelectedLayer) {
-        bool wholeGroup = !selection &&  mode == MoveGroup;
-        node = KisToolUtils::findNode(image->root(), pos, wholeGroup);
-    }
-
-    if ((!node && !(node = resources->currentNode())) || !node->isEditable()) {
-        event->ignore();
-        return;
-    }
-
-    setMode(KisTool::PAINT_MODE);
-
-    /**
-     * If the target node has changed, the stroke should be
-     * restarted. Otherwise just continue processing current node.
-     */
-    if (m_strokeId) {
-        if (node == m_currentlyProcessingNode) {
-            return;
-        } else {
-            endStroke();
-        }
-    }
-
-    KisStrokeStrategy *strategy;
-
-    KisPaintLayerSP paintLayer =
-        dynamic_cast<KisPaintLayer*>(node.data());
-
-    if (paintLayer && selection &&
-        !selection->isTotallyUnselected(image->bounds())) {
-
-        strategy =
-            new MoveSelectionStrokeStrategy(paintLayer,
-                                            selection,
-                                            image.data(),
-                                            image->postExecutionUndoAdapter());
+    if (startStrokeImpl(mode, &pos)) {
+        setMode(KisTool::PAINT_MODE);
     } else {
-        strategy =
-            new MoveStrokeStrategy(node, image.data(),
-                                   image->postExecutionUndoAdapter());
+        event->ignore();
     }
-
-    m_strokeId = image->startStroke(strategy);
-    m_currentlyProcessingNode = node;
-    m_accumulatedOffset = QPoint();
 }
 
 void KisToolMove::continueAction(KoPointerEvent *event)
@@ -348,7 +328,7 @@ void KisToolMove::endStroke()
     KisImageWSP image = currentImage();
     image->endStroke(m_strokeId);
     m_strokeId.clear();
-    m_currentlyProcessingNode.clear();
+    m_currentlyProcessingNodes.clear();
     m_moveInProgress = false;
     emit moveInProgressChanged();
 }
@@ -360,7 +340,7 @@ void KisToolMove::cancelStroke()
     KisImageWSP image = currentImage();
     image->cancelStroke(m_strokeId);
     m_strokeId.clear();
-    m_currentlyProcessingNode.clear();
+    m_currentlyProcessingNodes.clear();
     m_moveInProgress = false;
     emit moveInProgressChanged();
 }
diff --git a/plugins/tools/basictools/kis_tool_move.h b/plugins/tools/basictools/kis_tool_move.h
index 87973c0..35e1338 100644
--- a/plugins/tools/basictools/kis_tool_move.h
+++ b/plugins/tools/basictools/kis_tool_move.h
@@ -100,6 +100,8 @@ private:
     void cancelStroke();
     QPoint applyModifiers(Qt::KeyboardModifiers modifiers, QPoint pos);
 
+    bool startStrokeImpl(MoveToolMode mode, const QPoint *pos);
+
 private Q_SLOTS:
     void endStroke();
 
@@ -113,7 +115,7 @@ private:
     KisStrokeId m_strokeId;
 
     bool m_moveInProgress;
-    KisNodeSP m_currentlyProcessingNode;
+    KisNodeList m_currentlyProcessingNodes;
 
     int m_resolution;
 };
diff --git a/plugins/tools/basictools/strokes/move_stroke_strategy.cpp b/plugins/tools/basictools/strokes/move_stroke_strategy.cpp
index c61c73e..8eedf3e 100644
--- a/plugins/tools/basictools/strokes/move_stroke_strategy.cpp
+++ b/plugins/tools/basictools/strokes/move_stroke_strategy.cpp
@@ -23,13 +23,14 @@
 #include "kis_node.h"
 #include "commands_new/kis_update_command.h"
 #include "commands_new/kis_node_move_command2.h"
+#include "kis_layer_utils.h"
 
 
-MoveStrokeStrategy::MoveStrokeStrategy(KisNodeSP node,
+MoveStrokeStrategy::MoveStrokeStrategy(KisNodeList nodes,
                                        KisUpdatesFacade *updatesFacade,
                                        KisPostExecutionUndoAdapter *undoAdapter)
     : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Move"), false, undoAdapter),
-      m_node(node),
+      m_nodes(KisLayerUtils::sortAndFilterMergableInternalNodes(nodes, true)),
       m_updatesFacade(updatesFacade),
       m_undoEnabled(true),
       m_updatesEnabled(true)
@@ -39,21 +40,16 @@ MoveStrokeStrategy::MoveStrokeStrategy(KisNodeSP node,
 
 MoveStrokeStrategy::MoveStrokeStrategy(const MoveStrokeStrategy &rhs, bool suppressUndo)
     : KisStrokeStrategyUndoCommandBased(rhs, suppressUndo),
-      m_node(rhs.m_node),
+      m_nodes(rhs.m_nodes),
       m_updatesFacade(rhs.m_updatesFacade),
       m_finalOffset(rhs.m_finalOffset),
       m_dirtyRect(rhs.m_dirtyRect),
+      m_dirtyRects(rhs.m_dirtyRects),
       m_undoEnabled(rhs.m_undoEnabled),
       m_updatesEnabled(rhs.m_updatesEnabled)
 {
 }
 
-void MoveStrokeStrategy::setNode(KisNodeSP node)
-{
-    Q_ASSERT(!m_node);
-    m_node = node;
-}
-
 void MoveStrokeStrategy::saveInitialNodeOffsets(KisNodeSP node)
 {
     m_initialNodeOffsets.insert(node, QPoint(node->x(), node->y()));
@@ -67,8 +63,8 @@ void MoveStrokeStrategy::saveInitialNodeOffsets(KisNodeSP node)
 
 void MoveStrokeStrategy::initStrokeCallback()
 {
-    if (m_node) {
-        saveInitialNodeOffsets(m_node);
+    Q_FOREACH(KisNodeSP node, m_nodes) {
+        saveInitialNodeOffsets(node);
     }
 
     KisStrokeStrategyUndoCommandBased::initStrokeCallback();
@@ -76,19 +72,23 @@ void MoveStrokeStrategy::initStrokeCallback()
 
 void MoveStrokeStrategy::finishStrokeCallback()
 {
-    if(m_node && m_undoEnabled) {
-        KUndo2Command *updateCommand =
-            new KisUpdateCommand(m_node, m_dirtyRect, m_updatesFacade, true);
+    if (m_undoEnabled) {
+        Q_FOREACH (KisNodeSP node, m_nodes) {
+            KUndo2Command *updateCommand =
+                new KisUpdateCommand(node, m_dirtyRects[node], m_updatesFacade, true);
 
-        addMoveCommands(m_node, updateCommand);
+            addMoveCommands(node, updateCommand);
 
-        notifyCommandDone(KUndo2CommandSP(updateCommand),
-                          KisStrokeJobData::SEQUENTIAL,
-                          KisStrokeJobData::EXCLUSIVE);
+            notifyCommandDone(KUndo2CommandSP(updateCommand),
+                              KisStrokeJobData::SEQUENTIAL,
+                              KisStrokeJobData::EXCLUSIVE);
+        }
     }
 
-    if (m_node && !m_updatesEnabled) {
-        m_updatesFacade->refreshGraphAsync(m_node, m_dirtyRect);
+    if (!m_updatesEnabled) {
+        Q_FOREACH (KisNodeSP node, m_nodes) {
+            m_updatesFacade->refreshGraphAsync(node, m_dirtyRects[node]);
+        }
     }
 
     KisStrokeStrategyUndoCommandBased::finishStrokeCallback();
@@ -96,8 +96,7 @@ void MoveStrokeStrategy::finishStrokeCallback()
 
 void MoveStrokeStrategy::cancelStrokeCallback()
 {
-    if(m_node) {
-
+    if (!m_nodes.isEmpty()) {
         // FIXME: make cancel job exclusive instead
         m_updatesFacade->blockUpdates();
         moveAndUpdate(QPoint());
@@ -111,7 +110,7 @@ void MoveStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
 {
     Data *d = dynamic_cast<Data*>(data);
 
-    if(m_node && d) {
+    if(!m_nodes.isEmpty() && d) {
         moveAndUpdate(d->offset);
 
         /**
@@ -127,11 +126,13 @@ void MoveStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
 
 void MoveStrokeStrategy::moveAndUpdate(QPoint offset)
 {
-    QRect dirtyRect = moveNode(m_node, offset);
-    m_dirtyRect |= dirtyRect;
+    Q_FOREACH (KisNodeSP node, m_nodes) {
+        QRect dirtyRect = moveNode(node, offset);
+        m_dirtyRects[node] |= dirtyRect;
 
-    if (m_updatesEnabled) {
-        m_updatesFacade->refreshGraphAsync(m_node, dirtyRect);
+        if (m_updatesEnabled) {
+            m_updatesFacade->refreshGraphAsync(node, dirtyRect);
+        }
     }
 }
 
@@ -203,7 +204,9 @@ bool checkSupportsLodMoves(KisNodeSP node)
 
 KisStrokeStrategy* MoveStrokeStrategy::createLodClone(int levelOfDetail)
 {
-    if (!checkSupportsLodMoves(m_node)) return 0;
+    Q_FOREACH (KisNodeSP node, m_nodes) {
+        if (!checkSupportsLodMoves(node)) return 0;
+    }
 
     MoveStrokeStrategy *clone = new MoveStrokeStrategy(*this, levelOfDetail > 0);
     clone->setUndoEnabled(false);
diff --git a/plugins/tools/basictools/strokes/move_stroke_strategy.h b/plugins/tools/basictools/strokes/move_stroke_strategy.h
index 9eec619..bc1fb4f 100644
--- a/plugins/tools/basictools/strokes/move_stroke_strategy.h
+++ b/plugins/tools/basictools/strokes/move_stroke_strategy.h
@@ -57,19 +57,9 @@ public:
     };
 
 public:
-    MoveStrokeStrategy(KisNodeSP node, KisUpdatesFacade *updatesFacade,
+    MoveStrokeStrategy(KisNodeList nodes, KisUpdatesFacade *updatesFacade,
                        KisPostExecutionUndoAdapter *undoAdapter);
 
-    /**
-     * You can use deferred initialization of the node pointer
-     * To use it you need to pass 0 to the constructor, and
-     * set the node with setNode layer.
-     * NOTE: once set, you cannot change the node anymore,
-     *       you'll get an assert
-     */
-
-    void setNode(KisNodeSP node);
-
     void initStrokeCallback();
     void finishStrokeCallback();
     void cancelStrokeCallback();
@@ -88,10 +78,11 @@ private:
     void saveInitialNodeOffsets(KisNodeSP node);
 
 private:
-    KisNodeSP m_node;
+    KisNodeList m_nodes;
     KisUpdatesFacade *m_updatesFacade;
     QPoint m_finalOffset;
     QRect m_dirtyRect;
+    QHash<KisNodeSP, QRect> m_dirtyRects;
     bool m_undoEnabled;
     bool m_updatesEnabled;
     QHash<KisNodeSP, QPoint> m_initialNodeOffsets;
diff --git a/plugins/tools/basictools/tests/move_stroke_test.cpp b/plugins/tools/basictools/tests/move_stroke_test.cpp
index 54cc19e..754ed05 100644
--- a/plugins/tools/basictools/tests/move_stroke_test.cpp
+++ b/plugins/tools/basictools/tests/move_stroke_test.cpp
@@ -51,7 +51,7 @@ protected:
         Q_UNUSED(indirectPainting);
 
         KisNodeSP node = resources->currentNode();
-        return new MoveStrokeStrategy(node, image.data(), resources->postExecutionUndoAdapter());
+        return new MoveStrokeStrategy({node}, image.data(), resources->postExecutionUndoAdapter());
     }
 
     using utils::StrokeTester::addPaintingJobs;


More information about the kimageshop mailing list