Commit 36145fb1 authored by Dmitry Kazakov's avatar Dmitry Kazakov

[FEATURE] Implement 4-point perspective transform mode

Just select a third mode in the transform tool and it'll be yours! :)

CCMAIL:kimageshop@kde.rog
parent 7084ffda
......@@ -51,14 +51,26 @@ KisPerspectiveTransformWorker::KisPerspectiveTransformWorker(KisPaintDeviceSP de
QTransform forwardTransform = t.inverted() * project * t;
QPolygon bounds(dev->exactBounds());
QPolygon newBounds = forwardTransform.map(bounds);
init(forwardTransform);
}
KisPerspectiveTransformWorker::KisPerspectiveTransformWorker(KisPaintDeviceSP dev, const QTransform &transform, KoUpdaterPtr progress)
: m_dev(dev), m_progressUpdater(progress)
{
init(transform);
}
void KisPerspectiveTransformWorker::init(const QTransform &transform)
{
QPolygon bounds(m_dev->exactBounds());
QPolygon newBounds = transform.map(bounds);
m_isIdentity = forwardTransform.isIdentity();
m_isIdentity = transform.isIdentity();
if (!m_isIdentity && forwardTransform.isInvertible()) {
m_newTransform = forwardTransform.inverted();
m_srcRect = dev->exactBounds();
if (!m_isIdentity && transform.isInvertible()) {
m_newTransform = transform.inverted();
m_srcRect = m_dev->exactBounds();
newBounds = newBounds.intersected(m_dev->defaultBounds()->bounds());
QPainterPath path;
path.addPolygon(newBounds);
......
......@@ -36,12 +36,16 @@ class KRITAIMAGE_EXPORT KisPerspectiveTransformWorker : public QObject
Q_OBJECT
public:
//KisPerspectiveTransformWorker(KisPaintDeviceSP dev, KisSelectionSP selection, const QPointF& topLeft, const QPointF& topRight, const QPointF& bottomLeft, const QPointF& bottomRight, KoUpdaterPtr progress);
KisPerspectiveTransformWorker(KisPaintDeviceSP dev, QPointF center, double aX, double aY, double distance, KoUpdaterPtr progress);
KisPerspectiveTransformWorker(KisPaintDeviceSP dev, const QTransform &transform, KoUpdaterPtr progress);
~KisPerspectiveTransformWorker();
void run();
private:
void init(const QTransform &transform);
private:
KisPaintDeviceSP m_dev;
KoUpdaterPtr m_progressUpdater;
......
......@@ -8,6 +8,7 @@ set(kritatooltransform_PART_SRCS
kis_warp_transform_strategy.cpp
kis_free_transform_strategy.cpp
kis_free_transform_strategy_gsl_helpers.cpp
kis_perspective_transform_strategy.cpp
kis_transform_utils.cpp
strokes/transform_stroke_strategy.cpp
)
......
......@@ -57,13 +57,11 @@ struct KisFreeTransformStrategy::Private
Private(KisFreeTransformStrategy *_q,
const KisCoordinatesConverter *_converter,
ToolTransformArgs &_currentArgs,
TransformTransactionProperties &_transaction,
QTransform &_transform)
TransformTransactionProperties &_transaction)
: q(_q),
converter(_converter),
currentArgs(_currentArgs),
transaction(_transaction),
transform(_transform),
imageTooBig(false)
{
scaleCursors[0] = KisCursor::sizeHorCursor();
......@@ -118,7 +116,7 @@ struct KisFreeTransformStrategy::Private
};
HandlePoints transformedHandles;
QTransform &transform;
QTransform transform;
QCursor scaleCursors[8]; // cursors for the 8 directions
QPixmap shearCursorPixmap;
......@@ -136,9 +134,8 @@ struct KisFreeTransformStrategy::Private
KisFreeTransformStrategy::KisFreeTransformStrategy(const KisCoordinatesConverter *converter,
ToolTransformArgs &currentArgs,
TransformTransactionProperties &transaction,
QTransform &transform)
: m_d(new Private(this, converter, currentArgs, transaction, transform))
TransformTransactionProperties &transaction)
: m_d(new Private(this, converter, currentArgs, transaction))
{
}
......
......@@ -39,8 +39,7 @@ class KisFreeTransformStrategy : public KisTransformStrategyBase
public:
KisFreeTransformStrategy(const KisCoordinatesConverter *converter,
ToolTransformArgs &currentArgs,
TransformTransactionProperties &transaction,
QTransform &transform);
TransformTransactionProperties &transaction);
~KisFreeTransformStrategy();
void setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive);
......
/*
* Copyright (c) 2014 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 __KIS_PERSPECTIVE_TRANSFORM_STRATEGY_H
#define __KIS_PERSPECTIVE_TRANSFORM_STRATEGY_H
#include <QObject>
#include <QScopedPointer>
#include "kis_transform_strategy_base.h"
class QPointF;
class QPainter;
class KisCoordinatesConverter;
class ToolTransformArgs;
class QTransform;
class TransformTransactionProperties;
class QCursor;
class QImage;
class KisPerspectiveTransformStrategy : public KisTransformStrategyBase
{
Q_OBJECT
public:
KisPerspectiveTransformStrategy(const KisCoordinatesConverter *converter,
ToolTransformArgs &currentArgs,
TransformTransactionProperties &transaction);
~KisPerspectiveTransformStrategy();
void setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive);
void paint(QPainter &gc);
QCursor getCurrentCursor() const;
void externalConfigChanged();
bool beginPrimaryAction(const QPointF &pt);
void continuePrimaryAction(const QPointF &pt, bool specialModifierActve);
bool endPrimaryAction();
signals:
void requestCanvasUpdate();
void requestResetRotationCenterButtons();
void requestShowImageTooBig(bool value);
private:
class Private;
const QScopedPointer<Private> m_d;
};
#endif /* __KIS_PERSPECTIVE_TRANSFORM_STRATEGY_H */
......@@ -78,11 +78,10 @@
#include "widgets/kis_progress_widget.h"
#include "kis_transform_utils.h"
#include "kis_warp_transform_strategy.h"
#include "kis_free_transform_strategy.h"
#include <Eigen/Geometry>
using namespace Eigen;
#include "kis_perspective_transform_strategy.h"
#include "strokes/transform_stroke_strategy.h"
......@@ -98,7 +97,11 @@ KisToolTransform::KisToolTransform(KoCanvasBase * canvas)
, m_freeStrategy(
new KisFreeTransformStrategy(
dynamic_cast<KisCanvas2*>(canvas)->coordinatesConverter(),
m_currentArgs, m_transaction, m_transform))
m_currentArgs, m_transaction))
, m_perspectiveStrategy(
new KisPerspectiveTransformStrategy(
dynamic_cast<KisCanvas2*>(canvas)->coordinatesConverter(),
m_currentArgs, m_transaction))
, m_currentStrategy(0)
{
m_canvas = dynamic_cast<KisCanvas2*>(canvas);
......@@ -112,7 +115,8 @@ KisToolTransform::KisToolTransform(KoCanvasBase * canvas)
connect(m_freeStrategy.data(), SIGNAL(requestCanvasUpdate()), SLOT(canvasUpdateRequested()));
connect(m_freeStrategy.data(), SIGNAL(requestResetRotationCenterButtons()), SLOT(resetRotationCenterButtonsRequested()));
connect(m_freeStrategy.data(), SIGNAL(requestShowImageTooBig(bool)), SLOT(imageTooBigRequested(bool)));
m_currentStrategy = m_freeStrategy.data();
connect(m_perspectiveStrategy.data(), SIGNAL(requestCanvasUpdate()), SLOT(canvasUpdateRequested()));
m_currentStrategy = m_perspectiveStrategy.data();
connect(&m_changesTracker, SIGNAL(sigConfigChanged()),
this, SLOT(slotTrackerChangedConfig()));
......@@ -152,6 +156,8 @@ void KisToolTransform::strategyTypeSanityCheck()
KIS_ASSERT_RECOVER_NOOP(m_currentStrategy == m_freeStrategy.data());
} else if (m_currentArgs.mode() == ToolTransformArgs::WARP) {
KIS_ASSERT_RECOVER_NOOP(m_currentStrategy == m_warpStrategy.data());
} else if (m_currentArgs.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
KIS_ASSERT_RECOVER_NOOP(m_currentStrategy == m_perspectiveStrategy.data());
}
}
......@@ -161,6 +167,12 @@ void KisToolTransform::paint(QPainter& gc, const KoViewConverter &converter)
if (!m_strokeData.strokeId()) return;
QRectF newRefRect = KisTransformUtils::imageToFlake(m_canvas->coordinatesConverter(), QRectF(0.0,0.0,1.0,1.0));
if (m_refRect != newRefRect) {
m_refRect = newRefRect;
m_currentStrategy->externalConfigChanged();
}
gc.save();
if (m_optionsWidget && m_optionsWidget->showDecorations()) {
gc.setOpacity(0.3);
......@@ -292,7 +304,24 @@ bool KisToolTransform::isActive() const
KisToolTransform::TransformToolMode KisToolTransform::transformMode() const
{
return m_currentArgs.mode() == ToolTransformArgs::FREE_TRANSFORM ? FreeTransformMode : WarpTransformMode;
TransformToolMode mode = FreeTransformMode;
switch (m_currentArgs.mode())
{
case ToolTransformArgs::FREE_TRANSFORM:
mode = FreeTransformMode;
break;
case ToolTransformArgs::WARP:
mode = WarpTransformMode;
break;
case ToolTransformArgs::PERSPECTIVE_4POINT:
mode = PerspectiveTransformMode;
break;
default:
KIS_ASSERT_RECOVER_NOOP(0 && "unexpected transform mode");
}
return mode;
}
double KisToolTransform::translateX() const
......@@ -366,12 +395,29 @@ int KisToolTransform::warpPointDensity() const
void KisToolTransform::setTransformMode(KisToolTransform::TransformToolMode newMode)
{
ToolTransformArgs::TransformMode mode = newMode == FreeTransformMode ? ToolTransformArgs::FREE_TRANSFORM : ToolTransformArgs::WARP;
ToolTransformArgs::TransformMode mode = ToolTransformArgs::FREE_TRANSFORM;
switch (newMode) {
case FreeTransformMode:
mode = ToolTransformArgs::FREE_TRANSFORM;
break;
case WarpTransformMode:
mode = ToolTransformArgs::WARP;
break;
case PerspectiveTransformMode:
mode = ToolTransformArgs::PERSPECTIVE_4POINT;
break;
default:
KIS_ASSERT_RECOVER_NOOP(0 && "unexpected transform mode");
}
if( mode != m_currentArgs.mode() ) {
if( newMode == FreeTransformMode ) {
m_optionsWidget->slotSetFreeTransformModeButtonClicked( true );
} else {
} else if( newMode == WarpTransformMode ) {
m_optionsWidget->slotSetWrapModeButtonClicked( true );
} else if( newMode == PerspectiveTransformMode ) {
m_optionsWidget->slotSetPerspectiveModeButtonClicked( true );
}
emit transformModeChanged();
}
......@@ -467,13 +513,20 @@ void KisToolTransform::initTransformMode(ToolTransformArgs::TransformMode mode)
// levels.
QString filterId = m_currentArgs.filterId();
m_currentArgs = ToolTransformArgs();
m_currentArgs.setOriginalCenter(m_transaction.originalCenter());
m_currentArgs.setTransformedCenter(m_transaction.originalCenter());
if (mode == ToolTransformArgs::FREE_TRANSFORM) {
m_currentStrategy = m_freeStrategy.data();
m_currentArgs = ToolTransformArgs(ToolTransformArgs::FREE_TRANSFORM, m_transaction.originalCenter(), m_transaction.originalCenter(), QPointF(),0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, KisWarpTransformWorker::RIGID_TRANSFORM, 1.0, true, filterId);
} else /* if (mode == ToolTransformArgs::WARP) */ {
m_currentArgs.setMode(ToolTransformArgs::FREE_TRANSFORM);
} else if (mode == ToolTransformArgs::WARP) {
m_currentStrategy = m_warpStrategy.data();
m_currentArgs = ToolTransformArgs(ToolTransformArgs::WARP, m_transaction.originalCenter(), m_transaction.originalCenter(), QPointF(0, 0), 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, KisWarpTransformWorker::RIGID_TRANSFORM, 1.0, true, filterId);
m_currentArgs.setMode(ToolTransformArgs::WARP);
m_optionsWidget->setDefaultWarpPoints();
} else if (mode == ToolTransformArgs::PERSPECTIVE_4POINT) {
m_currentStrategy = m_perspectiveStrategy.data();
m_currentArgs.setMode(ToolTransformArgs::PERSPECTIVE_4POINT);
}
m_currentStrategy->externalConfigChanged();
......@@ -506,7 +559,6 @@ void KisToolTransform::updateSelectionPath()
void KisToolTransform::initThumbnailImage(KisPaintDeviceSP previewDevice)
{
m_transform = QTransform();
QImage origImg;
m_selectedPortionCache = previewDevice;
......@@ -542,6 +594,7 @@ void KisToolTransform::initThumbnailImage(KisPaintDeviceSP previewDevice)
// init both strokes since the thumbnail is initialized only once
// during the stroke
m_freeStrategy->setThumbnailImage(origImg, thumbToImageTransform);
m_perspectiveStrategy->setThumbnailImage(origImg, thumbToImageTransform);
m_warpStrategy->setThumbnailImage(origImg, thumbToImageTransform);
}
......
......@@ -55,9 +55,11 @@ class KisFilterStrategy;
class KisCanvas2;
class QTouchEvent;
class KisTransformStrategyBase;
class KisWarpTransformStrategy;
class KisFreeTransformStrategy;
class KisTransformStrategyBase;
class KisPerspectiveTransformStrategy;
/**
* Transform tool
......@@ -104,7 +106,8 @@ class KisToolTransform : public KisTool
public:
enum TransformToolMode {
FreeTransformMode,
WarpTransformMode
WarpTransformMode,
PerspectiveTransformMode
};
Q_ENUMS(TransformToolMode)
......@@ -219,7 +222,6 @@ private:
bool m_actuallyMoveWhileSelected; // true <=> selection has been moved while clicked
QTransform m_transform; // transformation to apply on origImg
KisPaintDeviceSP m_selectedPortionCache;
struct StrokeData {
......@@ -253,8 +255,16 @@ private:
TransformTransactionProperties m_transaction;
TransformChangesTracker m_changesTracker;
/**
* This artificial rect is used to store the image to flake
* transformation. We check against this rect to get to know
* whether zoom has changed.
*/
QRectF m_refRect;
QScopedPointer<KisWarpTransformStrategy> m_warpStrategy;
QScopedPointer<KisFreeTransformStrategy> m_freeStrategy;
QScopedPointer<KisPerspectiveTransformStrategy> m_perspectiveStrategy;
KisTransformStrategyBase *m_currentStrategy;
void strategyTypeSanityCheck();
......
......@@ -159,6 +159,7 @@ KisToolTransformConfigWidget::KisToolTransformConfigWidget(TransformTransactionP
// Mode switch buttons
connect(freeTransformButton, SIGNAL(clicked(bool)), this, SLOT(slotSetFreeTransformModeButtonClicked(bool)));
connect(warpButton, SIGNAL(clicked(bool)), this, SLOT(slotSetWrapModeButtonClicked(bool)));
connect(perspectiveTransformButton, SIGNAL(clicked(bool)), this, SLOT(slotSetPerspectiveModeButtonClicked(bool)));
// Connect Decorations switcher
connect(showDecorationsBox, SIGNAL(toggled(bool)), canvas, SLOT(updateCanvas()));
......@@ -195,10 +196,24 @@ void KisToolTransformConfigWidget::updateConfig(const ToolTransformArgs &config)
{
blockUiSlots();
if (config.mode() == ToolTransformArgs::FREE_TRANSFORM) {
if (config.mode() == ToolTransformArgs::FREE_TRANSFORM ||
config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
stackedWidget->setCurrentIndex(0);
freeTransformButton->setChecked(true);
bool freeTransformIsActive = config.mode() == ToolTransformArgs::FREE_TRANSFORM;
freeTransformButton->setChecked(freeTransformIsActive);
perspectiveTransformButton->setChecked(!freeTransformIsActive);
warpButton->setChecked(false);
aXBox->setEnabled(freeTransformIsActive);
aYBox->setEnabled(freeTransformIsActive);
aZBox->setEnabled(freeTransformIsActive);
foreach (QAbstractButton *button, m_rotationCenterButtons->buttons()) {
button->setEnabled(freeTransformIsActive);
}
scaleXBox->setValue(config.scaleX() * 100.);
scaleYBox->setValue(config.scaleY() * 100.);
shearXBox->setValue(config.shearX());
......@@ -354,6 +369,13 @@ void KisToolTransformConfigWidget::slotSetWrapModeButtonClicked(bool)
emit sigResetTransform();
}
void KisToolTransformConfigWidget::slotSetPerspectiveModeButtonClicked(bool)
{
ToolTransformArgs *config = m_transaction->currentConfig();
config->setMode(ToolTransformArgs::PERSPECTIVE_4POINT);
emit sigResetTransform();
}
void KisToolTransformConfigWidget::slotFilterChanged(const KoID &filterId)
{
ToolTransformArgs *config = m_transaction->currentConfig();
......
......@@ -81,6 +81,7 @@ public slots:
void slotSetFreeTransformModeButtonClicked(bool);
void slotSetWrapModeButtonClicked(bool);
void slotSetPerspectiveModeButtonClicked(bool);
void slotButtonBoxClicked(QAbstractButton *button);
void notifyEditingFinished();
......
......@@ -103,10 +103,15 @@ KisTransformUtils::MatricesPack::MatricesPack(const ToolTransformArgs &args)
SC = QTransform::fromScale(args.scaleX(), args.scaleY());
S.shear(0, args.shearY()); S.shear(args.shearX(), 0);
P.rotate(180. * args.aX() / M_PI, QVector3D(1, 0, 0));
P.rotate(180. * args.aY() / M_PI, QVector3D(0, 1, 0));
P.rotate(180. * args.aZ() / M_PI, QVector3D(0, 0, 1));
projectedP = P.toTransform(args.cameraPos().z());
if (args.mode() == ToolTransformArgs::FREE_TRANSFORM) {
P.rotate(180. * args.aX() / M_PI, QVector3D(1, 0, 0));
P.rotate(180. * args.aY() / M_PI, QVector3D(0, 1, 0));
P.rotate(180. * args.aZ() / M_PI, QVector3D(0, 0, 1));
projectedP = P.toTransform(args.cameraPos().z());
} else if (args.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
projectedP = args.flattenedPerspectiveTransform();
P = QMatrix4x4(projectedP);
}
QPointF translation = args.transformedCenter();
T = QTransform::fromTranslate(translation.x(), translation.y());
......
......@@ -42,6 +42,11 @@ public:
return converter->documentToImage(converter->flakeToDocument(object));
}
template <class T>
static T imageToFlake(const KisCoordinatesConverter *converter, T object) {
return converter->documentToFlake(converter->imageToDocument(object));
}
static QTransform imageToFlakeTransform(const KisCoordinatesConverter *converter);
static qreal effectiveHandleGrabRadius(const KisCoordinatesConverter *converter);
......
......@@ -272,12 +272,23 @@ void TransformStrokeStrategy::transformDevice(const ToolTransformArgs &config,
transformWorker.run();
KisPerspectiveTransformWorker perspectiveWorker(device,
config.transformedCenter(),
config.aX(),
config.aY(),
config.cameraPos().z(),
updater2);
perspectiveWorker.run();
if (config.mode() == ToolTransformArgs::FREE_TRANSFORM) {
KisPerspectiveTransformWorker perspectiveWorker(device,
config.transformedCenter(),
config.aX(),
config.aY(),
config.cameraPos().z(),
updater2);
perspectiveWorker.run();
} else if (config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
QTransform T =
QTransform::fromTranslate(config.transformedCenter().x(),
config.transformedCenter().y());
KisPerspectiveTransformWorker perspectiveWorker(device,
T.inverted() * config.flattenedPerspectiveTransform() * T,
updater2);
perspectiveWorker.run();
}
}
}
......@@ -37,7 +37,7 @@ ToolTransformArgs::ToolTransformArgs()
m_shearY = 0.0;
m_origPoints = QVector<QPointF>();
m_transfPoints = QVector<QPointF>();
m_warpType = KisWarpTransformWorker::AFFINE_TRANSFORM;
m_warpType = KisWarpTransformWorker::RIGID_TRANSFORM;
m_alpha = 1.0;
m_keepAspectRatio = false;
......@@ -66,6 +66,7 @@ void ToolTransformArgs::init(const ToolTransformArgs& args)
m_defaultPoints = args.defaultPoints();
m_keepAspectRatio = args.keepAspectRatio();
m_filter = args.m_filter;
m_flattenedPerspectiveTransform = args.m_flattenedPerspectiveTransform;
}
void ToolTransformArgs::clear()
......@@ -134,12 +135,19 @@ bool ToolTransformArgs::isIdentity() const
return (m_transformedCenter == m_originalCenter && m_scaleX == 1
&& m_scaleY == 1 && m_shearX == 0 && m_shearY == 0
&& m_aX == 0 && m_aY == 0 && m_aZ == 0);
} else {
} else if (m_mode == PERSPECTIVE_4POINT) {
return (m_transformedCenter == m_originalCenter && m_scaleX == 1
&& m_scaleY == 1 && m_shearX == 0 && m_shearY == 0
&& m_flattenedPerspectiveTransform.isIdentity());
} else if(m_mode == WARP) {
for (int i = 0; i < m_origPoints.size(); ++i)
if (m_origPoints[i] != m_transfPoints[i])
return false;
return true;
} else {
KIS_ASSERT_RECOVER_NOOP(0 && "unknown transform mode");
return true;
}
}
......@@ -37,7 +37,8 @@ class ToolTransformArgs
{
public:
enum TransformMode {FREE_TRANSFORM = 0,
WARP};
WARP,
PERSPECTIVE_4POINT};
/**
* Initializes the parameters for an identity transformation,
......@@ -221,6 +222,14 @@ public:
bool isIdentity() const;
inline QTransform flattenedPerspectiveTransform() const {
return m_flattenedPerspectiveTransform;
}
inline void setFlattenedPerspectiveTransform(const QTransform &value) {
m_flattenedPerspectiveTransform = value;
}
private:
void clear();
void init(const ToolTransformArgs& args);
......@@ -253,6 +262,8 @@ private:
double m_shearY;
bool m_keepAspectRatio;
QTransform m_flattenedPerspectiveTransform;
KisFilterStrategy *m_filter;
};
......
......@@ -115,6 +115,22 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="perspectiveTransformButton">
<property name="text">
<string>Perspective</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
......@@ -974,7 +990,7 @@ big!</string>
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
<string>buttonGroup</string>
</attribute>
</widget>
</item>
......@@ -1040,7 +1056,7 @@ big!</string>
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
<string>buttonGroup</string>
</attribute>
</widget>
</item>
......@@ -1095,7 +1111,7 @@ big!</string>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<property name="spacing">
<number>6</number>
</property>
<property name="topMargin">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment