Commit 1189dccb authored by Dmitry Kazakov's avatar Dmitry Kazakov

Implement copy-pasting of shapes!

This patch implements the following:

1) The shapes can be copy/pasted inside Krita
2) The shapes can be copy/pasted Krita->Inkscape
   (reverse does not yet work)
3) There are two shortcuts (reverse to Inkscape :( )
   Ctrl+V paste at original position
   Ctrl+Alt+V paste at cursor position

CC:kimageshop@kde.org
parent 948ec4e4
......@@ -388,18 +388,6 @@
<isCheckable>true</isCheckable>
<statusTip></statusTip>
</Action>
<Action name="paste_at">
<icon></icon>
<text>Paste at cursor</text>
<whatsThis></whatsThis>
<toolTip>Paste at cursor</toolTip>
<iconText>Paste at cursor</iconText>
<activationFlags>0</activationFlags>
<activationConditions>0</activationConditions>
<shortcut></shortcut>
<isCheckable>false</isCheckable>
<statusTip></statusTip>
</Action>
<Action name="invert">
<icon></icon>
<text>&amp;Invert Selection</text>
......
......@@ -2,7 +2,7 @@
<kpartgui xmlns="http://www.kde.org/standards/kxmlgui/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
name="Krita"
version="103"
version="104"
xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0 http://www.kde.org/standards/kxmlgui/1.0/kxmlgui.xsd">
<MenuBar>
<Menu name="file">
......@@ -49,6 +49,7 @@ xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0 http://www.kde.org
<Action name="copy_sharp"/>
<Action name="copy_merged"/>
<Action name="edit_paste"/>
<Action name="paste_at"/>
<Action name="paste_new"/>
<Action name="clear"/>
<Action name="fill_selection_foreground_color"/>
......
......@@ -368,6 +368,18 @@
<isCheckable>false</isCheckable>
<statusTip></statusTip>
</Action>
<Action name="paste_at">
<icon></icon>
<text>Paste at Cursor</text>
<whatsThis></whatsThis>
<toolTip>Paste at cursor</toolTip>
<iconText>Paste at cursor</iconText>
<activationFlags>0</activationFlags>
<activationConditions>0</activationConditions>
<shortcut>Ctrl+Alt+V</shortcut>
<isCheckable>false</isCheckable>
<statusTip></statusTip>
</Action>
<Action name="paste_new">
<icon></icon>
<text>Paste into &amp;New Image</text>
......
......@@ -80,6 +80,7 @@ set(kritaflake_SRCS
KoVectorPatternBackground.cpp
KoShapeConfigWidgetBase.cpp
KoDrag.cpp
KoSvgPaste.cpp
KoDragOdfSaveHelper.cpp
KoShapeOdfSaveHelper.cpp
KoShapePaste.cpp
......
......@@ -299,6 +299,12 @@ public:
QPoint documentOffset() const;
/**
* @return the current position of the cursor fetched from QCursor::pos() and
* converted into document coordinates
*/
virtual QPointF currentCursorPosition() const = 0;
protected:
void setDocumentSize(const QSize &sz);
QSize documentSize() const;
......@@ -465,6 +471,7 @@ public:
virtual void updateDocumentSize(const QSize &/*sz*/, bool /*recalculateCenter*/) {}
virtual void setZoomWithWheel(bool /*zoom*/) {}
virtual void setVastScrolling(qreal /*factor*/) {}
QPointF currentCursorPosition() const override { return QPointF(); }
};
......
......@@ -480,6 +480,13 @@ void KoCanvasControllerWidget::setVastScrolling(qreal factor)
d->vastScrollingFactor = factor;
}
QPointF KoCanvasControllerWidget::currentCursorPosition() const
{
QWidget *canvasWidget = d->canvas->canvasWidget();
const KoViewConverter *converter = d->canvas->viewConverter();
return converter->viewToDocument(canvasWidget->mapFromGlobal(QCursor::pos()) + d->canvas->canvasController()->documentOffset() - canvasWidget->pos());
}
void KoCanvasControllerWidget::pan(const QPoint &distance)
{
QPoint sourcePoint = scrollBarValue();
......
......@@ -139,6 +139,8 @@ public:
virtual void setVastScrolling(qreal factor);
QPointF currentCursorPosition() const override;
/**
* \internal
*/
......
......@@ -38,6 +38,11 @@
#include <KoEmbeddedDocumentSaver.h>
#include "KoShapeSavingContext.h"
#include <KoShape.h>
#include <QRect>
#include <SvgWriter.h>
class KoDragPrivate {
public:
KoDragPrivate() : mimeData(0) { }
......@@ -55,73 +60,36 @@ KoDrag::~KoDrag()
delete d;
}
bool KoDrag::setOdf(const char *mimeType, KoDragOdfSaveHelper &helper)
bool KoDrag::setOdf(const char *, KoDragOdfSaveHelper &)
{
struct Finally {
Finally(KoStore *s) : store(s) { }
~Finally() {
delete store;
}
KoStore *store;
};
QBuffer buffer;
KoStore *store = KoStore::createStore(&buffer, KoStore::Write, mimeType);
Finally finally(store); // delete store when we exit this scope
Q_ASSERT(store);
Q_ASSERT(!store->bad());
KoOdfWriteStore odfStore(store);
KoEmbeddedDocumentSaver embeddedSaver;
KoXmlWriter *manifestWriter = odfStore.manifestWriter(mimeType);
KoXmlWriter *contentWriter = odfStore.contentWriter();
if (!contentWriter) {
return false;
}
return false;
}
KoGenStyles mainStyles;
KoXmlWriter *bodyWriter = odfStore.bodyWriter();
KoShapeSavingContext *context = helper.context(bodyWriter, mainStyles, embeddedSaver);
bool KoDrag::setSvg(const QList<KoShape *> originalShapes)
{
QRectF boundingRect;
QList<KoShape*> shapes;
if (!helper.writeBody()) {
return false;
Q_FOREACH (KoShape *shape, originalShapes) {
boundingRect |= shape->boundingRect();
shapes.append(shape->cloneShape());
}
mainStyles.saveOdfStyles(KoGenStyles::DocumentAutomaticStyles, contentWriter);
qSort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex);
odfStore.closeContentWriter();
//add manifest line for content.xml
manifestWriter->addManifestEntry("content.xml", "text/xml");
if (!mainStyles.saveOdfStylesDotXml(store, manifestWriter)) {
return false;
}
if (!context->saveDataCenter(store, manifestWriter)) {
debugFlake << "save data centers failed";
return false;
}
QBuffer buffer;
QLatin1String mimeType("image/svg+xml");
// Save embedded objects
KoDocumentBase::SavingContext documentContext(odfStore, embeddedSaver);
if (!embeddedSaver.saveEmbeddedDocuments(documentContext)) {
debugFlake << "save embedded documents failed";
return false;
}
buffer.open(QIODevice::WriteOnly);
// Write out manifest file
if (!odfStore.closeManifestWriter()) {
return false;
}
const QSizeF pageSize(boundingRect.right(), boundingRect.bottom());
SvgWriter writer(shapes, pageSize);
writer.save(buffer);
delete store; // make sure the buffer if fully flushed.
finally.store = 0;
setData(mimeType, buffer.buffer());
buffer.close();
qDeleteAll(shapes);
setData(mimeType, buffer.data());
return true;
}
......
......@@ -22,11 +22,14 @@
#include "kritaflake_export.h"
#include <QList>
class QMimeData;
class QString;
class QByteArray;
class KoDragOdfSaveHelper;
class KoDragPrivate;
class KoShape;
/**
* Class for simplifying adding a odf to the clip board
......@@ -50,7 +53,9 @@ public:
* @param mimeType used for creating the odf document
* @param helper helper for saving the body of the odf document
*/
bool setOdf(const char *mimeType, KoDragOdfSaveHelper &helper);
bool setOdf(const char *, KoDragOdfSaveHelper &);
bool setSvg(const QList<KoShape*> shapes);
/**
* Add additional mimeTypes
......
......@@ -454,6 +454,15 @@ QRectF KoShape::boundingRect() const
return bb;
}
QRectF KoShape::boundingRect(const QList<KoShape *> &shapes)
{
QRectF boundingRect;
Q_FOREACH (KoShape *shape, shapes) {
boundingRect |= shape->boundingRect();
}
return boundingRect;
}
QTransform KoShape::absoluteTransformation(const KoViewConverter *converter) const
{
Q_D(const KoShape);
......
......@@ -331,6 +331,12 @@ public:
*/
virtual QRectF boundingRect() const;
/**
* Get the united bounding box of a group of shapes. This is a utility
* function used in many places in Krita.
*/
static QRectF boundingRect(const QList<KoShape*> &shapes);
/**
* @brief Add a connector point to the shape
*
......
......@@ -19,6 +19,7 @@
* Boston, MA 02110-1301, USA.
*/
#include <QTransform>
#include "KoShapeBasedDocumentBase.h"
#include "KoDocumentResourceManager.h"
#include "KoShapeRegistry.h"
......@@ -41,17 +42,15 @@ public:
}
// read persistent application wide resources
KSharedConfigPtr config = KSharedConfig::openConfig();
if (config->hasGroup("Misc")) {
KConfigGroup miscGroup = config->group("Misc");
const qreal pasteOffset = miscGroup.readEntry("CopyOffset", 10.0);
resourceManager->setPasteOffset(pasteOffset);
const bool pasteAtCursor = miscGroup.readEntry("PasteAtCursor", true);
resourceManager->enablePasteAtCursor(pasteAtCursor);
const uint grabSensitivity = miscGroup.readEntry("GrabSensitivity", 3);
resourceManager->setGrabSensitivity(grabSensitivity);
const uint handleRadius = miscGroup.readEntry("HandleRadius", 3);
resourceManager->setHandleRadius(handleRadius);
}
KConfigGroup miscGroup = config->group("Misc");
const qreal pasteOffset = miscGroup.readEntry("CopyOffset", 10.0);
resourceManager->setPasteOffset(pasteOffset);
const bool pasteAtCursor = miscGroup.readEntry("PasteAtCursor", true);
resourceManager->enablePasteAtCursor(pasteAtCursor);
const uint grabSensitivity = miscGroup.readEntry("GrabSensitivity", 3);
resourceManager->setGrabSensitivity(grabSensitivity);
const uint handleRadius = miscGroup.readEntry("HandleRadius", 3);
resourceManager->setHandleRadius(handleRadius);
}
~KoShapeBasedDocumentBasePrivate()
......@@ -80,3 +79,11 @@ KoDocumentResourceManager *KoShapeBasedDocumentBase::resourceManager() const
{
return d->resourceManager;
}
QRectF KoShapeBasedDocumentBase::documentRect() const
{
const qreal pxToPt = 72.0 / pixelsPerInch();
QTransform t = QTransform::fromScale(pxToPt, pxToPt);
return t.mapRect(documentRectInPixels());
}
......@@ -27,6 +27,7 @@
#include <QList>
class QRectF;
class KoShape;
class KoShapeBasedDocumentBasePrivate;
class KoDocumentResourceManager;
......@@ -78,6 +79,22 @@ public:
*/
virtual KoDocumentResourceManager *resourceManager() const;
/**
* The size of the document measured in rasterized pixels. This information is needed for loading
* SVG documents that use 'px' as the default unit.
*/
virtual QRectF documentRectInPixels() const = 0;
/**
* The size of the document measured in 'pt'
*/
QRectF documentRect() const;
/**
* Resolution of the rasterized representaiton of the document. Used to load SVG documents correctly.
*/
virtual qreal pixelsPerInch() const = 0;
private:
KoShapeBasedDocumentBasePrivate * const d;
};
......
......@@ -172,6 +172,21 @@ void KoShapeController::setShapeControllerBase(KoShapeBasedDocumentBase *shapeBa
d->shapeBasedDocument = shapeBasedDocument;
}
QRectF KoShapeController::documentRectInPixels() const
{
return d->shapeBasedDocument ? d->shapeBasedDocument->documentRectInPixels() : QRectF(0,0,1920,1080);
}
qreal KoShapeController::pixelsPerInch() const
{
return d->shapeBasedDocument ? d->shapeBasedDocument->pixelsPerInch() : 72.0;
}
QRectF KoShapeController::documentRect() const
{
return d->shapeBasedDocument ? d->shapeBasedDocument->documentRect() : documentRectInPixels();
}
KoDocumentResourceManager *KoShapeController::resourceManager() const
{
if (!d->shapeBasedDocument)
......
......@@ -110,6 +110,22 @@ public:
*/
void setShapeControllerBase(KoShapeBasedDocumentBase *shapeBasedDocument);
/**
* The size of the document measured in rasterized pixels. This information is needed for loading
* SVG documents that use 'px' as the default unit.
*/
QRectF documentRectInPixels() const;
/**
* Resolution of the rasterized representaiton of the document. Used to load SVG documents correctly.
*/
qreal pixelsPerInch() const;
/**
* Document rect measured in 'pt'
*/
QRectF documentRect() const;
/**
* Return a pointer to the resource manager associated with the
* shape-set (typically a document). The resource manager contains
......
/*
* Copyright (c) 2017 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "KoSvgPaste.h"
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <SvgParser.h>
#include <KoDocumentResourceManager.h>
#include <KoXmlReader.h>
#include <FlakeDebug.h>
#include <QRectF>
KoSvgPaste::KoSvgPaste()
{
}
bool KoSvgPaste::hasShapes() const
{
const QMimeData *mimeData = QApplication::clipboard()->mimeData();
return mimeData && mimeData->hasFormat("image/svg+xml");
}
QList<KoShape*> KoSvgPaste::fetchShapes(const QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize) const
{
QList<KoShape*> shapes;
const QMimeData *mimeData = QApplication::clipboard()->mimeData();
if (!mimeData) return shapes;
QByteArray data = mimeData->data("image/svg+xml");
if (data.isEmpty()) return shapes;
KoXmlDocument doc;
QString errorMsg;
int errorLine = 0;
int errorColumn = 0;
const bool documentValid = doc.setContent(data, false, &errorMsg, &errorLine, &errorColumn);
if (!documentValid) {
errorFlake << "Failed to process an SVG file at"
<< errorLine << ":" << errorColumn << "->" << errorMsg;
return shapes;
}
KoDocumentResourceManager resourceManager;
SvgParser parser(&resourceManager);
parser.setResolution(viewportInPx, resolutionPPI);
shapes = parser.parseSvg(doc.documentElement(), fragmentSize);
return shapes;
}
/*
* Copyright (c) 2017 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef KOSVGPASTE_H
#define KOSVGPASTE_H
#include "kritaflake_export.h"
#include <QList>
class KoShape;
class QRectF;
class QSizeF;
class KRITAFLAKE_EXPORT KoSvgPaste
{
public:
KoSvgPaste();
bool hasShapes() const;
QList<KoShape*> fetchShapes(const QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize = 0) const;
};
#endif // KOSVGPASTE_H
......@@ -44,6 +44,8 @@
#include "kis_action_registry.h"
#include "KoToolFactoryBase.h"
#include <krita_container_utils.h>
// Qt + kde
#include <QWidget>
#include <QEvent>
......@@ -374,13 +376,17 @@ KoCanvasController *KoToolManager::activeCanvasController() const
QString KoToolManager::preferredToolForSelection(const QList<KoShape*> &shapes)
{
QList<QString> types;
Q_FOREACH (KoShape *shape, shapes)
if (! types.contains(shape->shapeId()))
types.append(shape->shapeId());
Q_FOREACH (KoShape *shape, shapes) {
types << shape->shapeId();
}
KritaUtils::makeContainerUnique(types);
QString toolType = KoInteractionTool_ID;
int prio = INT_MAX;
Q_FOREACH (ToolHelper *helper, d->tools) {
if (helper->id() == KoCreateShapesTool_ID) continue;
if (helper->priority() >= prio)
continue;
......
......@@ -50,6 +50,12 @@
#include "KoViewConverter.h"
#include "KoShapeFactoryBase.h"
#include <KoSvgPaste.h>
#include <KoSelectedShapesProxy.h>
#include "kis_algebra_2d.h"
#include <KoShapeMoveCommand.h>
#include <KoViewConverter.h>
KoToolProxyPrivate::KoToolProxyPrivate(KoToolProxy *p)
: activeTool(0),
......@@ -456,27 +462,127 @@ void KoToolProxy::copy() const
d->activeTool->copy();
}
bool KoToolProxy::paste()
namespace {
QPointF getFittingOffset(QList<KoShape*> shapes,
const QPointF &shapesOffset,
const QRectF &documentRect,
const qreal fitRatio)
{
QPointF accumulatedFitOffset;
Q_FOREACH (KoShape *shape, shapes) {
const QRectF bounds = shape->boundingRect();
const QPointF center = bounds.center() + shapesOffset;
const qreal wMargin = (0.5 - fitRatio) * bounds.width();
const qreal hMargin = (0.5 - fitRatio) * bounds.height();
const QRectF allowedRect = documentRect.adjusted(-wMargin, -hMargin, wMargin, hMargin);
const QPointF fittedCenter = KisAlgebra2D::clampPoint(center, allowedRect);
accumulatedFitOffset += fittedCenter - center;
}
return accumulatedFitOffset;
}
}
bool KoToolProxy::paste(bool pasteAtCursorPosition)
{
bool success = false;
KoCanvasBase *canvas = d->controller->canvas();
if (d->activeTool && d->isActiveLayerEditable())
if (d->activeTool && d->isActiveLayerEditable()) {
success = d->activeTool->paste();
}
if (!success) {
const QMimeData *data = QApplication::clipboard()->mimeData();
KoSvgPaste paste;
if (!success && paste.hasShapes()) {
QSizeF fragmentSize;
QList<KoShape*> shapes =
paste.fetchShapes(canvas->shapeController()->documentRectInPixels(),
canvas->shapeController()->pixelsPerInch(), &fragmentSize);
if (data->hasFormat(KoOdf::mimeType(KoOdf::Text))) {
if (!shapes.isEmpty()) {
KoShapeManager *shapeManager = canvas->shapeManager();
KoShapePaste paste(canvas, shapeManager->selection()->activeLayer());
success = paste.paste(KoOdf::Text, data);
if (success) {
shapeManager->selection()->deselectAll();
Q_FOREACH (KoShape *shape, paste.pastedShapes()) {
shapeManager->selection()->select(shape);
shapeManager->selection()->deselectAll();
KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18n("Paste shapes"));
Q_FOREACH (KoShape *shape, shapes) {
canvas->shapeController()->addShapeDirect(shape, parentCommand);
}
QPointF finalShapesOffset;
if (pasteAtCursorPosition) {
QRectF boundingRect = KoShape::boundingRect(shapes);
const QPointF cursorPos = canvas->canvasController()->currentCursorPosition();
finalShapesOffset = cursorPos - boundingRect.center();
} else {
bool foundOverlapping = false;
QRectF boundingRect = KoShape::boundingRect(shapes);
const QPointF offsetStep = 0.1 * QPointF(boundingRect.width(), boundingRect.height());
QPointF offset;
Q_FOREACH (KoShape *shape, shapes) {
QRectF br1 = shape->boundingRect();
bool hasOverlappingShape = false;
do {
hasOverlappingShape = false;
// we cannot use shapesAt() here, because the groups are not
// handled in the shape manager's tree
QList<KoShape*> conflicts = shapeManager->shapes();
Q_FOREACH (KoShape *intersectedShape, conflicts) {
if (intersectedShape == shape) continue;
QRectF br2 = intersectedShape->boundingRect();
const qreal tolerance = 2.0; /* pt */
if (KisAlgebra2D::fuzzyCompareRects(br1, br2, tolerance)) {
br1.translate(offsetStep.x(), offsetStep.y());
offset += offsetStep;
hasOverlappingShape = true;
foundOverlapping = true;
break;
}
}
} while (hasOverlappingShape);
if (foundOverlapping) break;
}
if (foundOverlapping) {
finalShapesOffset = offset;
}
}
const QRectF documentRect = canvas->shapeController()->documentRect();
finalShapesOffset += getFittingOffset(shapes, finalShapesOffset, documentRect, 0.1);
if (!finalShapesOffset.isNull()) {
new KoShapeMoveCommand(shapes, finalShapesOffset, parentCommand);
}
canvas->addCommand(parentCommand);
Q_FOREACH (KoShape *shape, shapes) {
canvas->selectedShapesProxy()->selection()->select(shape);
}
success = true;
}
}
......
......@@ -142,7 +142,7 @@ public:
void copy() const;
/// Forwarded to the current KoToolBase
bool paste();
bool paste(bool pasteAtCursorPosition = false);
/// Forwarded to the current KoToolBase
QStringList supportedPasteMimeTypes() const;
......
......@@ -33,7 +33,7 @@ public:
};
KoShapeMoveCommand::KoShapeMoveCommand(const QList<KoShape*> &shapes, QList<QPointF> &previousPositions, QList<QPointF> &newPositions, KoFlake::AnchorPosition anchor, KUndo2Command *parent)
: KUndo2Command(parent),
: KUndo2Command(kundo2_i18n("Move shapes"), parent),
d(new Private())
{
d->shapes = shapes;
......@@ -42,8 +42,21 @@ KoShapeMoveCommand::KoShapeMoveCommand(const QList<KoShape*> &shapes, QList<QPoi
d->anchor = anchor;
Q_ASSERT(d->shapes.count() == d->previousPositions.count());
Q_ASSERT(d->shapes.count() == d->newPositions.count());
}
KoShapeMoveCommand::KoShapeMoveCommand(const QList<KoShape *> &shapes, const QPointF &offset, KUndo2Command *parent)
: KUndo2Command(kundo2_i18n("Move shapes"), parent),
d(new Private())
{
d->shapes = shapes;
d->anchor = KoFlake::Center;
Q_FOREACH (KoShape *shape, d->shapes) {