[krita/rempt/T1004-recreate-the-text-tool] /: Implemented predefined transformations for shapes

Dmitry Kazakov null at kde.org
Tue Jun 20 10:14:42 UTC 2017


Git commit b59f3333d629293e5d13044c30862c229276f2f4 by Dmitry Kazakov.
Committed on 20/06/2017 at 10:14.
Pushed by dkazakov into branch 'rempt/T1004-recreate-the-text-tool'.

Implemented predefined transformations for shapes

This patch implements actions in the shape's context menu:

1) Rotate Shape +-90deg, 180deg
2) Mirror X,Y
3) Reset all applied transformations (was not
   available previously)

This patch also refactors KoSelection a bit. The idea
with tracking shape's matrices and check for their
consistency, in the end, was incorrect. The shapes change
their transformations one-by-one, not atomically. That
makes all the iterative calls checkMatricesConsistent()
return false.

Now the strategies that cause visible transformations
to the selection outline just include the selection
into the list of transformed shapes and transform it
manually.

CC:kimageshop at kde.org
Ref T1005

M  +62   -0    krita/data/actions/InteractionTool.action
M  +3    -54   libs/flake/KoSelection.cpp
M  +0    -6    libs/flake/KoSelection_p.h
M  +121  -5    plugins/tools/defaulttool/defaulttool/DefaultTool.cpp
M  +2    -0    plugins/tools/defaulttool/defaulttool/DefaultTool.h
M  +11   -5    plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
M  +1    -1    plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h
M  +12   -5    plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
M  +1    -1    plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h

https://commits.kde.org/krita/b59f3333d629293e5d13044c30862c229276f2f4

diff --git a/krita/data/actions/InteractionTool.action b/krita/data/actions/InteractionTool.action
index 0ca5cdf4a4a..575906569bf 100644
--- a/krita/data/actions/InteractionTool.action
+++ b/krita/data/actions/InteractionTool.action
@@ -196,5 +196,67 @@
       <statusTip></statusTip>
       <isCheckable>false</isCheckable>
     </Action>
+
+    <Action name="object_transform_rotate_90_cw">
+      <text>Rotate 90° CW</text>
+      <toolTip>Rotate object 90° clockwise</toolTip>
+      <shortcut></shortcut>
+      <icon>object-rotate-right</icon>
+      <whatsThis></whatsThis>
+      <statusTip></statusTip>
+      <isCheckable>false</isCheckable>
+    </Action>
+
+    <Action name="object_transform_rotate_90_ccw">
+      <text>Rotate 90° CCW</text>
+      <toolTip>Rotate object 90° counterclockwise</toolTip>
+      <shortcut></shortcut>
+      <icon>object-rotate-left</icon>
+      <whatsThis></whatsThis>
+      <statusTip></statusTip>
+      <isCheckable>false</isCheckable>
+    </Action>
+
+    <Action name="object_transform_rotate_180">
+      <text>Rotate 180° CCW</text>
+      <toolTip>Rotate object 180°</toolTip>
+      <shortcut></shortcut>
+      <icon></icon>
+      <whatsThis></whatsThis>
+      <statusTip></statusTip>
+      <isCheckable>false</isCheckable>
+    </Action>
+
+    <Action name="object_transform_mirror_horizontally">
+      <text>Mirror Horizontally</text>
+      <toolTip>Mirror object horizontally</toolTip>
+      <shortcut></shortcut>
+      <icon>symmetry-horizontal</icon>
+      <whatsThis></whatsThis>
+      <statusTip></statusTip>
+      <isCheckable>false</isCheckable>
+    </Action>
+
+    <Action name="object_transform_mirror_vertically">
+      <text>Mirror Vertically</text>
+      <toolTip>Mirror object vertically</toolTip>
+      <shortcut></shortcut>
+      <icon>symmetry-vertical</icon>
+      <whatsThis></whatsThis>
+      <statusTip></statusTip>
+      <isCheckable>false</isCheckable>
+    </Action>
+
+    <Action name="object_transform_reset">
+      <text>Reset Transformations</text>
+      <toolTip>Reset object transformations</toolTip>
+      <shortcut></shortcut>
+      <icon></icon>
+      <whatsThis></whatsThis>
+      <statusTip></statusTip>
+      <isCheckable>false</isCheckable>
+    </Action>
+
+
   </Actions>
 </ActionCollection>
diff --git a/libs/flake/KoSelection.cpp b/libs/flake/KoSelection.cpp
index 571acd6d41c..082556ad321 100644
--- a/libs/flake/KoSelection.cpp
+++ b/libs/flake/KoSelection.cpp
@@ -106,8 +106,6 @@ void KoSelection::select(KoShape *shape)
     d->selectedShapes << shape;
     shape->addShapeChangeListener(this);
 
-    d->savedMatrices = d->fetchShapesMatrices();
-
     if (d->selectedShapes.size() == 1) {
         setTransformation(shape->absoluteTransformation(0));
     } else {
@@ -125,7 +123,6 @@ void KoSelection::deselect(KoShape *shape)
 
     d->selectedShapes.removeAll(shape);
     shape->removeShapeChangeListener(this);
-    d->savedMatrices = d->fetchShapesMatrices();
 
     if (d->selectedShapes.size() == 1) {
         setTransformation(d->selectedShapes.first()->absoluteTransformation(0));
@@ -144,7 +141,6 @@ void KoSelection::deselectAll()
     Q_FOREACH (KoShape *shape, d->selectedShapes) {
         shape->removeShapeChangeListener(this);
     }
-    d->savedMatrices = d->fetchShapesMatrices();
 
     // reset the transformation matrix of the selection
     setTransformation(QTransform());
@@ -245,20 +241,10 @@ void KoSelection::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape)
 
     if (type == KoShape::Deleted) {
         deselect(shape);
-        // HACK ALERT: the caller will also remove the listener, so re-add it here
-        shape->addShapeChangeListener(this);
 
-    } else if (type >= KoShape::PositionChanged && type <= KoShape::GenericMatrixChange) {
-        QList<QTransform> matrices = d->fetchShapesMatrices();
-
-        QTransform newTransform;
-        if (d->checkMatricesConsistent(matrices, &newTransform)) {
-            d->savedMatrices = matrices;
-            setTransformation(newTransform);
-        } else {
-            d->savedMatrices = matrices;
-            setTransformation(QTransform());
-        }
+        // HACK ALERT: the caller will also remove the listener, which was
+        // removed in deselect(), so re-add it here
+        shape->addShapeChangeListener(this);
     }
 }
 
@@ -270,40 +256,3 @@ bool KoSelection::loadOdf(const KoXmlElement &, KoShapeLoadingContext &)
 {
     return true;
 }
-
-
-QList<QTransform> KoSelectionPrivate::fetchShapesMatrices() const
-{
-    QList<QTransform> result;
-    Q_FOREACH (KoShape *shape, selectedShapes) {
-        result << shape->absoluteTransformation(0);
-    }
-    return result;
-}
-
-bool KoSelectionPrivate::checkMatricesConsistent(const QList<QTransform> &matrices, QTransform *newTransform)
-{
-    Q_Q(KoSelection);
-
-    QTransform commonDiff;
-    bool haveCommonDiff = false;
-
-    KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(matrices.size() == selectedShapes.size(), false);
-
-    for (int i = 0; i < matrices.size(); i++) {
-        QTransform t = savedMatrices[i];
-        QTransform diff = t.inverted() * matrices[i];
-
-
-        if (haveCommonDiff) {
-            if (!KisAlgebra2D::fuzzyMatrixCompare(commonDiff, diff, 1e-5)) {
-                return false;
-            }
-        } else {
-            commonDiff = diff;
-        }
-    }
-
-    *newTransform = q->transformation() * commonDiff;
-    return true;
-}
diff --git a/libs/flake/KoSelection_p.h b/libs/flake/KoSelection_p.h
index 39c12de9f05..affef2b0523 100644
--- a/libs/flake/KoSelection_p.h
+++ b/libs/flake/KoSelection_p.h
@@ -38,12 +38,6 @@ public:
 
     KisSignalCompressor selectionChangedCompressor;
 
-
-    QList<QTransform> savedMatrices;
-
-    QList<QTransform> fetchShapesMatrices() const;
-    bool checkMatricesConsistent(const QList<QTransform> &matrices, QTransform *newTransform);
-
     Q_DECLARE_PUBLIC(KoSelection)
 };
 
diff --git a/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp b/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp
index b7cdcbef48b..7ff84602726 100644
--- a/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp
+++ b/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp
@@ -45,6 +45,7 @@
 #include <KoCanvasResourceManager.h>
 #include <KoShapeRubberSelectStrategy.h>
 #include <commands/KoShapeMoveCommand.h>
