diff --git a/krita/data/actions/InteractionTool.action b/krita/data/actions/InteractionTool.action index 0ca5cdf4a4..575906569b 100644 --- a/krita/data/actions/InteractionTool.action +++ b/krita/data/actions/InteractionTool.action @@ -1,200 +1,262 @@ Interaction Tool Raise Ctrl+] Raise object-order-raise-calligra false &Raise Align Right Align Right object-align-horizontal-right-calligra false Align Right Ungroup Ungroup object-ungroup-calligra false Ungroup Send to Back Ctrl+Shift+[ Send to Back object-order-back-calligra false Send to &Back Bring to Front Ctrl+Shift+] Bring to Front object-order-front-calligra false Bring to &Front Vertically Center Vertically Center object-align-vertical-center-calligra false Vertically Center Group Group object-group-calligra false Group Align Left Align Left object-align-horizontal-left-calligra false Align Left Align Top Align Top object-align-vertical-top-calligra false Align Top Horizontally Center Horizontally Center object-align-horizontal-center-calligra false Horizontally Center Lower Ctrl+[ Lower object-order-lower-calligra false &Lower Align Bottom Align Bottom object-align-vertical-bottom-calligra false Align Bottom Distribute Left Distribute left edges equidistantly distribute-horizontal-left false Distribute Centers Horizontally Distribute centers equidistantly horizontally distribute-horizontal-center false Distribute Right Distribute right edges equidistantly distribute-horizontal-right false Distribute Horizontal Gap Make horizontal gaps between objects equal distribute-horizontal false Distribute Top Distribute top edges equidistantly distribute-vertical-top false Distribute Centers Vertically Distribute centers equidistantly vertically distribute-vertical-center false Distribute Bottom Distribute bottom edges equidistantly distribute-vertical-bottom false Distribute Vertical Gap Make vertical gaps between objects equal distribute-vertical false + + + Rotate 90° CW + Rotate object 90° clockwise + + object-rotate-right + + + false + + + + Rotate 90° CCW + Rotate object 90° counterclockwise + + object-rotate-left + + + false + + + + Rotate 180° CCW + Rotate object 180° + + + + + false + + + + Mirror Horizontally + Mirror object horizontally + + symmetry-horizontal + + + false + + + + Mirror Vertically + Mirror object vertically + + symmetry-vertical + + + false + + + + Reset Transformations + Reset object transformations + + + + + false + + + diff --git a/libs/flake/KoSelection.cpp b/libs/flake/KoSelection.cpp index 571acd6d41..082556ad32 100644 --- a/libs/flake/KoSelection.cpp +++ b/libs/flake/KoSelection.cpp @@ -1,309 +1,258 @@ /* This file is part of the KDE project Copyright (C) 2006 Boudewijn Rempt Copyright (C) 2006 Thorsten Zachmann Copyright (C) 2006 Jan Hambrecht Copyright (C) 2006-2007,2009 Thomas Zander This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoSelection.h" #include "KoSelection_p.h" #include "KoShapeContainer.h" #include "KoShapeGroup.h" #include "KoPointerEvent.h" #include "KoShapePaintingContext.h" #include "kis_algebra_2d.h" #include "krita_container_utils.h" #include #include "kis_debug.h" KoSelection::KoSelection() : KoShape(new KoSelectionPrivate(this)) { Q_D(KoSelection); connect(&d->selectionChangedCompressor, SIGNAL(timeout()), SIGNAL(selectionChanged())); } KoSelection::~KoSelection() { } void KoSelection::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintcontext); } void KoSelection::setSize(const QSizeF &size) { Q_UNUSED(size); qWarning() << "WARNING: KoSelection::setSize() should never be used!"; } QSizeF KoSelection::size() const { return outlineRect().size(); } QRectF KoSelection::outlineRect() const { Q_D(const KoSelection); QPolygonF globalPolygon; Q_FOREACH (KoShape *shape, d->selectedShapes) { globalPolygon = globalPolygon.united( shape->absoluteTransformation(0).map(QPolygonF(shape->outlineRect()))); } const QPolygonF localPolygon = transformation().inverted().map(globalPolygon); return localPolygon.boundingRect(); } QRectF KoSelection::boundingRect() const { Q_D(const KoSelection); return KoShape::boundingRect(d->selectedShapes); } void KoSelection::select(KoShape *shape) { Q_D(KoSelection); KIS_SAFE_ASSERT_RECOVER_RETURN(shape != this); KIS_SAFE_ASSERT_RECOVER_RETURN(shape); if (!shape->isSelectable() || !shape->isVisible(true)) { return; } // check recursively if (isSelected(shape)) { return; } // find the topmost parent to select while (KoShapeGroup *parentGroup = dynamic_cast(shape->parent())) { shape = parentGroup; } d->selectedShapes << shape; shape->addShapeChangeListener(this); - d->savedMatrices = d->fetchShapesMatrices(); - if (d->selectedShapes.size() == 1) { setTransformation(shape->absoluteTransformation(0)); } else { setTransformation(QTransform()); } d->selectionChangedCompressor.start(); } void KoSelection::deselect(KoShape *shape) { Q_D(KoSelection); if (!d->selectedShapes.contains(shape)) return; d->selectedShapes.removeAll(shape); shape->removeShapeChangeListener(this); - d->savedMatrices = d->fetchShapesMatrices(); if (d->selectedShapes.size() == 1) { setTransformation(d->selectedShapes.first()->absoluteTransformation(0)); } d->selectionChangedCompressor.start(); } void KoSelection::deselectAll() { Q_D(KoSelection); if (d->selectedShapes.isEmpty()) return; Q_FOREACH (KoShape *shape, d->selectedShapes) { shape->removeShapeChangeListener(this); } - d->savedMatrices = d->fetchShapesMatrices(); // reset the transformation matrix of the selection setTransformation(QTransform()); d->selectedShapes.clear(); d->selectionChangedCompressor.start(); } int KoSelection::count() const { Q_D(const KoSelection); return d->selectedShapes.size(); } bool KoSelection::hitTest(const QPointF &position) const { Q_D(const KoSelection); Q_FOREACH (KoShape *shape, d->selectedShapes) { if (shape->hitTest(position)) return true; } return false; } const QList KoSelection::selectedShapes() const { Q_D(const KoSelection); return d->selectedShapes; } const QList KoSelection::selectedEditableShapes() const { Q_D(const KoSelection); QList shapes = selectedShapes(); KritaUtils::filterContainer (shapes, [](KoShape *shape) { return shape->isEditable(); }); return shapes; } const QList KoSelection::selectedEditableShapesAndDelegates() const { QList shapes; Q_FOREACH (KoShape *shape, selectedShapes()) { QSet delegates = shape->toolDelegates(); if (delegates.isEmpty()) { shapes.append(shape); } else { Q_FOREACH (KoShape *delegatedShape, delegates) { shapes.append(delegatedShape); } } } return shapes; } bool KoSelection::isSelected(const KoShape *shape) const { Q_D(const KoSelection); if (shape == this) return true; const KoShape *tmpShape = shape; while (tmpShape && std::find(d->selectedShapes.begin(), d->selectedShapes.end(), tmpShape) == d->selectedShapes.end()/*d->selectedShapes.contains(tmpShape)*/) { tmpShape = tmpShape->parent(); } return tmpShape; } KoShape *KoSelection::firstSelectedShape() const { Q_D(const KoSelection); return !d->selectedShapes.isEmpty() ? d->selectedShapes.first() : 0; } void KoSelection::setActiveLayer(KoShapeLayer *layer) { Q_D(KoSelection); d->activeLayer = layer; emit currentLayerChanged(layer); } KoShapeLayer* KoSelection::activeLayer() const { Q_D(const KoSelection); return d->activeLayer; } void KoSelection::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) { Q_UNUSED(shape); Q_D(KoSelection); 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 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); } } void KoSelection::saveOdf(KoShapeSavingContext &) const { } bool KoSelection::loadOdf(const KoXmlElement &, KoShapeLoadingContext &) { return true; } - - -QList KoSelectionPrivate::fetchShapesMatrices() const -{ - QList result; - Q_FOREACH (KoShape *shape, selectedShapes) { - result << shape->absoluteTransformation(0); - } - return result; -} - -bool KoSelectionPrivate::checkMatricesConsistent(const QList &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 39c12de9f0..affef2b052 100644 --- a/libs/flake/KoSelection_p.h +++ b/libs/flake/KoSelection_p.h @@ -1,50 +1,44 @@ /* This file is part of the KDE project * Copyright (C) 2009 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSELECTIONPRIVATE_H #define KOSELECTIONPRIVATE_H #include "KoShape_p.h" #include "kis_signal_compressor.h" class KoShapeGroup; class KoSelectionPrivate : public KoShapePrivate { public: explicit KoSelectionPrivate(KoSelection *parent) : KoShapePrivate(parent), activeLayer(0), selectionChangedCompressor(1, KisSignalCompressor::FIRST_INACTIVE) {} QList selectedShapes; KoShapeLayer *activeLayer; KisSignalCompressor selectionChangedCompressor; - - QList savedMatrices; - - QList fetchShapesMatrices() const; - bool checkMatricesConsistent(const QList &matrices, QTransform *newTransform); - Q_DECLARE_PUBLIC(KoSelection) }; #endif diff --git a/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp b/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp index b7cdcbef48..7ff8460272 100644 --- a/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp +++ b/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp @@ -1,1349 +1,1465 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2008-2009 Jan Hambrecht Copyright (C) 2008 C. Boemann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "DefaultTool.h" #include "DefaultToolGeometryWidget.h" #include "DefaultToolTabbedWidget.h" #include "SelectionDecorator.h" #include "ShapeMoveStrategy.h" #include "ShapeRotateStrategy.h" #include "ShapeShearStrategy.h" #include "ShapeResizeStrategy.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include "kis_action_registry.h" #include #include "kis_document_aware_spin_box_unit_manager.h" #include #include #include #include #include #include #include #include #include #include #include "kis_assert.h" #include "kis_global.h" #include "kis_debug.h" #include #define HANDLE_DISTANCE 10 #define HANDLE_DISTANCE_SQ (HANDLE_DISTANCE * HANDLE_DISTANCE) #define INNER_HANDLE_DISTANCE_SQ 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) { QPolygonF result; QList selectedShapes = selection->selectedShapes(); if (!selectedShapes.size()) { return result; } if (selectedShapes.size() > 1) { QTransform matrix = selection->absoluteTransformation(0); result = matrix.map(QPolygonF(QRectF(QPointF(0, 0), selection->size()))); } else { KoShape *selectedShape = selectedShapes.first(); QTransform matrix = selectedShape->absoluteTransformation(0); result = matrix.map(QPolygonF(QRectF(QPointF(0, 0), selectedShape->size()))); } return result; } class NopInteractionStrategy : public KoInteractionStrategy { public: explicit NopInteractionStrategy(KoToolBase *parent) : KoInteractionStrategy(parent) { } KUndo2Command *createCommand() override { return 0; } void handleMouseMove(const QPointF & /*mouseLocation*/, Qt::KeyboardModifiers /*modifiers*/) override {} void finishInteraction(Qt::KeyboardModifiers /*modifiers*/) override {} void paint(QPainter &painter, const KoViewConverter &converter) override { Q_UNUSED(painter); Q_UNUSED(converter); } }; class SelectionInteractionStrategy : public KoShapeRubberSelectStrategy { public: explicit SelectionInteractionStrategy(KoToolBase *parent, const QPointF &clicked, bool useSnapToGrid) : KoShapeRubberSelectStrategy(parent, clicked, useSnapToGrid) { } void paint(QPainter &painter, const KoViewConverter &converter) override { KoShapeRubberSelectStrategy::paint(painter, converter); } }; #include #include "KoShapeGradientHandles.h" #include "ShapeGradientEditStrategy.h" class DefaultTool::MoveGradientHandleInteractionFactory : public KoInteractionStrategyFactory { public: MoveGradientHandleInteractionFactory(KoFlake::FillVariant fillVariant, int priority, const QString &id, DefaultTool *_q) : KoInteractionStrategyFactory(priority, id), q(_q), m_fillVariant(fillVariant) { } KoInteractionStrategy* createStrategy(KoPointerEvent *ev) override { m_currentHandle = handleAt(ev->point); if (m_currentHandle.type != KoShapeGradientHandles::Handle::None) { KoShape *shape = onlyEditableShape(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape, 0); return new ShapeGradientEditStrategy(q, m_fillVariant, shape, m_currentHandle.type, ev->point); } return 0; } bool hoverEvent(KoPointerEvent *ev) override { m_currentHandle = handleAt(ev->point); return false; } bool paintOnHover(QPainter &painter, const KoViewConverter &converter) override { Q_UNUSED(painter); Q_UNUSED(converter); return false; } bool tryUseCustomCursor() override { if (m_currentHandle.type != KoShapeGradientHandles::Handle::None) { q->useCursor(Qt::OpenHandCursor); } return m_currentHandle.type != KoShapeGradientHandles::Handle::None; } private: KoShape* onlyEditableShape() const { KoSelection *selection = q->koSelection(); QList shapes = selection->selectedEditableShapes(); KoShape *shape = 0; if (shapes.size() == 1) { shape = shapes.first(); } return shape; } KoShapeGradientHandles::Handle handleAt(const QPointF &pos) { KoShapeGradientHandles::Handle result; KoShape *shape = onlyEditableShape(); if (shape) { KoFlake::SelectionHandle globalHandle = q->handleAt(pos); const qreal distanceThresholdSq = globalHandle == KoFlake::NoHandle ? HANDLE_DISTANCE_SQ : 0.25 * HANDLE_DISTANCE_SQ; const KoViewConverter *converter = q->canvas()->viewConverter(); const QPointF viewPoint = converter->documentToView(pos); qreal minDistanceSq = std::numeric_limits::max(); KoShapeGradientHandles sh(m_fillVariant, shape); Q_FOREACH (const KoShapeGradientHandles::Handle &handle, sh.handles()) { const QPointF handlePoint = converter->documentToView(handle.pos); const qreal distanceSq = kisSquareDistance(viewPoint, handlePoint); if (distanceSq < distanceThresholdSq && distanceSq < minDistanceSq) { result = handle; minDistanceSq = distanceSq; } } } return result; } private: DefaultTool *q; KoFlake::FillVariant m_fillVariant; KoShapeGradientHandles::Handle m_currentHandle; }; class SelectionHandler : public KoToolSelection { public: SelectionHandler(DefaultTool *parent) : KoToolSelection(parent) , m_selection(parent->koSelection()) { } bool hasSelection() override { if (m_selection) { return m_selection->count(); } return false; } private: QPointer m_selection; }; DefaultTool::DefaultTool(KoCanvasBase *canvas) : KoInteractionTool(canvas) , m_lastHandle(KoFlake::NoHandle) , m_hotPosition(KoFlake::TopLeft) , m_mouseWasInsideHandles(false) , m_selectionHandler(new SelectionHandler(this)) , m_customEventStrategy(0) , m_tabbedOptionWidget(0) { setupActions(); QPixmap rotatePixmap, shearPixmap; rotatePixmap.load(":/cursor_rotate.png"); Q_ASSERT(!rotatePixmap.isNull()); shearPixmap.load(":/cursor_shear.png"); Q_ASSERT(!shearPixmap.isNull()); m_rotateCursors[0] = QCursor(rotatePixmap.transformed(QTransform().rotate(45))); m_rotateCursors[1] = QCursor(rotatePixmap.transformed(QTransform().rotate(90))); m_rotateCursors[2] = QCursor(rotatePixmap.transformed(QTransform().rotate(135))); m_rotateCursors[3] = QCursor(rotatePixmap.transformed(QTransform().rotate(180))); m_rotateCursors[4] = QCursor(rotatePixmap.transformed(QTransform().rotate(225))); m_rotateCursors[5] = QCursor(rotatePixmap.transformed(QTransform().rotate(270))); m_rotateCursors[6] = QCursor(rotatePixmap.transformed(QTransform().rotate(315))); m_rotateCursors[7] = QCursor(rotatePixmap); /* m_rotateCursors[0] = QCursor(Qt::RotateNCursor); m_rotateCursors[1] = QCursor(Qt::RotateNECursor); m_rotateCursors[2] = QCursor(Qt::RotateECursor); m_rotateCursors[3] = QCursor(Qt::RotateSECursor); m_rotateCursors[4] = QCursor(Qt::RotateSCursor); m_rotateCursors[5] = QCursor(Qt::RotateSWCursor); m_rotateCursors[6] = QCursor(Qt::RotateWCursor); m_rotateCursors[7] = QCursor(Qt::RotateNWCursor); */ m_shearCursors[0] = QCursor(shearPixmap); m_shearCursors[1] = QCursor(shearPixmap.transformed(QTransform().rotate(45))); m_shearCursors[2] = QCursor(shearPixmap.transformed(QTransform().rotate(90))); m_shearCursors[3] = QCursor(shearPixmap.transformed(QTransform().rotate(135))); m_shearCursors[4] = QCursor(shearPixmap.transformed(QTransform().rotate(180))); m_shearCursors[5] = QCursor(shearPixmap.transformed(QTransform().rotate(225))); m_shearCursors[6] = QCursor(shearPixmap.transformed(QTransform().rotate(270))); m_shearCursors[7] = QCursor(shearPixmap.transformed(QTransform().rotate(315))); m_sizeCursors[0] = Qt::SizeVerCursor; m_sizeCursors[1] = Qt::SizeBDiagCursor; m_sizeCursors[2] = Qt::SizeHorCursor; m_sizeCursors[3] = Qt::SizeFDiagCursor; m_sizeCursors[4] = Qt::SizeVerCursor; m_sizeCursors[5] = Qt::SizeBDiagCursor; m_sizeCursors[6] = Qt::SizeHorCursor; m_sizeCursors[7] = Qt::SizeFDiagCursor; connect(canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(updateActions())); } DefaultTool::~DefaultTool() { } void DefaultTool::slotActivateEditFillGradient(bool value) { if (value) { addInteractionFactory( new MoveGradientHandleInteractionFactory(KoFlake::Fill, 1, EditFillGradientFactoryId, this)); } else { removeInteractionFactory(EditFillGradientFactoryId); } repaintDecorations(); } void DefaultTool::slotActivateEditStrokeGradient(bool value) { if (value) { addInteractionFactory( new MoveGradientHandleInteractionFactory(KoFlake::StrokeFill, 0, EditStrokeGradientFactoryId, this)); } else { removeInteractionFactory(EditStrokeGradientFactoryId); } repaintDecorations(); } bool DefaultTool::wantsAutoScroll() const { return true; } void DefaultTool::addMappedAction(QSignalMapper *mapper, const QString &actionId, int commandType) { KisActionRegistry *actionRegistry = KisActionRegistry::instance(); QAction *action = actionRegistry->makeQAction(actionId, this); addAction(actionId, action); connect(action, SIGNAL(triggered()), mapper, SLOT(map())); mapper->setMapping(action, commandType); } void DefaultTool::setupActions() { KisActionRegistry *actionRegistry = KisActionRegistry::instance(); QAction *actionBringToFront = actionRegistry->makeQAction("object_order_front", this); addAction("object_order_front", actionBringToFront); connect(actionBringToFront, SIGNAL(triggered()), this, SLOT(selectionBringToFront())); QAction *actionRaise = actionRegistry->makeQAction("object_order_raise", this); addAction("object_order_raise", actionRaise); connect(actionRaise, SIGNAL(triggered()), this, SLOT(selectionMoveUp())); QAction *actionLower = actionRegistry->makeQAction("object_order_lower", this); addAction("object_order_lower", actionLower); connect(actionLower, SIGNAL(triggered()), this, SLOT(selectionMoveDown())); QAction *actionSendToBack = actionRegistry->makeQAction("object_order_back", this); addAction("object_order_back", actionSendToBack); connect(actionSendToBack, SIGNAL(triggered()), this, SLOT(selectionSendToBack())); QSignalMapper *alignSignalsMapper = new QSignalMapper(this); connect(alignSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionAlign(int))); addMappedAction(alignSignalsMapper, "object_align_horizontal_left", KoShapeAlignCommand::HorizontalLeftAlignment); addMappedAction(alignSignalsMapper, "object_align_horizontal_center", KoShapeAlignCommand::HorizontalCenterAlignment); addMappedAction(alignSignalsMapper, "object_align_horizontal_right", KoShapeAlignCommand::HorizontalRightAlignment); addMappedAction(alignSignalsMapper, "object_align_vertical_top", KoShapeAlignCommand::VerticalTopAlignment); addMappedAction(alignSignalsMapper, "object_align_vertical_center", KoShapeAlignCommand::VerticalCenterAlignment); addMappedAction(alignSignalsMapper, "object_align_vertical_bottom", KoShapeAlignCommand::VerticalBottomAlignment); QSignalMapper *distributeSignalsMapper = new QSignalMapper(this); connect(distributeSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionDistribute(int))); addMappedAction(distributeSignalsMapper, "object_distribute_horizontal_left", KoShapeDistributeCommand::HorizontalLeftDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_horizontal_center", KoShapeDistributeCommand::HorizontalCenterDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_horizontal_right", KoShapeDistributeCommand::HorizontalRightDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_horizontal_gaps", KoShapeDistributeCommand::HorizontalGapsDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_vertical_top", KoShapeDistributeCommand::VerticalTopDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_vertical_center", KoShapeDistributeCommand::VerticalCenterDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_vertical_bottom", KoShapeDistributeCommand::VerticalBottomDistribution); addMappedAction(distributeSignalsMapper, "object_distribute_vertical_gaps", KoShapeDistributeCommand::VerticalGapsDistribution); QAction *actionGroupBottom = actionRegistry->makeQAction("object_group", this); addAction("object_group", actionGroupBottom); connect(actionGroupBottom, SIGNAL(triggered()), this, SLOT(selectionGroup())); QAction *actionUngroupBottom = actionRegistry->makeQAction("object_ungroup", this); 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()); } qreal DefaultTool::rotationOfHandle(KoFlake::SelectionHandle handle, bool useEdgeRotation) { QPointF selectionCenter = koSelection()->absolutePosition(); QPointF direction; switch (handle) { case KoFlake::TopMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::TopRight) - koSelection()->absolutePosition(KoFlake::TopLeft); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::TopLeft); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::TopRight) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::TopRightHandle: direction = (QVector2D(koSelection()->absolutePosition(KoFlake::TopRight) - koSelection()->absolutePosition(KoFlake::TopLeft)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::TopRight) - koSelection()->absolutePosition(KoFlake::BottomRight)).normalized()).toPointF(); break; case KoFlake::RightMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::BottomRight) - koSelection()->absolutePosition(KoFlake::TopRight); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::TopRight); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::BottomRight) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::BottomRightHandle: direction = (QVector2D(koSelection()->absolutePosition(KoFlake::BottomRight) - koSelection()->absolutePosition(KoFlake::BottomLeft)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::BottomRight) - koSelection()->absolutePosition(KoFlake::TopRight)).normalized()).toPointF(); break; case KoFlake::BottomMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::BottomLeft) - koSelection()->absolutePosition(KoFlake::BottomRight); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::BottomLeft); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::BottomRight) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::BottomLeftHandle: direction = koSelection()->absolutePosition(KoFlake::BottomLeft) - selectionCenter; direction = (QVector2D(koSelection()->absolutePosition(KoFlake::BottomLeft) - koSelection()->absolutePosition(KoFlake::BottomRight)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::BottomLeft) - koSelection()->absolutePosition(KoFlake::TopLeft)).normalized()).toPointF(); break; case KoFlake::LeftMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::TopLeft) - koSelection()->absolutePosition(KoFlake::BottomLeft); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::TopLeft); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::BottomLeft) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::TopLeftHandle: direction = koSelection()->absolutePosition(KoFlake::TopLeft) - selectionCenter; direction = (QVector2D(koSelection()->absolutePosition(KoFlake::TopLeft) - koSelection()->absolutePosition(KoFlake::TopRight)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::TopLeft) - koSelection()->absolutePosition(KoFlake::BottomLeft)).normalized()).toPointF(); break; case KoFlake::NoHandle: return 0.0; break; } qreal rotation = atan2(direction.y(), direction.x()) * 180.0 / M_PI; switch (handle) { case KoFlake::TopMiddleHandle: if (useEdgeRotation) { rotation -= 0.0; } else { rotation -= 270.0; } break; case KoFlake::TopRightHandle: rotation -= 315.0; break; case KoFlake::RightMiddleHandle: if (useEdgeRotation) { rotation -= 90.0; } else { rotation -= 0.0; } break; case KoFlake::BottomRightHandle: rotation -= 45.0; break; case KoFlake::BottomMiddleHandle: if (useEdgeRotation) { rotation -= 180.0; } else { rotation -= 90.0; } break; case KoFlake::BottomLeftHandle: rotation -= 135.0; break; case KoFlake::LeftMiddleHandle: if (useEdgeRotation) { rotation -= 270.0; } else { rotation -= 180.0; } break; case KoFlake::TopLeftHandle: rotation -= 225.0; break; case KoFlake::NoHandle: break; } if (rotation < 0.0) { rotation += 360.0; } return rotation; } void DefaultTool::updateCursor() { if (tryUseCustomCursor()) return; QCursor cursor = Qt::ArrowCursor; QString statusText; if (koSelection()->count() > 0) { // has a selection bool editable = !koSelection()->selectedEditableShapes().isEmpty(); if (!m_mouseWasInsideHandles) { m_angle = rotationOfHandle(m_lastHandle, true); int rotOctant = 8 + int(8.5 + m_angle / 45); bool rotateHandle = false; bool shearHandle = false; switch (m_lastHandle) { case KoFlake::TopMiddleHandle: cursor = m_shearCursors[(0 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::TopRightHandle: cursor = m_rotateCursors[(1 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::RightMiddleHandle: cursor = m_shearCursors[(2 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::BottomRightHandle: cursor = m_rotateCursors[(3 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::BottomMiddleHandle: cursor = m_shearCursors[(4 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::BottomLeftHandle: cursor = m_rotateCursors[(5 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::LeftMiddleHandle: cursor = m_shearCursors[(6 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::TopLeftHandle: cursor = m_rotateCursors[(7 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::NoHandle: cursor = Qt::ArrowCursor; break; } if (rotateHandle) { statusText = i18n("Left click rotates around center, right click around highlighted position."); } if (shearHandle) { statusText = i18n("Click and drag to shear selection."); } } else { statusText = i18n("Click and drag to resize selection."); m_angle = rotationOfHandle(m_lastHandle, false); int rotOctant = 8 + int(8.5 + m_angle / 45); bool cornerHandle = false; switch (m_lastHandle) { case KoFlake::TopMiddleHandle: cursor = m_sizeCursors[(0 + rotOctant) % 8]; break; case KoFlake::TopRightHandle: cursor = m_sizeCursors[(1 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::RightMiddleHandle: cursor = m_sizeCursors[(2 + rotOctant) % 8]; break; case KoFlake::BottomRightHandle: cursor = m_sizeCursors[(3 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::BottomMiddleHandle: cursor = m_sizeCursors[(4 + rotOctant) % 8]; break; case KoFlake::BottomLeftHandle: cursor = m_sizeCursors[(5 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::LeftMiddleHandle: cursor = m_sizeCursors[(6 + rotOctant) % 8]; break; case KoFlake::TopLeftHandle: cursor = m_sizeCursors[(7 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::NoHandle: cursor = Qt::SizeAllCursor; statusText = i18n("Click and drag to move selection."); break; } if (cornerHandle) { statusText = i18n("Click and drag to resize selection. Middle click to set highlighted position."); } } if (!editable) { cursor = Qt::ArrowCursor; } } else { // there used to be guides... :'''( } useCursor(cursor); if (currentStrategy() == 0) { emit statusTextChanged(statusText); } } void DefaultTool::paint(QPainter &painter, const KoViewConverter &converter) { SelectionDecorator decorator(canvas()->resourceManager()); decorator.setSelection(koSelection()); decorator.setHandleRadius(handleRadius()); decorator.setShowFillGradientHandles(hasInteractioFactory(EditFillGradientFactoryId)); decorator.setShowStrokeFillGradientHandles(hasInteractioFactory(EditStrokeGradientFactoryId)); decorator.paint(painter, converter); KoInteractionTool::paint(painter, converter); painter.save(); KoShape::applyConversion(painter, converter); canvas()->snapGuide()->paint(painter, converter); painter.restore(); } void DefaultTool::mousePressEvent(KoPointerEvent *event) { KoInteractionTool::mousePressEvent(event); updateCursor(); } void DefaultTool::mouseMoveEvent(KoPointerEvent *event) { KoInteractionTool::mouseMoveEvent(event); if (currentStrategy() == 0 && koSelection() && koSelection()->count() > 0) { QRectF bound = handlesSize(); if (bound.contains(event->point)) { bool inside; KoFlake::SelectionHandle newDirection = handleAt(event->point, &inside); if (inside != m_mouseWasInsideHandles || m_lastHandle != newDirection) { m_lastHandle = newDirection; m_mouseWasInsideHandles = inside; //repaintDecorations(); } } else { /*if (m_lastHandle != KoFlake::NoHandle) repaintDecorations(); */ m_lastHandle = KoFlake::NoHandle; m_mouseWasInsideHandles = false; // there used to be guides... :'''( } } else { // there used to be guides... :'''( } updateCursor(); } QRectF DefaultTool::handlesSize() { KoSelection *selection = koSelection(); if (!selection->count()) return QRectF(); recalcSelectionBox(selection); QRectF bound = m_selectionOutline.boundingRect(); // expansion Border if (!canvas() || !canvas()->viewConverter()) { return bound; } QPointF border = canvas()->viewConverter()->viewToDocument(QPointF(HANDLE_DISTANCE, HANDLE_DISTANCE)); bound.adjust(-border.x(), -border.y(), border.x(), border.y()); return bound; } void DefaultTool::mouseReleaseEvent(KoPointerEvent *event) { KoInteractionTool::mouseReleaseEvent(event); updateCursor(); } void DefaultTool::mouseDoubleClickEvent(KoPointerEvent *event) { KoSelection *selection = canvas()->selectedShapesProxy()->selection(); KoShape *shape = canvas()->shapeManager()->shapeAt(event->point, KoFlake::ShapeOnTop); if (shape && !selection->isSelected(shape)) { if (!(event->modifiers() & Qt::ShiftModifier)) { selection->deselectAll(); } selection->select(shape); } explicitUserStrokeEndRequest(); } bool DefaultTool::moveSelection(int direction, Qt::KeyboardModifiers modifiers) { bool result = false; qreal x = 0.0, y = 0.0; if (direction == Qt::Key_Left) { x = -5; } else if (direction == Qt::Key_Right) { x = 5; } else if (direction == Qt::Key_Up) { y = -5; } else if (direction == Qt::Key_Down) { y = 5; } if (x != 0.0 || y != 0.0) { // actually move if ((modifiers & Qt::ShiftModifier) != 0) { x *= 10; y *= 10; } else if ((modifiers & Qt::AltModifier) != 0) { // more precise x /= 5; y /= 5; } QList shapes = koSelection()->selectedEditableShapes(); if (!shapes.isEmpty()) { canvas()->addCommand(new KoShapeMoveCommand(shapes, QPointF(x, y))); result = true; } } return result; } void DefaultTool::keyPressEvent(QKeyEvent *event) { KoInteractionTool::keyPressEvent(event); if (currentStrategy() == 0) { switch (event->key()) { case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Up: case Qt::Key_Down: if (moveSelection(event->key(), event->modifiers())) { event->accept(); } break; case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: canvas()->resourceManager()->setResource(HotPosition, event->key() - Qt::Key_1); event->accept(); break; default: return; } } } void DefaultTool::repaintDecorations() { if (koSelection() && koSelection()->count() > 0) { canvas()->updateCanvas(handlesSize()); } } void DefaultTool::copy() const { // all the selected shapes, not only editable! QList shapes = canvas()->selectedShapesProxy()->selection()->selectedShapes(); if (!shapes.isEmpty()) { KoDrag drag; drag.setSvg(shapes); drag.addToClipboard(); } } void DefaultTool::deleteSelection() { QList shapes; foreach (KoShape *s, canvas()->selectedShapesProxy()->selection()->selectedShapes()) { if (s->isGeometryProtected()) { continue; } shapes << s; } if (!shapes.empty()) { canvas()->addCommand(canvas()->shapeController()->removeShapes(shapes)); } } bool DefaultTool::paste() { // we no longer have to do anything as tool Proxy will do it for us return false; } KoSelection *DefaultTool::koSelection() { Q_ASSERT(canvas()); Q_ASSERT(canvas()->selectedShapesProxy()); return canvas()->selectedShapesProxy()->selection(); } KoFlake::SelectionHandle DefaultTool::handleAt(const QPointF &point, bool *innerHandleMeaning) { // check for handles in this order; meaning that when handles overlap the one on top is chosen static const KoFlake::SelectionHandle handleOrder[] = { KoFlake::BottomRightHandle, KoFlake::TopLeftHandle, KoFlake::BottomLeftHandle, KoFlake::TopRightHandle, KoFlake::BottomMiddleHandle, KoFlake::RightMiddleHandle, KoFlake::LeftMiddleHandle, KoFlake::TopMiddleHandle, KoFlake::NoHandle }; const KoViewConverter *converter = canvas()->viewConverter(); KoSelection *selection = koSelection(); if (!selection->count() || !converter) { return KoFlake::NoHandle; } recalcSelectionBox(selection); if (innerHandleMeaning) { QPainterPath path; path.addPolygon(m_selectionOutline); *innerHandleMeaning = path.contains(point) || path.intersects(handlePaintRect(point)); } const QPointF viewPoint = converter->documentToView(point); for (int i = 0; i < KoFlake::NoHandle; ++i) { KoFlake::SelectionHandle handle = handleOrder[i]; const QPointF handlePoint = converter->documentToView(m_selectionBox[handle]); const qreal distanceSq = kisSquareDistance(viewPoint, handlePoint); // if just inside the outline if (distanceSq < HANDLE_DISTANCE_SQ) { if (innerHandleMeaning) { if (distanceSq < INNER_HANDLE_DISTANCE_SQ) { *innerHandleMeaning = true; } } return handle; } } return KoFlake::NoHandle; } void DefaultTool::recalcSelectionBox(KoSelection *selection) { KIS_ASSERT_RECOVER_RETURN(selection->count()); QTransform matrix = selection->absoluteTransformation(0); m_selectionOutline = matrix.map(QPolygonF(selection->outlineRect())); m_angle = 0.0; QPolygonF outline = m_selectionOutline; //shorter name in the following :) m_selectionBox[KoFlake::TopMiddleHandle] = (outline.value(0) + outline.value(1)) / 2; m_selectionBox[KoFlake::TopRightHandle] = outline.value(1); m_selectionBox[KoFlake::RightMiddleHandle] = (outline.value(1) + outline.value(2)) / 2; m_selectionBox[KoFlake::BottomRightHandle] = outline.value(2); m_selectionBox[KoFlake::BottomMiddleHandle] = (outline.value(2) + outline.value(3)) / 2; m_selectionBox[KoFlake::BottomLeftHandle] = outline.value(3); m_selectionBox[KoFlake::LeftMiddleHandle] = (outline.value(3) + outline.value(0)) / 2; m_selectionBox[KoFlake::TopLeftHandle] = outline.value(0); if (selection->count() == 1) { #if 0 // TODO detect mirroring KoShape *s = koSelection()->firstSelectedShape(); if (s->scaleX() < 0) { // vertically mirrored: swap left / right qSwap(m_selectionBox[KoFlake::TopLeftHandle], m_selectionBox[KoFlake::TopRightHandle]); qSwap(m_selectionBox[KoFlake::LeftMiddleHandle], m_selectionBox[KoFlake::RightMiddleHandle]); qSwap(m_selectionBox[KoFlake::BottomLeftHandle], m_selectionBox[KoFlake::BottomRightHandle]); } if (s->scaleY() < 0) { // vertically mirrored: swap top / bottom qSwap(m_selectionBox[KoFlake::TopLeftHandle], m_selectionBox[KoFlake::BottomLeftHandle]); qSwap(m_selectionBox[KoFlake::TopMiddleHandle], m_selectionBox[KoFlake::BottomMiddleHandle]); qSwap(m_selectionBox[KoFlake::TopRightHandle], m_selectionBox[KoFlake::BottomRightHandle]); } #endif } } void DefaultTool::activate(ToolActivation activation, const QSet &shapes) { KoToolBase::activate(activation, shapes); m_mouseWasInsideHandles = false; m_lastHandle = KoFlake::NoHandle; useCursor(Qt::ArrowCursor); repaintDecorations(); updateActions(); if (m_tabbedOptionWidget) { m_tabbedOptionWidget->activate(); } } void DefaultTool::deactivate() { KoToolBase::deactivate(); if (m_tabbedOptionWidget) { m_tabbedOptionWidget->deactivate(); } } void DefaultTool::selectionGroup() { KoSelection *selection = koSelection(); if (!selection) return; QList selectedShapes = selection->selectedEditableShapes(); qSort(selectedShapes.begin(), selectedShapes.end(), KoShape::compareShapeZIndex); KoShapeGroup *group = new KoShapeGroup(); // TODO what if only one shape is left? KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Group shapes")); canvas()->shapeController()->addShapeDirect(group, cmd); new KoShapeGroupCommand(group, selectedShapes, false, true, true, cmd); canvas()->addCommand(cmd); // update selection so we can ungroup immediately again selection->deselectAll(); selection->select(group); } void DefaultTool::selectionUngroup() { KoSelection *selection = koSelection(); if (!selection) return; QList selectedShapes = selection->selectedEditableShapes(); qSort(selectedShapes.begin(), selectedShapes.end(), KoShape::compareShapeZIndex); KUndo2Command *cmd = 0; // add a ungroup command for each found shape container to the macro command Q_FOREACH (KoShape *shape, selectedShapes) { KoShapeGroup *group = dynamic_cast(shape); if (group) { cmd = cmd ? cmd : new KUndo2Command(kundo2_i18n("Ungroup shapes")); new KoShapeUngroupCommand(group, group->shapes(), group->parent() ? QList() : canvas()->shapeManager()->topLevelShapes(), cmd); canvas()->shapeController()->removeShape(group, cmd); } } if (cmd) { canvas()->addCommand(cmd); } } +void DefaultTool::selectionTransform(int transformAction) +{ + KoSelection *selection = koSelection(); + if (!selection) return; + + QList 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 oldTransforms; + QList 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 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 = static_cast(_align); KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.isEmpty()) { return; } // TODO add an option to the widget so that one can align to the page // with multiple selected shapes too QRectF bb; // single selected shape is automatically aligned to document rect if (editableShapes.count() == 1) { if (!canvas()->resourceManager()->hasResource(KoCanvasResourceManager::PageSize)) { return; } bb = QRectF(QPointF(0, 0), canvas()->resourceManager()->sizeResource(KoCanvasResourceManager::PageSize)); } else { bb = KoShape::absoluteOutlineRect(editableShapes); } KoShapeAlignCommand *cmd = new KoShapeAlignCommand(editableShapes, align, bb); canvas()->addCommand(cmd); } void DefaultTool::selectionDistribute(int _distribute) { KoShapeDistributeCommand::Distribute distribute = static_cast(_distribute); KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.size() < 3) { return; } QRectF bb = KoShape::absoluteOutlineRect(editableShapes); KoShapeDistributeCommand *cmd = new KoShapeDistributeCommand(editableShapes, distribute, bb); canvas()->addCommand(cmd); } void DefaultTool::selectionBringToFront() { selectionReorder(KoShapeReorderCommand::BringToFront); } void DefaultTool::selectionMoveUp() { selectionReorder(KoShapeReorderCommand::RaiseShape); } void DefaultTool::selectionMoveDown() { selectionReorder(KoShapeReorderCommand::LowerShape); } void DefaultTool::selectionSendToBack() { selectionReorder(KoShapeReorderCommand::SendToBack); } void DefaultTool::selectionReorder(KoShapeReorderCommand::MoveShapeType order) { KoSelection *selection = canvas()->selectedShapesProxy()->selection(); if (!selection) { return; } QList selectedShapes = selection->selectedEditableShapes(); if (selectedShapes.isEmpty()) { return; } KUndo2Command *cmd = KoShapeReorderCommand::createCommand(selectedShapes, canvas()->shapeManager(), order); if (cmd) { canvas()->addCommand(cmd); } } QList > DefaultTool::createOptionWidgets() { QList > widgets; m_tabbedOptionWidget = new DefaultToolTabbedWidget(this); if (isActivated()) { m_tabbedOptionWidget->activate(); } widgets.append(m_tabbedOptionWidget); connect(m_tabbedOptionWidget, SIGNAL(sigSwitchModeEditFillGradient(bool)), SLOT(slotActivateEditFillGradient(bool))); connect(m_tabbedOptionWidget, SIGNAL(sigSwitchModeEditStrokeGradient(bool)), SLOT(slotActivateEditStrokeGradient(bool))); return widgets; } void DefaultTool::canvasResourceChanged(int key, const QVariant &res) { if (key == HotPosition) { m_hotPosition = KoFlake::AnchorPosition(res.toInt()); repaintDecorations(); } } KoInteractionStrategy *DefaultTool::createStrategy(KoPointerEvent *event) { KoShapeManager *shapeManager = canvas()->shapeManager(); KoSelection *selection = koSelection(); bool insideSelection = false; KoFlake::SelectionHandle handle = handleAt(event->point, &insideSelection); bool editableShape = !selection->selectedEditableShapes().isEmpty(); const bool selectMultiple = event->modifiers() & Qt::ShiftModifier; const bool selectNextInStack = event->modifiers() & Qt::ControlModifier; const bool avoidSelection = event->modifiers() & Qt::AltModifier; if (selectNextInStack) { // change the hot selection position when middle clicking on a handle KoFlake::AnchorPosition newHotPosition = m_hotPosition; switch (handle) { case KoFlake::TopMiddleHandle: newHotPosition = KoFlake::Top; break; case KoFlake::TopRightHandle: newHotPosition = KoFlake::TopRight; break; case KoFlake::RightMiddleHandle: newHotPosition = KoFlake::Right; break; case KoFlake::BottomRightHandle: newHotPosition = KoFlake::BottomRight; break; case KoFlake::BottomMiddleHandle: newHotPosition = KoFlake::Bottom; break; case KoFlake::BottomLeftHandle: newHotPosition = KoFlake::BottomLeft; break; case KoFlake::LeftMiddleHandle: newHotPosition = KoFlake::Left; break; case KoFlake::TopLeftHandle: newHotPosition = KoFlake::TopLeft; break; case KoFlake::NoHandle: default: // check if we had hit the center point const KoViewConverter *converter = canvas()->viewConverter(); QPointF pt = converter->documentToView(event->point); // TODO: use calculated values instead! QPointF centerPt = converter->documentToView(selection->absolutePosition()); if (kisSquareDistance(pt, centerPt) < HANDLE_DISTANCE_SQ) { newHotPosition = KoFlake::Center; } break; } if (m_hotPosition != newHotPosition) { canvas()->resourceManager()->setResource(HotPosition, newHotPosition); return new NopInteractionStrategy(this); } } if (!avoidSelection && editableShape) { // manipulation of selected shapes goes first if (handle != KoFlake::NoHandle) { // resizing or shearing only with left mouse button if (insideSelection) { return new ShapeResizeStrategy(this, event->point, handle); } if (handle == KoFlake::TopMiddleHandle || handle == KoFlake::RightMiddleHandle || handle == KoFlake::BottomMiddleHandle || handle == KoFlake::LeftMiddleHandle) { return new ShapeShearStrategy(this, event->point, handle); } // rotating is allowed for rigth mouse button too if (handle == KoFlake::TopLeftHandle || handle == KoFlake::TopRightHandle || handle == KoFlake::BottomLeftHandle || handle == KoFlake::BottomRightHandle) { return new ShapeRotateStrategy(this, event->point, event->buttons()); } } if (!selectMultiple && !selectNextInStack) { if (insideSelection) { return new ShapeMoveStrategy(this, event->point); } } } KoShape *shape = shapeManager->shapeAt(event->point, selectNextInStack ? KoFlake::NextUnselected : KoFlake::ShapeOnTop); if (avoidSelection || (!shape && handle == KoFlake::NoHandle)) { if (!selectMultiple) { repaintDecorations(); selection->deselectAll(); } return new SelectionInteractionStrategy(this, event->point, false); } if (selection->isSelected(shape)) { if (selectMultiple) { repaintDecorations(); selection->deselect(shape); } } else if (handle == KoFlake::NoHandle) { // clicked on shape which is not selected repaintDecorations(); if (!selectMultiple) { shapeManager->selection()->deselectAll(); } selection->select(shape); repaintDecorations(); // tablet selection isn't precise and may lead to a move, preventing that if (event->isTabletEvent()) { return new NopInteractionStrategy(this); } return new ShapeMoveStrategy(this, event->point); } return 0; } void DefaultTool::updateActions() { QList editableShapes; if (koSelection()) { 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 || (!editableShapes.isEmpty() && canvas()->resourceManager()->hasResource(KoCanvasResourceManager::PageSize)); action("object_align_horizontal_left")->setEnabled(alignmentEnabled); action("object_align_horizontal_center")->setEnabled(alignmentEnabled); action("object_align_horizontal_right")->setEnabled(alignmentEnabled); action("object_align_vertical_top")->setEnabled(alignmentEnabled); action("object_align_vertical_center")->setEnabled(alignmentEnabled); action("object_align_vertical_bottom")->setEnabled(alignmentEnabled); action("object_group")->setEnabled(editableShapes.size() > 1); const bool distributionEnabled = editableShapes.size() > 2; action("object_distribute_horizontal_left")->setEnabled(distributionEnabled); action("object_distribute_horizontal_center")->setEnabled(distributionEnabled); action("object_distribute_horizontal_right")->setEnabled(distributionEnabled); action("object_distribute_horizontal_gaps")->setEnabled(distributionEnabled); action("object_distribute_vertical_top")->setEnabled(distributionEnabled); action("object_distribute_vertical_center")->setEnabled(distributionEnabled); action("object_distribute_vertical_bottom")->setEnabled(distributionEnabled); action("object_distribute_vertical_gaps")->setEnabled(distributionEnabled); bool hasGroupShape = false; foreach (KoShape *shape, editableShapes) { if (dynamic_cast(shape)) { hasGroupShape = true; break; } } action("object_ungroup")->setEnabled(hasGroupShape); emit selectionChanged(editableShapes.size()); } KoToolSelection *DefaultTool::selection() { return m_selectionHandler; } QMenu* DefaultTool::popupActionsMenu() { if (m_contextMenu) { m_contextMenu->clear(); KActionCollection *collection = this->canvas()->canvasController()->actionCollection(); m_contextMenu->addAction(collection->action("edit_cut")); m_contextMenu->addAction(collection->action("edit_copy")); m_contextMenu->addAction(collection->action("edit_paste")); m_contextMenu->addSeparator(); m_contextMenu->addAction(action("object_order_front")); m_contextMenu->addAction(action("object_order_raise")); m_contextMenu->addAction(action("object_order_lower")); m_contextMenu->addAction(action("object_order_back")); if (action("object_group")->isEnabled() || action("object_ungroup")->isEnabled()) { m_contextMenu->addSeparator(); 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(); } void DefaultTool::explicitUserStrokeEndRequest() { QList shapes = koSelection()->selectedEditableShapesAndDelegates(); emit activateTemporary(KoToolManager::instance()->preferredToolForSelection(shapes)); } diff --git a/plugins/tools/defaulttool/defaulttool/DefaultTool.h b/plugins/tools/defaulttool/defaulttool/DefaultTool.h index 85d336cf3f..4f41fb9cea 100644 --- a/plugins/tools/defaulttool/defaulttool/DefaultTool.h +++ b/plugins/tools/defaulttool/defaulttool/DefaultTool.h @@ -1,173 +1,175 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006-2008 Thomas Zander This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef DEFAULTTOOL_H #define DEFAULTTOOL_H #include #include #include #include #include #include class QSignalMapper; class KoInteractionStrategy; class KoShapeMoveCommand; class KoSelection; class DefaultToolTabbedWidget; /** * The default tool (associated with the arrow icon) implements the default * interactions you have with flake objects.
* The tool provides scaling, moving, selecting, rotation and soon skewing of * any number of shapes. *

Note that the implementation of those different strategies are delegated * to the InteractionStrategy class and its subclasses. */ class DefaultTool : public KoInteractionTool { Q_OBJECT public: /** * Constructor for basic interaction tool where user actions are translated * and handled by interaction strategies of type KoInteractionStrategy. * @param canvas the canvas this tool will be working for. */ explicit DefaultTool(KoCanvasBase *canvas); ~DefaultTool() override; enum CanvasResource { HotPosition = 1410100299 }; public: bool wantsAutoScroll() const override; void paint(QPainter &painter, const KoViewConverter &converter) override; void repaintDecorations() override; ///reimplemented void copy() const override; ///reimplemented void deleteSelection() override; ///reimplemented bool paste() override; ///reimplemented KoToolSelection *selection() override; QMenu* popupActionsMenu() override; /** * Returns which selection handle is at params point (or NoHandle if none). * @return which selection handle is at params point (or NoHandle if none). * @param point the location (in pt) where we should look for a handle * @param innerHandleMeaning this boolean is altered to true if the point * is inside the selection rectangle and false if it is just outside. * The value of innerHandleMeaning is undefined if the handle location is NoHandle */ KoFlake::SelectionHandle handleAt(const QPointF &point, bool *innerHandleMeaning = 0); public Q_SLOTS: void activate(ToolActivation activation, const QSet &shapes) override; void deactivate() override; private Q_SLOTS: void selectionAlign(int _align); void selectionDistribute(int _distribute); void selectionBringToFront(); void selectionSendToBack(); void selectionMoveUp(); void selectionMoveDown(); void selectionGroup(); void selectionUngroup(); + void selectionTransform(int transformAction); + void slotActivateEditFillGradient(bool value); void slotActivateEditStrokeGradient(bool value); /// Update actions on selection change void updateActions(); public: // Events void mousePressEvent(KoPointerEvent *event) override; void mouseMoveEvent(KoPointerEvent *event) override; void mouseReleaseEvent(KoPointerEvent *event) override; void mouseDoubleClickEvent(KoPointerEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void explicitUserStrokeEndRequest() override; protected: QList > createOptionWidgets() override; KoInteractionStrategy *createStrategy(KoPointerEvent *event) override; private: class MoveGradientHandleInteractionFactory; private: void setupActions(); void recalcSelectionBox(KoSelection *selection); void updateCursor(); /// Returns rotation angle of given handle of the current selection qreal rotationOfHandle(KoFlake::SelectionHandle handle, bool useEdgeRotation); void addMappedAction(QSignalMapper *mapper, const QString &actionId, int type); void selectionReorder(KoShapeReorderCommand::MoveShapeType order); bool moveSelection(int direction, Qt::KeyboardModifiers modifiers); /// Returns selection rectangle adjusted by handle proximity threshold QRectF handlesSize(); // convenience method; KoSelection *koSelection(); void canvasResourceChanged(int key, const QVariant &res) override; KoFlake::SelectionHandle m_lastHandle; KoFlake::AnchorPosition m_hotPosition; bool m_mouseWasInsideHandles; QPointF m_selectionBox[8]; QPolygonF m_selectionOutline; QPointF m_lastPoint; // TODO alter these 3 arrays to be static const instead QCursor m_sizeCursors[8]; QCursor m_rotateCursors[8]; QCursor m_shearCursors[8]; qreal m_angle; KoToolSelection *m_selectionHandler; friend class SelectionHandler; KoInteractionStrategy *m_customEventStrategy; QScopedPointer m_contextMenu; DefaultToolTabbedWidget *m_tabbedOptionWidget; }; #endif diff --git a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp index a9df04da7b..8fd959d3b8 100644 --- a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp +++ b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.cpp @@ -1,111 +1,117 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * Copyright (C) 2007-2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ShapeRotateStrategy.h" #include "SelectionDecorator.h" #include #include #include #include #include #include #include #include #include #include ShapeRotateStrategy::ShapeRotateStrategy(KoToolBase *tool, const QPointF &clicked, Qt::MouseButtons buttons) : 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(); } KoFlake::AnchorPosition anchor = !(buttons & Qt::RightButton) ? KoFlake::Center : KoFlake::AnchorPosition(tool->canvas()->resourceManager()->resource(KoFlake::HotPosition).toInt()); m_rotationCenter = tool->canvas()->shapeManager()->selection()->absolutePosition(anchor); tool->setStatusText(i18n("Press ALT to rotate in 45 degree steps.")); } void ShapeRotateStrategy::handleMouseMove(const QPointF &point, Qt::KeyboardModifiers modifiers) { qreal angle = atan2(point.y() - m_rotationCenter.y(), point.x() - m_rotationCenter.x()) - atan2(m_start.y() - m_rotationCenter.y(), m_start.x() - m_rotationCenter.x()); angle = angle / M_PI * 180; // convert to degrees. if (modifiers & (Qt::AltModifier | Qt::ControlModifier)) { // limit to 45 degree angles qreal modula = qAbs(angle); while (modula > 45.0) { modula -= 45.0; } if (modula > 22.5) { modula -= 45.0; } angle += (angle > 0 ? -1 : 1) * modula; } rotateBy(angle); } void ShapeRotateStrategy::rotateBy(qreal angle) { QTransform matrix; matrix.translate(m_rotationCenter.x(), m_rotationCenter.y()); matrix.rotate(angle); matrix.translate(-m_rotationCenter.x(), -m_rotationCenter.y()); 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()); } } void ShapeRotateStrategy::paint(QPainter &painter, const KoViewConverter &converter) { // paint the rotation center painter.setPen(QPen(Qt::red)); painter.setBrush(QBrush(Qt::red)); painter.setRenderHint(QPainter::Antialiasing, true); QRectF circle(0, 0, 5, 5); circle.moveCenter(converter.documentToView(m_rotationCenter)); painter.drawEllipse(circle); } KUndo2Command *ShapeRotateStrategy::createCommand() { QList 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 6f20d3eef4..0ff63222d9 100644 --- a/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h +++ b/plugins/tools/defaulttool/defaulttool/ShapeRotateStrategy.h @@ -1,68 +1,68 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SHAPEROTATESTRATEGY_H #define SHAPEROTATESTRATEGY_H #include #include #include #include #include class KoToolBase; class KoShape; /** * A strategy for the KoInteractionTool. * This strategy is invoked when the user starts a rotate of a selection of objects, * the stategy will then rotate the objects interactively and provide a command afterwards. */ class ShapeRotateStrategy : public KoInteractionStrategy { public: /** * Constructor that starts to rotate the objects. * @param tool the parent tool which controls this strategy * @param clicked the initial point that the user depressed (in pt). */ ShapeRotateStrategy(KoToolBase *tool, const QPointF &clicked, Qt::MouseButtons buttons); ~ShapeRotateStrategy() override {} void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) override; KUndo2Command *createCommand() override; void finishInteraction(Qt::KeyboardModifiers modifiers) override { Q_UNUSED(modifiers); } void paint(QPainter &painter, const KoViewConverter &converter) override; private: void rotateBy(qreal angle); QPointF m_start; QTransform m_rotationMatrix; QList m_oldTransforms; QPointF m_rotationCenter; - QList m_selectedShapes; + QList m_transformedShapesAndSelection; }; #endif diff --git a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp index 0739e84b1f..0a6318a0b8 100644 --- a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp +++ b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.cpp @@ -1,171 +1,178 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * Copyright (C) 2006 C. Boemann * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ShapeShearStrategy.h" #include "SelectionDecorator.h" #include #include #include #include #include #include #include #include #include #include #include #include ShapeShearStrategy::ShapeShearStrategy(KoToolBase *tool, const QPointF &clicked, KoFlake::SelectionHandle direction) : KoInteractionStrategy(tool) , 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(); } // Eventhoug we aren't currently activated by the corner handles we might as well code like it switch (direction) { case KoFlake::TopMiddleHandle: m_top = true; m_bottom = false; m_left = false; m_right = false; break; case KoFlake::TopRightHandle: m_top = true; m_bottom = false; m_left = false; m_right = true; break; case KoFlake::RightMiddleHandle: m_top = false; m_bottom = false; m_left = false; m_right = true; break; case KoFlake::BottomRightHandle: m_top = false; m_bottom = true; m_left = false; m_right = true; break; case KoFlake::BottomMiddleHandle: m_top = false; m_bottom = true; m_left = false; m_right = false; break; case KoFlake::BottomLeftHandle: m_top = false; m_bottom = true; m_left = true; m_right = false; break; case KoFlake::LeftMiddleHandle: m_top = false; m_bottom = false; m_left = true; m_right = false; break; case KoFlake::TopLeftHandle: m_top = true; m_bottom = false; m_left = true; m_right = false; break; default: ;// throw exception ? TODO } m_initialSize = sel->size(); m_solidPoint = QPointF(m_initialSize.width() / 2, m_initialSize.height() / 2); if (m_top) { m_solidPoint += QPointF(0, m_initialSize.height() / 2); } else if (m_bottom) { m_solidPoint -= QPointF(0, m_initialSize.height() / 2); } if (m_left) { m_solidPoint += QPointF(m_initialSize.width() / 2, 0); } else if (m_right) { m_solidPoint -= QPointF(m_initialSize.width() / 2, 0); } m_solidPoint = sel->absoluteTransformation(0).map(sel->outlineRect().topLeft() + m_solidPoint); QPointF edge; qreal angle = 0.0; if (m_top) { edge = sel->absolutePosition(KoFlake::BottomLeft) - sel->absolutePosition(KoFlake::BottomRight); angle = 180.0; } else if (m_bottom) { edge = sel->absolutePosition(KoFlake::TopRight) - sel->absolutePosition(KoFlake::TopLeft); angle = 0.0; } else if (m_left) { edge = sel->absolutePosition(KoFlake::BottomLeft) - sel->absolutePosition(KoFlake::TopLeft); angle = 90.0; } else if (m_right) { edge = sel->absolutePosition(KoFlake::TopRight) - sel->absolutePosition(KoFlake::BottomRight); angle = 270.0; } qreal currentAngle = atan2(edge.y(), edge.x()) / M_PI * 180; m_initialSelectionAngle = currentAngle - angle; // use crossproduct of top edge and left edge of selection bounding rect // to determine if the selection is mirrored QPointF top = sel->absolutePosition(KoFlake::TopRight) - sel->absolutePosition(KoFlake::TopLeft); QPointF left = sel->absolutePosition(KoFlake::BottomLeft) - sel->absolutePosition(KoFlake::TopLeft); m_isMirrored = (top.x() * left.y() - top.y() * left.x()) < 0.0; } void ShapeShearStrategy::handleMouseMove(const QPointF &point, Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers); QPointF shearVector = point - m_start; QTransform m; m.rotate(-m_initialSelectionAngle); shearVector = m.map(shearVector); qreal shearX = 0, shearY = 0; if (m_top || m_left) { shearVector = - shearVector; } if (m_top || m_bottom) { shearX = shearVector.x() / m_initialSize.height(); } if (m_left || m_right) { shearY = shearVector.y() / m_initialSize.width(); } // if selection is mirrored invert the shear values if (m_isMirrored) { shearX *= -1.0; shearY *= -1.0; } QTransform matrix; matrix.translate(m_solidPoint.x(), m_solidPoint.y()); matrix.rotate(m_initialSelectionAngle); matrix.shear(shearX, shearY); matrix.rotate(-m_initialSelectionAngle); matrix.translate(-m_solidPoint.x(), -m_solidPoint.y()); 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()); } m_shearMatrix = matrix; } void ShapeShearStrategy::paint(QPainter &painter, const KoViewConverter &converter) { Q_UNUSED(painter); Q_UNUSED(converter); } KUndo2Command *ShapeShearStrategy::createCommand() { QList 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 53667a1304..a24a145dfd 100644 --- a/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h +++ b/plugins/tools/defaulttool/defaulttool/ShapeShearStrategy.h @@ -1,71 +1,71 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SHAPESHEARSTRATEGY_H #define SHAPESHEARSTRATEGY_H #include #include #include #include #include class KoToolBase; class KoShape; /** * A strategy for the KoInteractionTool. * This strategy is invoked when the user starts a shear of a selection of objects, * the stategy will then shear the objects interactively and provide a command afterwards. */ class ShapeShearStrategy : public KoInteractionStrategy { public: /** * Constructor that starts to rotate the objects. * @param tool the parent tool which controls this strategy * @param clicked the initial point that the user depressed (in pt). * @param direction the handle that was grabbed */ ShapeShearStrategy(KoToolBase *tool, const QPointF &clicked, KoFlake::SelectionHandle direction); ~ShapeShearStrategy() override {} void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) override; KUndo2Command *createCommand() override; void finishInteraction(Qt::KeyboardModifiers modifiers) override { Q_UNUSED(modifiers); } void paint(QPainter &painter, const KoViewConverter &converter) override; private: QPointF m_start; QPointF m_solidPoint; QSizeF m_initialSize; bool m_top, m_left, m_bottom, m_right; qreal m_initialSelectionAngle; QTransform m_shearMatrix; bool m_isMirrored; QList m_oldTransforms; - QList m_selectedShapes; + QList m_transformedShapesAndSelection; }; #endif