Commit 01c32383 authored by Dmitry Kazakov's avatar Dmitry Kazakov

A huge KoFlake refactoring to make shape transformations work correctly

1) KoSelection is completely refactored. Now it is always *recursive*.
   More than that, now it automatically tracks the transformation changes
   of the containing shapes (using KoShape::ShapeChangeListener)

2) The size of KoSelection is now automatically tracked as well.

3) A lot of small fixes in DefaultTool transformation strategies. Now
   you can easily transform everything: single shapes, multiple
   selections and groups.

There is still one bug: scaling of multi-object selection works weirdly.
parent 74506b55
......@@ -73,3 +73,41 @@ QPointF KoFlake::toAbsolute(const QPointF &relative, const QSizeF &size)
{
return QPointF(relative.x() * size.width(), relative.y() * size.height());
}
#include <KoShape.h>
#include <QTransform>
#include "kis_debug.h"
#include "kis_algebra_2d.h"
void KoFlake::resizeShape(KoShape *shape, qreal scaleX, qreal scaleY, const QPointF &absoluteStillPoint, bool usePostScaling)
{
QPointF localStillPoint = shape->absoluteTransformation(0).inverted().map(absoluteStillPoint);
QPointF relativeStillPoint = KisAlgebra2D::absoluteToRelative(localStillPoint, shape->outlineRect());
QPointF parentalStillPointBefore = shape->transformation().map(localStillPoint);
if (usePostScaling) {
const QTransform scale = QTransform::fromScale(scaleX, scaleY);
shape->setTransformation(shape->transformation() * scale);
} else {
using namespace KisAlgebra2D;
const QSizeF oldSize(shape->size());
const QSizeF newSize(oldSize.width() * qAbs(scaleX), oldSize.height() * qAbs(scaleY));
const QTransform mirrorTransform = QTransform::fromScale(signPZ(scaleX), signPZ(scaleY));
shape->setSize(newSize);
if (!mirrorTransform.isIdentity()) {
shape->setTransformation(mirrorTransform * shape->transformation());
}
}
QPointF newLocalStillPoint = KisAlgebra2D::relativeToAbsolute(relativeStillPoint, shape->outlineRect());
QPointF parentalStillPointAfter = shape->transformation().map(newLocalStillPoint);
QPointF diff = parentalStillPointBefore - parentalStillPointAfter;
shape->setTransformation(shape->transformation() * QTransform::fromTranslate(diff.x(), diff.y()));
}
......@@ -27,6 +27,11 @@ class QGradient;
class QPointF;
class QSizeF;
class KoShape;
class QTransform;
#include <QtGlobal>
/**
* Flake reference
*/
......@@ -101,6 +106,9 @@ namespace KoFlake
* @return absolute position
*/
KRITAFLAKE_EXPORT QPointF toAbsolute(const QPointF &relative, const QSizeF &size);
KRITAFLAKE_EXPORT void resizeShape(KoShape *shape, qreal scaleX, qreal scaleY,
const QPointF &absoluteStillPoint, bool usePostScaling = true);
}
#endif
This diff is collapsed.
......@@ -48,7 +48,7 @@ class KoSelectionPrivate;
* A selection, however, should not be selectable. We need to think
* a little about the interaction here.
*/
class KRITAFLAKE_EXPORT KoSelection : public QObject, public KoShape
class KRITAFLAKE_EXPORT KoSelection : public QObject, public KoShape, public KoShape::ShapeChangeListener
{
Q_OBJECT
......@@ -57,7 +57,11 @@ public:
KoSelection();
virtual ~KoSelection();
virtual void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext);
void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) override;
void setSize(const QSizeF &size) override;
QSizeF size() const override;
QRectF outlineRect() const override;
QRectF boundingRect() const override;
/**
* Adds a shape to the selection.
......@@ -73,7 +77,7 @@ public:
* @param shape the shape to add to the selection
* @param recursive enables recursively selecting shapes of parent groups
*/
void select(KoShape *shape, bool recursive = true);
void select(KoShape *shape);
/**
* Removes a selected shape.
......@@ -89,7 +93,7 @@ public:
* @param shape the shape to remove from the selection
* @param recursive enables recursively deselecting shapes of parent groups
*/
void deselect(KoShape *shape, bool recursive = true);
void deselect(KoShape *shape);
/// clear the selections list
void deselectAll();
......@@ -117,8 +121,6 @@ public:
virtual bool hitTest(const QPointF &position) const;
virtual QRectF boundingRect() const;
/**
* Sets the currently active layer.
* @param layer the new active layer
......@@ -132,8 +134,7 @@ public:
*/
KoShapeLayer *activeLayer() const;
/// Updates the size and position of the selection
void updateSizeAndPosition();
void notifyShapeChanged(ChangeType type, KoShape *shape);
Q_SIGNALS:
/// emitted when the selection is changed
......@@ -146,7 +147,6 @@ private:
virtual void saveOdf(KoShapeSavingContext &) const;
virtual bool loadOdf(const KoXmlElement &, KoShapeLoadingContext &);
Q_PRIVATE_SLOT(d_func(), void selectionChangedEvent())
Q_DECLARE_PRIVATE_D(KoShape::d_ptr, KoSelection)
};
......
......@@ -21,27 +21,28 @@
#include "KoShape_p.h"
#include "kis_signal_compressor.h"
class KoShapeGroup;
class KoSelectionPrivate : public KoShapePrivate
{
public:
explicit KoSelectionPrivate(KoSelection *parent)
: KoShapePrivate(parent), eventTriggered(false), activeLayer(0) {}
: KoShapePrivate(parent),
activeLayer(0),
selectionChangedCompressor(1, KisSignalCompressor::FIRST_INACTIVE)
{}
QList<KoShape*> selectedShapes;
bool eventTriggered;
KoShapeLayer *activeLayer;
void requestSelectionChangedEvent();
void selectGroupChildren(KoShapeGroup *group);
void deselectGroupChildren(KoShapeGroup *group);
KisSignalCompressor selectionChangedCompressor;
void selectionChangedEvent();
QRectF sizeRect();
QList<QTransform> savedMatrices;
QRectF globalBound;
QList<QTransform> fetchShapesMatrices() const;
bool checkMatricesConsistent(const QList<QTransform> &matrices, QTransform *newTransform);
Q_DECLARE_PUBLIC(KoSelection)
};
......
......@@ -174,9 +174,16 @@ void KoShapePrivate::shapeChanged(KoShape::ChangeType type)
Q_Q(KoShape);
if (parent)
parent->model()->childChanged(q, type);
q->shapeChanged(type);
Q_FOREACH (KoShape * shape, dependees)
Q_FOREACH (KoShape * shape, dependees) {
shape->shapeChanged(type, q);
}
Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners) {
listener->notifyShapeChangedImpl(type, q);
}
}
void KoShapePrivate::updateStroke()
......@@ -680,13 +687,15 @@ QPainterPath KoShape::shadowOutline() const
QPointF KoShape::absolutePosition(KoFlake::Position anchor) const
{
const QRectF rc = outlineRect();
QPointF point;
switch (anchor) {
case KoFlake::TopLeftCorner: break;
case KoFlake::TopRightCorner: point = QPointF(size().width(), 0.0); break;
case KoFlake::BottomLeftCorner: point = QPointF(0.0, size().height()); break;
case KoFlake::BottomRightCorner: point = QPointF(size().width(), size().height()); break;
case KoFlake::CenteredPosition: point = QPointF(size().width() / 2.0, size().height() / 2.0); break;
case KoFlake::TopLeftCorner: point = rc.topLeft(); break;
case KoFlake::TopRightCorner: point = rc.topRight(); break;
case KoFlake::BottomLeftCorner: point = rc.bottomLeft(); break;
case KoFlake::BottomRightCorner: point = rc.bottomRight(); break;
case KoFlake::CenteredPosition: point = rc.center(); break;
}
return absoluteTransformation(0).map(point);
}
......@@ -812,7 +821,7 @@ QSizeF KoShape::size() const
QPointF KoShape::position() const
{
Q_D(const KoShape);
QPointF center(0.5*size().width(), 0.5*size().height());
QPointF center = outlineRect().center();
return d->localMatrix.map(center) - center;
}
......@@ -2306,3 +2315,51 @@ KoShapePrivate *KoShape::priv()
Q_D(KoShape);
return d;
}
KoShape::ShapeChangeListener::~ShapeChangeListener()
{
Q_FOREACH(KoShape *shape, m_registeredShapes) {
shape->removeShapeChangeListener(this);
}
}
void KoShape::ShapeChangeListener::registerShape(KoShape *shape)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(!m_registeredShapes.contains(shape));
m_registeredShapes.append(shape);
}
void KoShape::ShapeChangeListener::unregisterShape(KoShape *shape)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_registeredShapes.contains(shape));
m_registeredShapes.removeAll(shape);
}
void KoShape::ShapeChangeListener::notifyShapeChangedImpl(KoShape::ChangeType type, KoShape *shape)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_registeredShapes.contains(shape));
notifyShapeChanged(type, shape);
if (type == KoShape::Deleted) {
unregisterShape(shape);
}
}
void KoShape::addShapeChangeListener(KoShape::ShapeChangeListener *listener)
{
Q_D(KoShape);
KIS_SAFE_ASSERT_RECOVER_RETURN(!d->listeners.contains(listener));
listener->registerShape(this);
d->listeners.append(listener);
}
void KoShape::removeShapeChangeListener(KoShape::ShapeChangeListener *listener)
{
Q_D(KoShape);
KIS_SAFE_ASSERT_RECOVER_RETURN(d->listeners.contains(listener));
d->listeners.removeAll(listener);
listener->unregisterShape(this);
}
......@@ -1092,6 +1092,25 @@ public:
*/
KoShapePrivate *priv();
public:
struct ShapeChangeListener {
virtual ~ShapeChangeListener();
virtual void notifyShapeChanged(ChangeType type, KoShape *shape) = 0;
private:
friend class KoShape;
friend class KoShapePrivate;
void registerShape(KoShape *shape);
void unregisterShape(KoShape *shape);
void notifyShapeChangedImpl(ChangeType type, KoShape *shape);
QList<KoShape*> m_registeredShapes;
};
void addShapeChangeListener(ShapeChangeListener *listener);
void removeShapeChangeListener(ShapeChangeListener *listener);
protected:
/// constructor
KoShape(KoShapePrivate *);
......
......@@ -151,10 +151,7 @@ void KoShapeGroupPrivate::tryUpdateCachedSize() const
if (!sizeCached) {
QRectF bound;
Q_FOREACH (KoShape *shape, q->shapes()) {
if (bound.isEmpty())
bound = shape->transformation().mapRect(shape->outlineRect());
else
bound |= shape->transformation().mapRect(shape->outlineRect());
bound |= shape->transformation().mapRect(shape->outlineRect());
}
savedOutlineRect = bound;
size = bound.size();
......@@ -170,6 +167,19 @@ QSizeF KoShapeGroup::size() const
return d->size;
}
void KoShapeGroup::setSize(const QSizeF &size)
{
QSizeF oldSize = this->size();
if (!shapeCount() || oldSize.isNull()) return;
const QTransform scale =
QTransform::fromScale(size.width() / oldSize.width(), size.height() / oldSize.height());
setTransformation(scale * transformation());
KoShapeContainer::setSize(size);
}
QRectF KoShapeGroup::outlineRect() const
{
Q_D(const KoShapeGroup);
......@@ -180,15 +190,9 @@ QRectF KoShapeGroup::outlineRect() const
QRectF KoShapeGroup::boundingRect() const
{
bool first = true;
QRectF groupBound;
Q_FOREACH (KoShape* shape, shapes()) {
if (first) {
groupBound = shape->boundingRect();
first = false;
} else {
groupBound = groupBound.united(shape->boundingRect());
}
groupBound |= shape->boundingRect();
}
if (shadow()) {
......@@ -274,6 +278,8 @@ void KoShapeGroup::shapeChanged(ChangeType type, KoShape *shape)
default:
break;
}
invalidateSizeCache();
}
void KoShapeGroup::invalidateSizeCache()
......
......@@ -60,6 +60,7 @@ public:
/// always returns false since the group itself can't be selected or hit
virtual bool hitTest(const QPointF &position) const;
QSizeF size() const override;
void setSize(const QSizeF &size) override;
QRectF outlineRect() const override;
/// a group's boundingRect
QRectF boundingRect() const override;
......
......@@ -80,7 +80,6 @@ void KoShapeManager::Private::updateTree()
detector.fireSignals();
if (selectionModified) {
selection->updateSizeAndPosition();
emit q->selectionContentChanged();
}
if (anyModified) {
......
......@@ -84,6 +84,7 @@ public:
QSharedPointer<KoShapeStrokeModel> stroke; ///< points to a stroke, or 0 if there is no stroke
QSharedPointer<KoShapeBackground> fill; ///< Stands for the background color / fill etc.
QList<KoShape*> dependees; ///< list of shape dependent on this shape
QList<KoShape::ShapeChangeListener*> listeners;
KoShapeShadow * shadow; ///< the current shape shadow
KoBorder *border; ///< the current shape border
QScopedPointer<KoClipPath> clipPath; ///< the current clip path
......
......@@ -190,21 +190,6 @@ void KoToolBase::inputMethodEvent(QInputMethodEvent * event)
event->accept();
}
void KoToolBase::customPressEvent(KoPointerEvent * event)
{
event->ignore();
}
void KoToolBase::customReleaseEvent(KoPointerEvent * event)
{
event->ignore();
}
void KoToolBase::customMoveEvent(KoPointerEvent * event)
{
event->ignore();
}
bool KoToolBase::wantsTouch() const
{
return false;
......
......@@ -209,29 +209,6 @@ public:
*/
virtual void inputMethodEvent(QInputMethodEvent *event);
/**
* Called when (one of) a custom device buttons is pressed.
* Implementors should call event->ignore() if they do not actually use the event.
* @param event state and reason of this custom device press
*/
virtual void customPressEvent(KoPointerEvent *event);
/**
* Called when (one of) a custom device buttons is released.
* Implementors should call event->ignore() if they do not actually use the event.
* @param event state and reason of this custom device release
*/
virtual void customReleaseEvent(KoPointerEvent *event);
/**
* Called when a custom device moved over the canvas.
* Implementors should call event->ignore() if they do not actually use the event.
* @param event state and reason of this custom device move
*/
virtual void customMoveEvent(KoPointerEvent *event);
/**
* @return true if synthetic mouse events on the canvas should be eaten.
*
......
......@@ -400,18 +400,6 @@ QString KoToolManager::preferredToolForSelection(const QList<KoShape*> &shapes)
return toolType;
}
void KoToolManager::injectDeviceEvent(KoInputDeviceHandlerEvent * event)
{
if (d->canvasData && d->canvasData->canvas->canvas()) {
if (static_cast<KoInputDeviceHandlerEvent::Type>(event->type()) == KoInputDeviceHandlerEvent::ButtonPressed)
d->canvasData->activeTool->customPressEvent(event->pointerEvent());
else if (static_cast<KoInputDeviceHandlerEvent::Type>(event->type()) == KoInputDeviceHandlerEvent::ButtonReleased)
d->canvasData->activeTool->customReleaseEvent(event->pointerEvent());
else if (static_cast<KoInputDeviceHandlerEvent::Type>(event->type()) == KoInputDeviceHandlerEvent::PositionChanged)
d->canvasData->activeTool->customMoveEvent(event->pointerEvent());
}
}
void KoToolManager::addDeferredToolFactory(KoToolFactoryBase *toolFactory)
{
ToolHelper *tool = new ToolHelper(toolFactory);
......
......@@ -230,9 +230,6 @@ public:
/// Request tool activation for the given canvas controller
void requestToolActivation(KoCanvasController *controller);
/// Injects an input event from a plugin based input device
void injectDeviceEvent(KoInputDeviceHandlerEvent *event);
/// Returns the toolId of the currently active tool
QString activeToolId() const;
......
......@@ -215,20 +215,14 @@ void KoShapeGroupCommand::undo()
QRectF KoShapeGroupCommandPrivate::containerBoundingRect()
{
bool boundingRectInitialized = true;
QRectF bound;
if (container->shapeCount() > 0)
bound = container->boundingRect();
else
boundingRectInitialized = false;
if (container->shapeCount() > 0) {
bound = container->absoluteTransformation(0).mapRect(container->outlineRect());
}
Q_FOREACH (KoShape *shape, shapes) {
if (boundingRectInitialized)
bound = bound.united(shape->boundingRect());
else {
bound = shape->boundingRect();
boundingRectInitialized = true;
}
bound |= shape->absoluteTransformation(0).mapRect(shape->outlineRect());
}
return bound;
}
......@@ -182,7 +182,7 @@ void TestShapeContainer::testScaling2()
groupCommand->redo();
KoSelection* selection = new KoSelection();
selection->select(shape1, true);
selection->select(shape1);
QList<KoShape*> transformShapes;
transformShapes.append(selection->selectedShapes());
......@@ -212,7 +212,7 @@ void TestShapeContainer::testScaling2()
QSizeF shapeSize=r1.united(r2).size();
selection = new KoSelection();
selection->select(shape1, true);
selection->select(shape1);
QSizeF selecSize = selection->size();
bool works=false;
......
......@@ -51,11 +51,6 @@ KoInteractionStrategy::~KoInteractionStrategy()
delete d_ptr;
}
void KoInteractionStrategy::handleCustomEvent(KoPointerEvent *event)
{
Q_UNUSED(event);
}
void KoInteractionStrategy::paint(QPainter &, const KoViewConverter &)
{
}
......
......@@ -68,13 +68,6 @@ public:
*/
virtual void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) = 0;
/**
* Extending classes should implement this method to update the selectedShapes
* based on the new pointer event. The default implementations does nothing.
* @param event the new pointer event
*/
virtual void handleCustomEvent(KoPointerEvent *event);
/**
* For interactions that are undo-able this method should be implemented to return such
* a command. Implementations should return 0 otherwise.
......
......@@ -106,12 +106,6 @@ void KoPanTool::activate(ToolActivation toolActivation, const QSet<KoShape*> &)
useCursor(QCursor(Qt::OpenHandCursor));
}
void KoPanTool::customMoveEvent(KoPointerEvent * event)
{
m_controller->pan(QPoint(-event->x(), -event->y()));
event->accept();
}
QPointF KoPanTool::documentToViewport(const QPointF &p)
{
Q_D(KoToolBase);
......
......@@ -55,8 +55,6 @@ public:
/// reimplemented from superclass
virtual void activate(ToolActivation toolActivation, const QSet<KoShape*> &shapes);
/// reimplemented method
virtual void customMoveEvent(KoPointerEvent *event);
/// reimplemented method
virtual void mouseDoubleClickEvent(KoPointerEvent *event);
......
......@@ -15,6 +15,8 @@ set(kritaglobal_LIB_SRCS
kis_dom_utils.cpp
kis_painting_tweaks.cpp
KisHandlePainterHelper.cpp
kis_signal_compressor.cpp
kis_signal_compressor_with_param.cpp
)
add_library(kritaglobal SHARED ${kritaglobal_LIB_SRCS} )
......
......@@ -20,7 +20,7 @@
#define __KIS_SIGNAL_COMPRESSOR_H
#include <QTimer>
#include "kritaimage_export.h"
#include "kritaglobal_export.h"
class QTimer;
......@@ -52,7 +52,7 @@ class QTimer;
* the timer has elapsed.
*
*/
class KRITAIMAGE_EXPORT KisSignalCompressor : public QObject
class KRITAGLOBAL_EXPORT KisSignalCompressor : public QObject
{
Q_OBJECT
......
......@@ -36,7 +36,7 @@
* called. std::bind allows us to call any method of any class without
* changing signature of the class or creating special wrappers.
*/
class KRITAIMAGE_EXPORT SignalToFunctionProxy : public QObject
class KRITAGLOBAL_EXPORT SignalToFunctionProxy : public QObject
{
Q_OBJECT
public:
......
......@@ -189,8 +189,6 @@ set(kritaimage_LIB_SRCS
kis_regenerate_frame_stroke_strategy.cpp
kis_switch_time_stroke_strategy.cpp
kis_crop_saved_extra_data.cpp
kis_signal_compressor.cpp
kis_signal_compressor_with_param.cpp
kis_thread_safe_signal_compressor.cpp
kis_acyclic_signal_connector.cpp
kis_timed_signal_threshold.cpp
......
......@@ -34,9 +34,9 @@
#include <KoShapeGroupCommand.h>
void KisShapeCommandsTest::test()
void KisShapeCommandsTest::testGrouping()
{
TestUtil::ExternalImageChecker chk("shape_commands_test", "grouping");
TestUtil::ExternalImageChecker chk("grouping", "shape_commands_test");
QRect refRect(0,0,64,64);
......@@ -120,4 +120,110 @@ void KisShapeCommandsTest::test()
}
void KisShapeCommandsTest::testResizeShape(bool normalizeGroup)
{
TestUtil::ExternalImageChecker chk("resize_shape", "shape_commands_test");
QRect refRect(0,0,64,64);
QScopedPointer<KisDocument> doc(KisPart::instance()->createDocument());
TestUtil::MaskParent p(refRect);
const qreal resolution = 72.0 / 72.0;
p.image->setResolution(resolution, resolution);
doc->setCurrentImage(p.image);
KisShapeLayerSP shapeLayer = new KisShapeLayer(doc->shapeController(), p.image, "shapeLayer1", 75);
{
KoPathShape* path = new KoPathShape();
path->setShapeId(KoPathShapeId);
path->moveTo(QPointF(5, 5));
path->lineTo(QPointF(5, 55));
path->lineTo(QPointF(55, 55));
path->lineTo(QPointF(55, 5));
path->close();
path->normalize();
path->setBackground(toQShared(new KoColorBackground(Qt::red)));
path->setName("shape1");
path->setZIndex(1);
shapeLayer->addShape(path);
}
{
KoPathShape* path = new KoPathShape();
path->setShapeId(KoPathShapeId);
path->moveTo(QPointF(30, 30));
path->lineTo(QPointF(30, 60));
path->lineTo(QPointF(60, 60));
path->lineTo(QPointF(60, 30));
path->close();
path->normalize();
path->setBackground(toQShared(new KoColorBackground(Qt::green)));
path->setName("shape2");
path->setZIndex(2);
shapeLayer->addShape(path);
}
p.image->addNode(shapeLayer);
shapeLayer->setDirty();
qApp->processEvents();
p.image->waitForDone();
chk.checkImage(p.image, "00_initial_layer_update");
QList<KoShape*> shapes = shapeLayer->shapes();
KoShapeGroup *group = new KoShapeGroup();
group->setName("group_shape");
shapeLayer->addShape(group);
QScopedPointer<KoShapeGroupCommand> cmd(
new KoShapeGroupCommand(group, shapes, false, true, normalizeGroup));
cmd->redo();
shapeLayer->setDirty();
qApp->processEvents();
p.image->waitForDone();
chk.checkImage(p.image, "00_initial_layer_update");
qDebug() << "Before:";
qDebug() << ppVar(group->absolutePosition(KoFlake::TopLeftCorner));
qDebug() << ppVar(group->absolutePosition(KoFlake::BottomRightCorner));
qDebug() << ppVar(group->outlineRect());
qDebug() << ppVar(group->transformation());
QCOMPARE(group->absolutePosition(KoFlake::TopLeftCorner), QPointF(5,5));
QCOMPARE(group->absolutePosition(KoFlake::BottomRightCorner), QPointF(60,60));
const QPointF stillPoint = group->absolutePosition(KoFlake::BottomRightCorner);
KoFlake::resizeShape(group, 1.2, 1.4, stillPoint);
qDebug() << "After:";