+#include <commands/KoShapeTransformCommand.h>
 #include <commands/KoShapeDeleteCommand.h>
 #include <commands/KoShapeCreateCommand.h>
 #include <commands/KoShapeGroupCommand.h>
@@ -84,6 +85,16 @@
 namespace {
 static const QString EditFillGradientFactoryId = "edit_fill_gradient";
 static const QString EditStrokeGradientFactoryId = "edit_stroke_gradient";
+
+enum TransformActionType {
+    TransformRotate90CW,
+    TransformRotate90CCW,
+    TransformRotate180,
+    TransformMirrorX,
+    TransformMirrorY,
+    TransformReset
+};
+
 }
 
 QPolygonF selectionPolygon(KoSelection *selection)
@@ -414,6 +425,16 @@ void DefaultTool::setupActions()
     addAction("object_ungroup", actionUngroupBottom);
     connect(actionUngroupBottom, SIGNAL(triggered()), this, SLOT(selectionUngroup()));
 
+    QSignalMapper *transformSignalsMapper = new QSignalMapper(this);
+    connect(transformSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionTransform(int)));
+
+    addMappedAction(transformSignalsMapper, "object_transform_rotate_90_cw", TransformRotate90CW);
+    addMappedAction(transformSignalsMapper, "object_transform_rotate_90_ccw", TransformRotate90CCW);
+    addMappedAction(transformSignalsMapper, "object_transform_rotate_180", TransformRotate180);
+    addMappedAction(transformSignalsMapper, "object_transform_mirror_horizontally", TransformMirrorX);
+    addMappedAction(transformSignalsMapper, "object_transform_mirror_vertically", TransformMirrorY);
+    addMappedAction(transformSignalsMapper, "object_transform_reset", TransformReset);
+
     m_contextMenu.reset(new QMenu());
 }
 
@@ -1013,6 +1034,84 @@ void DefaultTool::selectionUngroup()
     }
 }
 
+void DefaultTool::selectionTransform(int transformAction)
+{
+    KoSelection *selection = koSelection();
+    if (!selection) return;
+
+    QList<KoShape *> editableShapes = selection->selectedEditableShapes();
+    if (editableShapes.isEmpty()) {
+        return;
+    }
+
+    QTransform applyTransform;
+    bool shouldReset = false;
+    KUndo2MagicString actionName = kundo2_noi18n("BUG: No transform action");
+
+
+    switch (TransformActionType(transformAction)) {
+    case TransformRotate90CW:
+        applyTransform.rotate(90.0);
+        actionName = kundo2_i18n("Rotate Object 90° CW");
+        break;
+    case TransformRotate90CCW:
+        applyTransform.rotate(-90.0);
+        actionName = kundo2_i18n("Rotate Object 90° CCW");
+        break;
+    case TransformRotate180:
+        applyTransform.rotate(180.0);
+        actionName = kundo2_i18n("Rotate Object 180°");
+        break;
+    case TransformMirrorX:
+        applyTransform.scale(-1.0, 1.0);
+        actionName = kundo2_i18n("Mirror Object Horizontally");
+        break;
+    case TransformMirrorY:
+        applyTransform.scale(1.0, -1.0);
+        actionName = kundo2_i18n("Mirror Object Vertically");
+        break;
+    case TransformReset:
+        shouldReset = true;
+        actionName = kundo2_i18n("Reset Object Transformations");
+        break;
+    }
+
+    if (!shouldReset && applyTransform.isIdentity()) return;
+
+    QList<QTransform> oldTransforms;
+    QList<QTransform> newTransforms;
+
+    const QRectF outlineRect = KoShape::absoluteOutlineRect(editableShapes);
+    const QPointF centerPoint = outlineRect.center();
+    const QTransform centerTrans = QTransform::fromTranslate(centerPoint.x(), centerPoint.y());
+    const QTransform centerTransInv = QTransform::fromTranslate(-centerPoint.x(), -centerPoint.y());
+
+    // we also add selection to the list of trasformed shapes, so that its outline is updated correctly
+    QList<KoShape*> transformedShapes = editableShapes;
+    transformedShapes << selection;
+
+    Q_FOREACH (KoShape *shape, transformedShapes) {
+        oldTransforms.append(shape->transformation());
+
+        QTransform t;
+
+        if (!shouldReset) {
+            const QTransform world = shape->absoluteTransformation(0);
+            t =  world * centerTransInv * applyTransform * centerTrans * world.inverted() * shape->transformation();
+        } else {
+            const QPointF center = shape->outlineRect().center();
+            const QPointF offset = shape->transformation().map(center) - center;
+            t = QTransform::fromTranslate(offset.x(), offset.y());
+        }
+
+        newTransforms.append(t);
+    }
+
+    KoShapeTransformCommand *cmd = new KoShapeTransformCommand(transformedShapes, oldTransforms, newTransforms);
+    cmd->setText(actionName);
+    canvas()->addCommand(cmd);
+}
+
 void DefaultTool::selectionAlign(int _align)
 {
     KoShapeAlignCommand::Align align =
@@ -1263,12 +1362,19 @@ void DefaultTool::updateActions()
         editableShapes = koSelection()->selectedEditableShapes();
     }
 
