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