File indexing completed on 2024-06-23 04:28:10
0001 /* This file is part of the KDE project 0002 * SPDX-FileCopyrightText: 2007 Martin Pfeiffer <hubipete@gmx.net> 0003 * SPDX-FileCopyrightText: 2007 Jan Hambrecht <jaham@gmx.net> 0004 * SPDX-FileCopyrightText: 2008 Thorsten Zachmann <zachmann@kde.org> 0005 * SPDX-FileCopyrightText: 2010 Thomas Zander <zander@kde.org> 0006 * 0007 * SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include "DefaultToolGeometryWidget.h" 0011 #include "DefaultTool.h" 0012 0013 #include <KoInteractionTool.h> 0014 #include <KoCanvasBase.h> 0015 #include <KoCanvasResourceProvider.h> 0016 #include <KoSelectedShapesProxy.h> 0017 #include <KoSelection.h> 0018 #include <KoUnit.h> 0019 #include <commands/KoShapeResizeCommand.h> 0020 #include <commands/KoShapeMoveCommand.h> 0021 #include <commands/KoShapeSizeCommand.h> 0022 #include <commands/KoShapeTransformCommand.h> 0023 #include <commands/KoShapeKeepAspectRatioCommand.h> 0024 #include <commands/KoShapeTransparencyCommand.h> 0025 #include <commands/KoShapePaintOrderCommand.h> 0026 #include "SelectionDecorator.h" 0027 #include <KoShapeGroup.h> 0028 0029 #include "KoAnchorSelectionWidget.h" 0030 0031 #include <QAction> 0032 #include <QSize> 0033 #include <QRadioButton> 0034 #include <QLabel> 0035 #include <QCheckBox> 0036 #include <QDoubleSpinBox> 0037 #include <QList> 0038 #include <QTransform> 0039 #include <kis_algebra_2d.h> 0040 0041 #include "kis_aspect_ratio_locker.h" 0042 #include "kis_debug.h" 0043 #include "kis_acyclic_signal_connector.h" 0044 #include "kis_signal_compressor.h" 0045 #include "kis_signals_blocker.h" 0046 #include "kis_icon.h" 0047 0048 0049 DefaultToolGeometryWidget::DefaultToolGeometryWidget(KoInteractionTool *tool, QWidget *parent) 0050 : QWidget(parent) 0051 , m_tool(tool) 0052 , m_sizeAspectLocker(new KisAspectRatioLocker()) 0053 , m_savedUniformScaling(false) 0054 { 0055 setupUi(this); 0056 0057 setUnit(KoUnit(KoUnit::Point)); 0058 0059 // Connect and initialize automated aspect locker 0060 m_sizeAspectLocker->connectSpinBoxes(widthSpinBox, heightSpinBox, aspectButton); 0061 aspectButton->setKeepAspectRatio(false); 0062 0063 0064 // TODO: use valueChanged() instead! 0065 connect(positionXSpinBox, SIGNAL(valueChangedPt(qreal)), this, SLOT(slotRepositionShapes())); 0066 connect(positionYSpinBox, SIGNAL(valueChangedPt(qreal)), this, SLOT(slotRepositionShapes())); 0067 0068 KoSelectedShapesProxy *selectedShapesProxy = m_tool->canvas()->selectedShapesProxy(); 0069 0070 connect(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateCheckboxes())); 0071 connect(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdatePositionBoxes())); 0072 connect(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateOpacitySlider())); 0073 0074 connect(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdatePositionBoxes())); 0075 connect(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdateOpacitySlider())); 0076 0077 connect(chkGlobalCoordinates, SIGNAL(toggled(bool)), SLOT(slotUpdateSizeBoxes())); 0078 connect(chkGlobalCoordinates, SIGNAL(toggled(bool)), SLOT(slotUpdateAspectButton())); 0079 0080 0081 /** 0082 * A huge block of self-blocking acyclic connections 0083 */ 0084 KisAcyclicSignalConnector *acyclicConnector = new KisAcyclicSignalConnector(this); 0085 acyclicConnector->connectForwardVoid(m_sizeAspectLocker.data(), SIGNAL(aspectButtonChanged()), this, SLOT(slotAspectButtonToggled())); 0086 acyclicConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateAspectButton())); 0087 acyclicConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdateAspectButton())); 0088 0089 KisAcyclicSignalConnector *sizeConnector = acyclicConnector->createCoordinatedConnector(); 0090 sizeConnector->connectForwardVoid(m_sizeAspectLocker.data(), SIGNAL(sliderValueChanged()), this, SLOT(slotResizeShapes())); 0091 sizeConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionChanged()), this, SLOT(slotUpdateSizeBoxes())); 0092 0093 KisAcyclicSignalConnector *contentSizeConnector = acyclicConnector->createCoordinatedConnector(); 0094 contentSizeConnector->connectBackwardVoid(selectedShapesProxy, SIGNAL(selectionContentChanged()), this, SLOT(slotUpdateSizeBoxesNoAspectChange())); 0095 0096 0097 // Connect and initialize anchor point resource 0098 KoCanvasResourceProvider *resourceManager = m_tool->canvas()->resourceManager(); 0099 connect(resourceManager, 0100 SIGNAL(canvasResourceChanged(int,QVariant)), 0101 SLOT(resourceChanged(int,QVariant))); 0102 resourceManager->setResource(DefaultTool::HotPosition, int(KoFlake::AnchorPosition::Center)); 0103 positionSelector->setValue(KoFlake::AnchorPosition(resourceManager->resource(DefaultTool::HotPosition).toInt())); 0104 0105 // Connect anchor point selector 0106 connect(positionSelector, SIGNAL(valueChanged(KoFlake::AnchorPosition)), SLOT(slotAnchorPointChanged())); 0107 0108 cmbPaintOrder->setIconSize(QSize(22, 22)); 0109 cmbPaintOrder->addItem(KisIconUtils::loadIcon("paint-order-fill-stroke-marker"), i18n("Fill, Stroke, Markers")); 0110 cmbPaintOrder->addItem(KisIconUtils::loadIcon("paint-order-fill-marker-stroke"), i18n("Fill, Markers, Stroke")); 0111 cmbPaintOrder->addItem(KisIconUtils::loadIcon("paint-order-stroke-fill-marker"), i18n("Stroke, Fill, Markers")); 0112 cmbPaintOrder->addItem(KisIconUtils::loadIcon("paint-order-stroke-marker-fill"), i18n("Stroke, Markers, Fill")); 0113 cmbPaintOrder->addItem(KisIconUtils::loadIcon("paint-order-marker-fill-stroke"), i18n("Markers, Fill, Stroke")); 0114 cmbPaintOrder->addItem(KisIconUtils::loadIcon("paint-order-marker-stroke-fill"), i18n("Markers, Stroke, Fill")); 0115 connect(cmbPaintOrder, SIGNAL(currentIndexChanged(int)), SLOT(slotPaintOrderChanged())); 0116 0117 0118 dblOpacity->setRange(0.0, 1.0, 2); 0119 dblOpacity->setSingleStep(0.01); 0120 dblOpacity->setFastSliderStep(0.1); 0121 dblOpacity->setTextTemplates(i18nc("{n} is the number value, % is the percent sign", "Opacity: {n}"), 0122 i18nc("{n} is the number value, % is the percent sign", "Opacity [*varies*]: {n}")); 0123 0124 dblOpacity->setValueGetter( 0125 [](KoShape *s) { return 1.0 - s->transparency(); } 0126 ); 0127 0128 connect(dblOpacity, SIGNAL(valueChanged(qreal)), SLOT(slotOpacitySliderChanged(qreal))); 0129 0130 // cold init 0131 slotUpdateOpacitySlider(); 0132 } 0133 0134 DefaultToolGeometryWidget::~DefaultToolGeometryWidget() 0135 { 0136 } 0137 0138 namespace { 0139 0140 void tryAnchorPosition(KoFlake::AnchorPosition anchor, 0141 const QRectF &rect, 0142 QPointF *position) 0143 { 0144 bool valid = false; 0145 QPointF anchoredPosition = KoFlake::anchorToPoint(anchor, rect, &valid); 0146 0147 if (valid) { 0148 *position = anchoredPosition; 0149 } 0150 } 0151 0152 QRectF calculateSelectionBounds(KoSelection *selection, 0153 KoFlake::AnchorPosition anchor, 0154 bool useGlobalSize, 0155 QList<KoShape*> *outShapes = 0) 0156 { 0157 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0158 0159 KoShape *shape = shapes.size() == 1 ? shapes.first() : selection; 0160 0161 QRectF resultRect = shape->outlineRect(); 0162 0163 QPointF resultPoint = resultRect.topLeft(); 0164 tryAnchorPosition(anchor, resultRect, &resultPoint); 0165 0166 if (useGlobalSize) { 0167 resultRect = shape->absoluteTransformation().mapRect(resultRect); 0168 } else { 0169 /** 0170 * Some shapes, e.g. KoSelection and KoShapeGroup don't have real size() and 0171 * do all the resizing with transformation(), just try to cover this case and 0172 * fetch their scale using the transform. 0173 */ 0174 0175 KisAlgebra2D::DecomposedMatrix matrix(shape->transformation()); 0176 resultRect = matrix.scaleTransform().mapRect(resultRect); 0177 } 0178 0179 resultPoint = shape->absoluteTransformation().map(resultPoint); 0180 0181 if (outShapes) { 0182 *outShapes = shapes; 0183 } 0184 0185 return QRectF(resultPoint, resultRect.size()); 0186 } 0187 0188 } 0189 0190 void DefaultToolGeometryWidget::slotAnchorPointChanged() 0191 { 0192 if (!isVisible()) return; 0193 0194 QVariant newValue(positionSelector->value()); 0195 m_tool->canvas()->resourceManager()->setResource(DefaultTool::HotPosition, newValue); 0196 slotUpdatePositionBoxes(); 0197 } 0198 0199 void DefaultToolGeometryWidget::slotUpdateCheckboxes() 0200 { 0201 if (!isVisible()) return; 0202 0203 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0204 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0205 0206 KoShapeGroup *onlyGroupShape = 0; 0207 0208 if (shapes.size() == 1) { 0209 onlyGroupShape = dynamic_cast<KoShapeGroup*>(shapes.first()); 0210 } 0211 0212 const bool uniformScalingAvailable = shapes.size() <= 1 && !onlyGroupShape; 0213 0214 if (uniformScalingAvailable && !chkUniformScaling->isEnabled()) { 0215 chkUniformScaling->setChecked(m_savedUniformScaling); 0216 chkUniformScaling->setEnabled(uniformScalingAvailable); 0217 } else if (!uniformScalingAvailable && chkUniformScaling->isEnabled()) { 0218 m_savedUniformScaling = chkUniformScaling->isChecked(); 0219 chkUniformScaling->setChecked(true); 0220 chkUniformScaling->setEnabled(uniformScalingAvailable); 0221 } 0222 0223 // TODO: not implemented yet! 0224 chkAnchorLock->setEnabled(false); 0225 } 0226 0227 void DefaultToolGeometryWidget::slotAspectButtonToggled() 0228 { 0229 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0230 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0231 0232 KUndo2Command *cmd = 0233 new KoShapeKeepAspectRatioCommand(shapes, aspectButton->keepAspectRatio()); 0234 0235 m_tool->canvas()->addCommand(cmd); 0236 } 0237 0238 void DefaultToolGeometryWidget::slotUpdateAspectButton() 0239 { 0240 if (!isVisible()) return; 0241 0242 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0243 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0244 0245 bool hasKeepAspectRatio = false; 0246 bool hasNotKeepAspectRatio = false; 0247 0248 Q_FOREACH (KoShape *shape, shapes) { 0249 if (shape->keepAspectRatio()) { 0250 hasKeepAspectRatio = true; 0251 } else { 0252 hasNotKeepAspectRatio = true; 0253 } 0254 0255 if (hasKeepAspectRatio && hasNotKeepAspectRatio) break; 0256 } 0257 0258 Q_UNUSED(hasNotKeepAspectRatio); // TODO: use for tristated mode of the checkbox 0259 0260 const bool useGlobalSize = chkGlobalCoordinates->isChecked(); 0261 const KoFlake::AnchorPosition anchor = positionSelector->value(); 0262 const QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize); 0263 const bool hasNullDimensions = bounds.isEmpty(); 0264 0265 aspectButton->setKeepAspectRatio(hasKeepAspectRatio && !hasNullDimensions); 0266 aspectButton->setEnabled(!hasNullDimensions); 0267 } 0268 0269 //namespace { 0270 //qreal calculateCommonShapeTransparency(const QList<KoShape*> &shapes) 0271 //{ 0272 // qreal commonTransparency = -1.0; 0273 0274 // Q_FOREACH (KoShape *shape, shapes) { 0275 // if (commonTransparency < 0) { 0276 // commonTransparency = shape->transparency(); 0277 // } else if (!qFuzzyCompare(commonTransparency, shape->transparency())) { 0278 // commonTransparency = -1.0; 0279 // break; 0280 // } 0281 // } 0282 0283 // return commonTransparency; 0284 //} 0285 //} 0286 0287 void DefaultToolGeometryWidget::slotOpacitySliderChanged(qreal newOpacity) 0288 { 0289 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0290 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0291 if (shapes.isEmpty()) return; 0292 0293 KUndo2Command *cmd = 0294 new KoShapeTransparencyCommand(shapes, 1.0 - newOpacity); 0295 0296 m_tool->canvas()->addCommand(cmd); 0297 } 0298 0299 void DefaultToolGeometryWidget::slotUpdateOpacitySlider() 0300 { 0301 if (!isVisible()) return; 0302 0303 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0304 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0305 0306 dblOpacity->setSelection(shapes); 0307 } 0308 0309 void DefaultToolGeometryWidget::slotPaintOrderChanged() 0310 { 0311 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0312 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0313 if (shapes.isEmpty()) return; 0314 0315 KoShape::PaintOrder first = KoShape::Fill; 0316 KoShape::PaintOrder second = KoShape::Stroke; 0317 0318 switch(cmbPaintOrder->currentIndex()) { 0319 case 1: 0320 first = KoShape::Fill; 0321 second = KoShape::Markers; 0322 break; 0323 case 2: 0324 first = KoShape::Stroke; 0325 second = KoShape::Fill; 0326 break; 0327 case 3: 0328 first = KoShape::Stroke; 0329 second = KoShape::Markers; 0330 break; 0331 case 4: 0332 first = KoShape::Markers; 0333 second = KoShape::Fill; 0334 break; 0335 case 5: 0336 first = KoShape::Markers; 0337 second = KoShape::Stroke; 0338 break; 0339 default: 0340 first = KoShape::Fill; 0341 second = KoShape::Stroke; 0342 } 0343 0344 KUndo2Command *cmd = 0345 new KoShapePaintOrderCommand(shapes, first, second); 0346 0347 m_tool->canvas()->addCommand(cmd); 0348 } 0349 0350 void DefaultToolGeometryWidget::slotUpdatePaintOrder() { 0351 if (!isVisible()) return; 0352 0353 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0354 QList<KoShape*> shapes = selection->selectedEditableShapes(); 0355 0356 if (!shapes.isEmpty()) { 0357 KoShape *shape = shapes.first(); 0358 QVector<KoShape::PaintOrder> paintOrder = shape->paintOrder(); 0359 int index = 0; 0360 if (paintOrder.first() == KoShape::Fill) { 0361 index = paintOrder.at(1) == KoShape::Stroke? 0: 1; 0362 } else if (paintOrder.first() == KoShape::Stroke) { 0363 index = paintOrder.at(1) == KoShape::Fill? 2: 3; 0364 } else { 0365 index = paintOrder.at(1) == KoShape::Fill? 4: 5; 0366 } 0367 cmbPaintOrder->setCurrentIndex(index); 0368 } 0369 } 0370 0371 void DefaultToolGeometryWidget::slotUpdateSizeBoxes(bool updateAspect) 0372 { 0373 if (!isVisible()) return; 0374 0375 const bool useGlobalSize = chkGlobalCoordinates->isChecked(); 0376 const KoFlake::AnchorPosition anchor = positionSelector->value(); 0377 0378 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0379 const QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize); 0380 0381 const bool hasSizeConfiguration = !bounds.isNull(); 0382 0383 widthSpinBox->setEnabled(hasSizeConfiguration && bounds.width() > 0); 0384 heightSpinBox->setEnabled(hasSizeConfiguration && bounds.height() > 0); 0385 0386 if (hasSizeConfiguration) { 0387 KisSignalsBlocker b(widthSpinBox, heightSpinBox); 0388 widthSpinBox->changeValue(bounds.width()); 0389 heightSpinBox->changeValue(bounds.height()); 0390 0391 if (updateAspect) { 0392 m_sizeAspectLocker->updateAspect(); 0393 } 0394 } 0395 } 0396 0397 void DefaultToolGeometryWidget::slotUpdateSizeBoxesNoAspectChange() 0398 { 0399 slotUpdateSizeBoxes(false); 0400 } 0401 0402 void DefaultToolGeometryWidget::slotUpdatePositionBoxes() 0403 { 0404 if (!isVisible()) return; 0405 0406 const bool useGlobalSize = chkGlobalCoordinates->isChecked(); 0407 const KoFlake::AnchorPosition anchor = positionSelector->value(); 0408 0409 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0410 QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize); 0411 0412 const bool hasSizeConfiguration = !bounds.isNull(); 0413 0414 positionXSpinBox->setEnabled(hasSizeConfiguration); 0415 positionYSpinBox->setEnabled(hasSizeConfiguration); 0416 0417 if (hasSizeConfiguration) { 0418 KisSignalsBlocker b(positionXSpinBox, positionYSpinBox); 0419 positionXSpinBox->changeValue(bounds.x()); 0420 positionYSpinBox->changeValue(bounds.y()); 0421 } 0422 } 0423 0424 void DefaultToolGeometryWidget::slotRepositionShapes() 0425 { 0426 static const qreal eps = 1e-6; 0427 0428 const bool useGlobalSize = chkGlobalCoordinates->isChecked(); 0429 const KoFlake::AnchorPosition anchor = positionSelector->value(); 0430 0431 QList<KoShape*> shapes; 0432 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0433 QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize, &shapes); 0434 0435 if (bounds.isNull()) return; 0436 0437 const QPointF oldPosition = bounds.topLeft(); 0438 const QPointF newPosition(positionXSpinBox->value(), positionYSpinBox->value()); 0439 const QPointF diff = newPosition - oldPosition; 0440 0441 if (diff.manhattanLength() < eps) return; 0442 0443 QList<QPointF> oldPositions; 0444 QList<QPointF> newPositions; 0445 0446 Q_FOREACH (KoShape *shape, shapes) { 0447 const QPointF oldShapePosition = shape->absolutePosition(anchor); 0448 0449 oldPositions << shape->absolutePosition(anchor); 0450 newPositions << oldShapePosition + diff; 0451 } 0452 0453 KUndo2Command *cmd = new KoShapeMoveCommand(shapes, oldPositions, newPositions, anchor); 0454 m_tool->canvas()->addCommand(cmd); 0455 } 0456 0457 void DefaultToolGeometryWidget::slotResizeShapes() 0458 { 0459 static const qreal eps = 1e-4; 0460 0461 const bool useGlobalSize = chkGlobalCoordinates->isChecked(); 0462 const KoFlake::AnchorPosition anchor = positionSelector->value(); 0463 0464 QList<KoShape*> shapes; 0465 KoSelection *selection = m_tool->canvas()->selectedShapesProxy()->selection(); 0466 QRectF bounds = calculateSelectionBounds(selection, anchor, useGlobalSize, &shapes); 0467 0468 if (bounds.isNull()) return; 0469 0470 const QSizeF oldSize(bounds.size()); 0471 0472 QSizeF newSize(widthSpinBox->value(), heightSpinBox->value()); 0473 newSize = KisAlgebra2D::ensureSizeNotSmaller(newSize, QSizeF(eps, eps)); 0474 0475 const qreal scaleX = oldSize.width() > 0 ? newSize.width() / oldSize.width() : 1.0; 0476 const qreal scaleY = oldSize.height() > 0 ? newSize.height() / oldSize.height() : 1.0; 0477 0478 if (qAbs(scaleX - 1.0) < eps && qAbs(scaleY - 1.0) < eps) return; 0479 0480 const bool usePostScaling = 0481 shapes.size() > 1 || chkUniformScaling->isChecked(); 0482 0483 KUndo2Command *cmd = new KoShapeResizeCommand(shapes, 0484 scaleX, scaleY, 0485 bounds.topLeft(), 0486 useGlobalSize, 0487 usePostScaling, 0488 selection->transformation()); 0489 m_tool->canvas()->addCommand(cmd); 0490 } 0491 0492 void DefaultToolGeometryWidget::setUnit(const KoUnit &unit) 0493 { 0494 positionXSpinBox->setUnit(unit); 0495 positionYSpinBox->setUnit(unit); 0496 widthSpinBox->setUnit(unit); 0497 heightSpinBox->setUnit(unit); 0498 0499 positionXSpinBox->setDecimals(2); 0500 positionYSpinBox->setDecimals(2); 0501 widthSpinBox->setDecimals(2); 0502 heightSpinBox->setDecimals(2); 0503 0504 positionXSpinBox->setLineStep(1.0); 0505 positionYSpinBox->setLineStep(1.0); 0506 widthSpinBox->setLineStep(1.0); 0507 heightSpinBox->setLineStep(1.0); 0508 0509 slotUpdatePositionBoxes(); 0510 slotUpdateSizeBoxes(); 0511 } 0512 0513 bool DefaultToolGeometryWidget::useUniformScaling() const 0514 { 0515 return chkUniformScaling->isChecked(); 0516 } 0517 0518 void DefaultToolGeometryWidget::showEvent(QShowEvent *event) 0519 { 0520 QWidget::showEvent(event); 0521 0522 slotUpdatePositionBoxes(); 0523 slotUpdateSizeBoxes(); 0524 slotUpdateOpacitySlider(); 0525 slotUpdateAspectButton(); 0526 slotUpdateCheckboxes(); 0527 slotAnchorPointChanged(); 0528 slotUpdatePaintOrder(); 0529 } 0530 0531 void DefaultToolGeometryWidget::resourceChanged(int key, const QVariant &res) 0532 { 0533 if (key == KoCanvasResource::Unit) { 0534 setUnit(res.value<KoUnit>()); 0535 } else if (key == DefaultTool::HotPosition) { 0536 positionSelector->setValue(KoFlake::AnchorPosition(res.toInt())); 0537 } 0538 }