1 /* This file is part of the KDE project
2  * Copyright (C) 2007 Martin Pfeiffer <hubipete@gmx.net>
3  * Copyright (C) 2007 Jan Hambrecht <jaham@gmx.net>
4    Copyright (C) 2008 Thorsten Zachmann <zachmann@kde.org>
5  * Copyright (C) 2010 Thomas Zander <zander@kde.org>
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public License
18  * along with this library; see the file COPYING.LIB.  If not, write to
19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  */
22 
23 #include "DefaultToolGeometryWidget.h"
24 #include "DefaultTool.h"
25 
26 #include <KoInteractionTool.h>
27 #include <KoCanvasBase.h>
28 #include <KoCanvasResourceProvider.h>
29 #include <KoSelectedShapesProxy.h>
30 #include <KoSelection.h>
31 #include <KoUnit.h>
32 #include <commands/KoShapeResizeCommand.h>
33 #include <commands/KoShapeMoveCommand.h>
34 #include <commands/KoShapeSizeCommand.h>
35 #include <commands/KoShapeTransformCommand.h>
36 #include <commands/KoShapeKeepAspectRatioCommand.h>
37 #include <commands/KoShapeTransparencyCommand.h>
38 #include "SelectionDecorator.h"
39 #include <KoShapeGroup.h>
40 
41 #include "KoAnchorSelectionWidget.h"
42 
43 #include <QAction>
44 #include <QSize>
45 #include <QRadioButton>
46 #include <QLabel>
47 #include <QCheckBox>
48 #include <QDoubleSpinBox>
49 #include <QList>
50 #include <QTransform>
51 #include <kis_algebra_2d.h>
52 
53 #include "kis_aspect_ratio_locker.h"
54 #include "kis_debug.h"
55 #include "kis_acyclic_signal_connector.h"
56 #include "kis_signal_compressor.h"
57 #include "kis_signals_blocker.h"
58 
59 
DefaultToolGeometryWidget(KoInteractionTool * tool,QWidget * parent)60 DefaultToolGeometryWidget::DefaultToolGeometryWidget(KoInteractionTool *tool, QWidget *parent)
61     : QWidget(parent)
62     , m_tool(tool)
63     , m_sizeAspectLocker(new KisAspectRatioLocker())
64     , m_savedUniformScaling(false)
65 {
66     setupUi(this);
67 
68     setUnit(m_tool->canvas()->unit());
69 
70     // Connect and initialize automated aspect locker
71     m_sizeAspectLocker->connectSpinBoxes(widthSpinBox, heightSpinBox, aspectButton);
72     aspectButton->setKeepAspectRatio(false);
73 
74 
75     // TODO: use valueChanged() instead!
76     connect(positionXSpinBox, SIGNAL(valueChangedPt(qreal)), this, SLOT(slotRepositionShapes()));
77     connect(positionYSpinBox, SIGNAL(valueChangedPt(qreal)), this, SLOT(slotRepositionShapes()));
78 
79     KoSelectedShapesProxy *selectedShapesProxy = m_tool->canvas()->selectedShapesProxy();
80 
81     connect(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateCheckboxes()));
82     connect(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdatePositionBoxes()));
83     connect(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateOpacitySlider()));
84 
85     connect(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdatePositionBoxes()));
86     connect(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdateOpacitySlider()));
87 
88     connect(chkGlobalCoordinates, SIGNAL(toggled(bool)), SLOT(slotUpdateSizeBoxes()));
89     connect(chkGlobalCoordinates, SIGNAL(toggled(bool)), SLOT(slotUpdateAspectButton()));
90 
91 
92     /**
93      * A huge block of self-blocking acycled connections
94      */
95     KisAcyclicSignalConnector *acyclicConnector = new KisAcyclicSignalConnector(this);
96     acyclicConnector->connectForwardVoid(m_sizeAspectLocker.data(), SIGNAL(aspectButtonChanged()), this, SLOT(slotAspectButtonToggled()));
97     acyclicConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateAspectButton()));
98     acyclicConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdateAspectButton()));
99 
100     KisAcyclicSignalConnector *sizeConnector = acyclicConnector->createCoordinatedConnector();
101     sizeConnector->connectForwardVoid(m_sizeAspectLocker.data(), SIGNAL(sliderValueChanged()), this, SLOT(slotResizeShapes()));
102     sizeConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateSizeBoxes()));
103 
104     KisAcyclicSignalConnector *contentSizeConnector = acyclicConnector->createCoordinatedConnector();
105     contentSizeConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdateSizeBoxesNoAspectChange()));
106 
107 
108     // Connect and initialize anchor point resource
109     KoCanvasResourceProvider *resourceManager = m_tool->canvas()->resourceManager();
110     connect(resourceManager,
111             SIGNAL(canvasResourceChanged(int,QVariant)),
112             SLOT(resourceChanged(int,QVariant)));
113     resourceManager->setResource(DefaultTool::HotPosition, int(KoFlake::AnchorPosition::Center));
114     positionSelector->setValue(KoFlake::AnchorPosition(resourceManager->resource(DefaultTool::HotPosition).toInt()));
115 
116     // Connect anchor point selector
117     connect(positionSelector, SIGNAL(valueChanged(KoFlake::AnchorPosition)), SLOT(slotAnchorPointChanged()));
118 
119 
120     dblOpacity->setRange(0.0, 1.0, 2);
121     dblOpacity->setSingleStep(0.01);
122     dblOpacity->setFastSliderStep(0.1);
123     dblOpacity->setPrefixes(i18n("Opacity: "), i18n("Opacity [*varies*]: "));
124 
125     dblOpacity->setValueGetter(
126         [](KoShape *s) { return 1.0 - s->transparency(); }
127     );
128 
129     connect(dblOpacity, SIGNAL(valueChanged(qreal)), SLOT(slotOpacitySliderChanged(qreal)));
130 
131     // cold init
132     slotUpdateOpacitySlider();
133 }
134 
~DefaultToolGeometryWidget()135 DefaultToolGeometryWidget::~DefaultToolGeometryWidget()
136 {
137 }
138 
139 namespace {
140 
tryAnchorPosition(KoFlake::AnchorPosition anchor,const QRectF & rect,QPointF * position)141 void tryAnchorPosition(KoFlake::AnchorPosition anchor,
142                        const QRectF &rect,
143                        QPointF *position)
144 {
145     bool valid = false;
146     QPointF anchoredPosition = KoFlake::anchorToPoint(anchor, rect, &valid);
147 
148     if (valid) {
149         *position = anchoredPosition;
150     }
151 }
152 
calculateSelectionBounds(KoSelection * selection,KoFlake::AnchorPosition anchor,bool useGlobalSize,QList<KoShape * > * outShapes=0)153 QRectF calculateSelectionBounds(KoSelection *selection,
154                                 KoFlake::AnchorPosition anchor,
155                                 bool useGlobalSize,
156                                 QList<KoShape*> *outShapes = 0)
157 {
158     QList<KoShape*> shapes = selection->selectedEditableShapes();
159 
160     KoShape *shape = shapes.size() == 1 ? shapes.first() : selection;
161 
162     QRectF resultRect = shape->outlineRect();
163 
164     QPointF resultPoint = resultRect.topLeft();
165     tryAnchorPosition(anchor, resultRect, &resultPoint);
166 
167     if (useGlobalSize) {
168         resultRect = shape->absoluteTransformation().mapRect(resultRect);
169     } else {
170         /**
171          * Some shapes, e.g. KoSelection and KoShapeGroup don't have real size() and
172          * do all the resizing with transformation(), just try to cover this case and
173          * fetch their scale using the transform.
174          */
175 
176         KisAlgebra2D::DecomposedMatix matrix(shape->transformation());
177         resultRect = matrix.scaleTransform().mapRect(resultRect);
178     }
179 
180     resultPoint = shape->absoluteTransformation().map(resultPoint);
181 
182     if (outShapes) {
183         *outShapes = shapes;
184     }
185 
186     return QRectF(resultPoint, resultRect.size());
187 }
188 
189 }
190 
slotAnchorPointChanged()191 void DefaultToolGeometryWidget::slotAnchorPointChanged()
192 {
193     if (!isVisible()) return;
194 
195     QVariant newValue(positionSelector->value());
196     m_tool->canvas()->resourceManager()->setResource(DefaultTool::HotPosition, newValue);
197     slotUpdatePositionBoxes();
198 }
199 
slotUpdateCheckboxes()200 void DefaultToolGeometryWidget::slotUpdateCheckboxes()
201 {
202     if (!isVisible()) return;
203 
204     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
205     QList<KoShape*> shapes = selection->selectedEditableShapes();
206 
207     KoShapeGroup *onlyGroupShape = 0;
208 
209     if (shapes.size() == 1) {
210         onlyGroupShape = dynamic_cast<KoShapeGroup*>(shapes.first());
211     }
212 
213     const bool uniformScalingAvailable = shapes.size() <= 1 && !onlyGroupShape;
214 
215     if (uniformScalingAvailable && !chkUniformScaling->isEnabled()) {
216         chkUniformScaling->setChecked(m_savedUniformScaling);
217         chkUniformScaling->setEnabled(uniformScalingAvailable);
218     } else if (!uniformScalingAvailable && chkUniformScaling->isEnabled()) {
219         m_savedUniformScaling = chkUniformScaling->isChecked();
220         chkUniformScaling->setChecked(true);
221         chkUniformScaling->setEnabled(uniformScalingAvailable);
222     }
223 
224     // TODO: not implemented yet!
225     chkAnchorLock->setEnabled(false);
226 }
227 
slotAspectButtonToggled()228 void DefaultToolGeometryWidget::slotAspectButtonToggled()
229 {
230     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
231     QList<KoShape*> shapes = selection->selectedEditableShapes();
232 
233     KUndo2Command *cmd =
234         new KoShapeKeepAspectRatioCommand(shapes, aspectButton->keepAspectRatio());
235 
236     m_tool->canvas()->addCommand(cmd);
237 }
238 
slotUpdateAspectButton()239 void DefaultToolGeometryWidget::slotUpdateAspectButton()
240 {
241     if (!isVisible()) return;
242 
243     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
244     QList<KoShape*> shapes = selection->selectedEditableShapes();
245 
246     bool hasKeepAspectRatio = false;
247     bool hasNotKeepAspectRatio = false;
248 
249     Q_FOREACH (KoShape *shape, shapes) {
250         if (shape->keepAspectRatio()) {
251             hasKeepAspectRatio = true;
252         } else {
253             hasNotKeepAspectRatio = true;
254         }
255 
256         if (hasKeepAspectRatio && hasNotKeepAspectRatio) break;
257     }
258 
259     Q_UNUSED(hasNotKeepAspectRatio); // TODO: use for tristated mode of the checkbox
260 
261     const bool useGlobalSize = chkGlobalCoordinates->isChecked();
262     const KoFlake::AnchorPosition anchor = positionSelector->value();
263     const QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize);
264     const bool hasNullDimensions = bounds.isEmpty();
265 
266     aspectButton->setKeepAspectRatio(hasKeepAspectRatio && !hasNullDimensions);
267     aspectButton->setEnabled(!hasNullDimensions);
268 }
269 
270 //namespace {
271 //qreal calculateCommonShapeTransparency(const QList<KoShape*> &shapes)
272 //{
273 //    qreal commonTransparency = -1.0;
274 
275 //    Q_FOREACH (KoShape *shape, shapes) {
276 //        if (commonTransparency < 0) {
277 //            commonTransparency = shape->transparency();
278 //        } else if (!qFuzzyCompare(commonTransparency, shape->transparency())) {
279 //            commonTransparency = -1.0;
280 //            break;
281 //        }
282 //    }
283 
284 //    return commonTransparency;
285 //}
286 //}
287 
slotOpacitySliderChanged(qreal newOpacity)288 void DefaultToolGeometryWidget::slotOpacitySliderChanged(qreal newOpacity)
289 {
290     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
291     QList<KoShape*> shapes = selection->selectedEditableShapes();
292     if (shapes.isEmpty()) return;
293 
294     KUndo2Command *cmd =
295         new KoShapeTransparencyCommand(shapes, 1.0 - newOpacity);
296 
297     m_tool->canvas()->addCommand(cmd);
298 }
299 
slotUpdateOpacitySlider()300 void DefaultToolGeometryWidget::slotUpdateOpacitySlider()
301 {
302     if (!isVisible()) return;
303 
304     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
305     QList<KoShape*> shapes = selection->selectedEditableShapes();
306 
307     dblOpacity->setSelection(shapes);
308 }
309 
slotUpdateSizeBoxes(bool updateAspect)310 void DefaultToolGeometryWidget::slotUpdateSizeBoxes(bool updateAspect)
311 {
312     if (!isVisible()) return;
313 
314     const bool useGlobalSize = chkGlobalCoordinates->isChecked();
315     const KoFlake::AnchorPosition anchor = positionSelector->value();
316 
317     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
318     const QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize);
319 
320     const bool hasSizeConfiguration = !bounds.isNull();
321     const bool hasNullDimensions = bounds.isEmpty();
322 
323     widthSpinBox->setEnabled(hasSizeConfiguration && bounds.width() > 0);
324     heightSpinBox->setEnabled(hasSizeConfiguration && bounds.height() > 0);
325 
326     if (hasSizeConfiguration) {
327         KisSignalsBlocker b(widthSpinBox, heightSpinBox);
328         widthSpinBox->changeValue(bounds.width());
329         heightSpinBox->changeValue(bounds.height());
330 
331         if (updateAspect) {
332             m_sizeAspectLocker->updateAspect();
333         }
334     }
335 }
336 
slotUpdateSizeBoxesNoAspectChange()337 void DefaultToolGeometryWidget::slotUpdateSizeBoxesNoAspectChange()
338 {
339     slotUpdateSizeBoxes(false);
340 }
341 
slotUpdatePositionBoxes()342 void DefaultToolGeometryWidget::slotUpdatePositionBoxes()
343 {
344     if (!isVisible()) return;
345 
346     const bool useGlobalSize = chkGlobalCoordinates->isChecked();
347     const KoFlake::AnchorPosition anchor = positionSelector->value();
348 
349     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
350     QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize);
351 
352     const bool hasSizeConfiguration = !bounds.isNull();
353 
354     positionXSpinBox->setEnabled(hasSizeConfiguration);
355     positionYSpinBox->setEnabled(hasSizeConfiguration);
356 
357     if (hasSizeConfiguration) {
358         KisSignalsBlocker b(positionXSpinBox, positionYSpinBox);
359         positionXSpinBox->changeValue(bounds.x());
360         positionYSpinBox->changeValue(bounds.y());
361     }
362 }
363 
slotRepositionShapes()364 void DefaultToolGeometryWidget::slotRepositionShapes()
365 {
366     static const qreal eps = 1e-6;
367 
368     const bool useGlobalSize = chkGlobalCoordinates->isChecked();
369     const KoFlake::AnchorPosition anchor = positionSelector->value();
370 
371     QList<KoShape*> shapes;
372     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
373     QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize, &shapes);
374 
375     if (bounds.isNull()) return;
376 
377     const QPointF oldPosition = bounds.topLeft();
378     const QPointF newPosition(positionXSpinBox->value(), positionYSpinBox->value());
379     const QPointF diff = newPosition - oldPosition;
380 
381     if (diff.manhattanLength() < eps) return;
382 
383     QList<QPointF> oldPositions;
384     QList<QPointF> newPositions;
385 
386     Q_FOREACH (KoShape *shape, shapes) {
387         const QPointF oldShapePosition = shape->absolutePosition(anchor);
388 
389         oldPositions << shape->absolutePosition(anchor);
390         newPositions << oldShapePosition + diff;
391     }
392 
393     KUndo2Command *cmd = new KoShapeMoveCommand(shapes, oldPositions, newPositions, anchor);
394     m_tool->canvas()->addCommand(cmd);
395 }
396 
slotResizeShapes()397 void DefaultToolGeometryWidget::slotResizeShapes()
398 {
399     static const qreal eps = 1e-4;
400 
401     const bool useGlobalSize = chkGlobalCoordinates->isChecked();
402     const KoFlake::AnchorPosition anchor = positionSelector->value();
403 
404     QList<KoShape*> shapes;
405     KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection();
406     QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize, &shapes);
407 
408     if (bounds.isNull()) return;
409 
410     const QSizeF oldSize(bounds.size());
411 
412     QSizeF newSize(widthSpinBox->value(), heightSpinBox->value());
413     newSize = KisAlgebra2D::ensureSizeNotSmaller(newSize, QSizeF(eps, eps));
414 
415     const qreal scaleX = oldSize.width() > 0 ? newSize.width() / oldSize.width() : 1.0;
416     const qreal scaleY = oldSize.height() > 0 ? newSize.height() / oldSize.height() : 1.0;
417 
418     if (qAbs(scaleX - 1.0) < eps && qAbs(scaleY - 1.0) < eps) return;
419 
420     const bool usePostScaling =
421         shapes.size() > 1 || chkUniformScaling->isChecked();
422 
423     KUndo2Command *cmd = new KoShapeResizeCommand(shapes,
424                                                   scaleX, scaleY,
425                                                   bounds.topLeft(),
426                                                   useGlobalSize,
427                                                   usePostScaling,
428                                                   selection->transformation());
429     m_tool->canvas()->addCommand(cmd);
430 }
431 
setUnit(const KoUnit & unit)432 void DefaultToolGeometryWidget::setUnit(const KoUnit &unit)
433 {
434     positionXSpinBox->setUnit(unit);
435     positionYSpinBox->setUnit(unit);
436     widthSpinBox->setUnit(unit);
437     heightSpinBox->setUnit(unit);
438 
439     positionXSpinBox->setLineStep(1.0);
440     positionYSpinBox->setLineStep(1.0);
441     widthSpinBox->setLineStep(1.0);
442     heightSpinBox->setLineStep(1.0);
443 
444     slotUpdatePositionBoxes();
445     slotUpdateSizeBoxes();
446 }
447 
useUniformScaling() const448 bool DefaultToolGeometryWidget::useUniformScaling() const
449 {
450     return chkUniformScaling->isChecked();
451 }
452 
showEvent(QShowEvent * event)453 void DefaultToolGeometryWidget::showEvent(QShowEvent *event)
454 {
455     QWidget::showEvent(event);
456 
457     slotUpdatePositionBoxes();
458     slotUpdateSizeBoxes();
459     slotUpdateOpacitySlider();
460     slotUpdateAspectButton();
461     slotUpdateCheckboxes();
462     slotAnchorPointChanged();
463 }
464 
resourceChanged(int key,const QVariant & res)465 void DefaultToolGeometryWidget::resourceChanged(int key, const QVariant &res)
466 {
467     if (key == KoCanvasResourceProvider::Unit) {
468         setUnit(res.value<KoUnit>());
469     } else if (key == DefaultTool::HotPosition) {
470         positionSelector->setValue(KoFlake::AnchorPosition(res.toInt()));
471     }
472 }
473