-    const bool orderingEnabled = !editableShapes.isEmpty();
+    const bool hasEditableShapes = !editableShapes.isEmpty();
 
-    action("object_order_front")->setEnabled(orderingEnabled);
-    action("object_order_raise")->setEnabled(orderingEnabled);
-    action("object_order_lower")->setEnabled(orderingEnabled);
-    action("object_order_back")->setEnabled(orderingEnabled);
+    action("object_order_front")->setEnabled(hasEditableShapes);
+    action("object_order_raise")->setEnabled(hasEditableShapes);
+    action("object_order_lower")->setEnabled(hasEditableShapes);
+    action("object_order_back")->setEnabled(hasEditableShapes);
+
+    action("object_transform_rotate_90_cw")->setEnabled(hasEditableShapes);
+    action("object_transform_rotate_90_ccw")->setEnabled(hasEditableShapes);
+    action("object_transform_rotate_180")->setEnabled(hasEditableShapes);
+    action("object_transform_mirror_horizontally")->setEnabled(hasEditableShapes);
+    action("object_transform_mirror_vertically")->setEnabled(hasEditableShapes);
+    action("object_transform_reset")->setEnabled(hasEditableShapes);
 
     const bool alignmentEnabled =
        editableShapes.size() > 1 ||
@@ -1337,6 +1443,16 @@ QMenu* DefaultTool::popupActionsMenu()
             m_contextMenu->addAction(action("object_group"));
             m_contextMenu->addAction(action("object_ungroup"));
         }
+
+        QMenu *transform = m_contextMenu->addMenu(i18n("Transform"));
+        transform->addAction(action("object_transform_rotate_90_cw"));
+        transform->addAction(action("object_transform_rotate_90_ccw"));
+        transform->addAction(action("object_transform_rotate_180"));
+        transform->addSeparator();
+        transform->addAction(action("object_transform_mirror_horizontally"));
+        transform->addAction(action("object_transform_mirror_vertically"));
+        transform->addSeparator();
+        transform->addAction(action("object_transform_reset"));
     }
 
     return m_contextMenu.data();
diff --git a/plugins/tools/defaulttool/defaulttool/DefaultTool.h b/plugins/tools/defaulttool/defaulttool/DefaultTool.h
index 85d336cf3fd..4f41fb9ceae 100644
--- a/plugins/tools/defaulttool/defaulttool/DefaultTool.h
+++ b/plugins/tools/defaulttool/defaulttool/DefaultTool.h
@@ -106,6 +106,8 @@ private Q_SLOTS:
     void selectionGroup();
     void selectionUngroup();
 
+    void selectionTransform(int transformAction);
+
     void slotActivateEditFillGradient(bool value);
     void slotActivateEditStrokeGradient(bool value);
 
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
index a9df04da7b4..8fd959d3b81 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
+++ b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp
@@ -37,8 +37,14 @@ ShapeRotateStrategy::ShapeRotateStrategy(KoToolBase *tool, const QPointF &clicke
     : KoInteractionStrategy(tool)
     , m_start(clicked)
 {
-    m_selectedShapes = tool->canvas()->shapeManager()->selection()->selectedEditableShapes();
-    Q_FOREACH (KoShape *shape, m_selectedShapes) {
+    /**
+     * The outline of the selection should look as if it is also rotated, so we
+     * add it to the transformed shapes list.
+     */
+    m_transformedShapesAndSelection = tool->canvas()->shapeManager()->selection()->selectedEditableShapes();
+    m_transformedShapesAndSelection << tool->canvas()->shapeManager()->selection();
+
+    Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
         m_oldTransforms << shape->transformation();
     }
 
@@ -80,7 +86,7 @@ void ShapeRotateStrategy::rotateBy(qreal angle)
 
     QTransform applyMatrix = matrix * m_rotationMatrix.inverted();
     m_rotationMatrix = matrix;
-    Q_FOREACH (KoShape *shape, m_selectedShapes) {
+    Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
         const QRectF oldDirtyRect = shape->boundingRect();
         shape->applyAbsoluteTransformation(applyMatrix);
         shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
@@ -101,11 +107,11 @@ void ShapeRotateStrategy::paint(QPainter &painter, const KoViewConverter &conver
 KUndo2Command *ShapeRotateStrategy::createCommand()
 {
     QList<QTransform> newTransforms;
-    Q_FOREACH (KoShape *shape, m_selectedShapes) {
+    Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
         newTransforms << shape->transformation();
     }
 
-    KoShapeTransformCommand *cmd = new KoShapeTransformCommand(m_selectedShapes, m_oldTransforms, newTransforms);
+    KoShapeTransformCommand *cmd = new KoShapeTransformCommand(m_transformedShapesAndSelection, m_oldTransforms, newTransforms);
     cmd->setText(kundo2_i18n("Rotate"));
     return cmd;
 }
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h
index 6f20d3eef42..0ff63222d94 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h
+++ b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h
@@ -61,7 +61,7 @@ private:
     QTransform m_rotationMatrix;
     QList<QTransform> m_oldTransforms;
     QPointF m_rotationCenter;
-    QList<KoShape *> m_selectedShapes;
+    QList<KoShape *> m_transformedShapesAndSelection;
 };
 
 #endif
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
index 0739e84b1f6..0a6318a0b87 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
+++ b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp
@@ -42,8 +42,15 @@ ShapeShearStrategy::ShapeShearStrategy(KoToolBase *tool, const QPointF &clicked,
     , m_start(clicked)
 {
     KoSelection *sel = tool->canvas()->shapeManager()->selection();
-    m_selectedShapes = sel->selectedEditableShapes();
-    Q_FOREACH (KoShape *shape, m_selectedShapes) {
+
+    /**
+     * The outline of the selection should look as if it is also shear'ed, so we
+     * add it to the transformed shapes list.
+     */
+    m_transformedShapesAndSelection = sel->selectedEditableShapes();
+    m_transformedShapesAndSelection << sel;
+
+    Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
         m_oldTransforms << shape->transformation();
     }
 
@@ -145,7 +152,7 @@ void ShapeShearStrategy::handleMouseMove(const QPointF &point, Qt::KeyboardModif
 
     QTransform applyMatrix = matrix * m_shearMatrix.inverted();
 
-    Q_FOREACH (KoShape *shape, m_selectedShapes) {
+    Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
         const QRectF oldDirtyRect = shape->boundingRect();
         shape->applyAbsoluteTransformation(applyMatrix);
         shape->updateAbsolute(oldDirtyRect | shape->boundingRect());
@@ -162,10 +169,10 @@ void ShapeShearStrategy::paint(QPainter &painter, const KoViewConverter &convert
 KUndo2Command *ShapeShearStrategy::createCommand()
 {
     QList<QTransform> newTransforms;
-    Q_FOREACH (KoShape *shape, m_selectedShapes) {
+    Q_FOREACH (KoShape *shape, m_transformedShapesAndSelection) {
         newTransforms << shape->transformation();
     }
-    KoShapeTransformCommand *cmd = new KoShapeTransformCommand(m_selectedShapes, m_oldTransforms, newTransforms);
+    KoShapeTransformCommand *cmd = new KoShapeTransformCommand(m_transformedShapesAndSelection, m_oldTransforms, newTransforms);
     cmd->setText(kundo2_i18n("Shear"));
     return cmd;
 }
diff --git a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h
index 53667a13040..a24a145dfd9 100644
--- a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h
+++ b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h
@@ -64,7 +64,7 @@ private:
     QTransform m_shearMatrix;
     bool m_isMirrored;
     QList<QTransform> m_oldTransforms;
-    QList<KoShape *> m_selectedShapes;
+    QList<KoShape *> m_transformedShapesAndSelection;
 };
 
 #endif


More information about the kimageshop mailing list