File indexing completed on 2024-12-22 04:13:02
0001 /* This file is part of the KDE project 0002 * SPDX-FileCopyrightText: 2009 Boudewijn Rempt <boud@valdyas.org> 0003 * SPDX-FileCopyrightText: 2015 Michael Abrahams <miabraha@gmail.com> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #ifndef KISTOOLSELECTBASE_H 0009 #define KISTOOLSELECTBASE_H 0010 0011 #include "KoPointerEvent.h" 0012 #include "kis_tool.h" 0013 #include "kis_canvas2.h" 0014 #include "kis_selection.h" 0015 #include "kis_selection_options.h" 0016 #include "kis_selection_tool_config_widget_helper.h" 0017 #include "KisViewManager.h" 0018 #include "kis_selection_manager.h" 0019 #include "kis_selection_modifier_mapper.h" 0020 #include "strokes/move_stroke_strategy.h" 0021 #include "kis_image.h" 0022 #include "kis_cursor.h" 0023 #include "kis_action_manager.h" 0024 #include "kis_action.h" 0025 #include "kis_signal_auto_connection.h" 0026 #include "kis_selection_tool_helper.h" 0027 #include "kis_assert.h" 0028 #include <input/kis_extended_modifiers_mapper.h> 0029 0030 /** 0031 * This is a basic template to create selection tools from basic path based drawing tools. 0032 * The template overrides the ability to execute alternate actions correctly. 0033 * The default behavior for the modifier keys is as follows: 0034 * 0035 * Shift: add to selection 0036 * Alt: subtract from selection 0037 * Shift+Alt: intersect current selection 0038 * Ctrl+Alt: symmetric difference 0039 * Ctrl: replace selection 0040 * 0041 * The mapping itself is done in KisSelectionModifierMapper. 0042 * 0043 * Certain tools also use modifier keys to alter their behavior, e.g. forcing square proportions with the rectangle tool. 0044 * The template enables the following rules for forwarding keys: 0045 0046 * 1) If the user is not selecting, then changing the modifier combination 0047 * changes the selection method. 0048 * 0049 * 2) If the user is selecting then the modifier keys are forwarded to the 0050 * specific tool, so that it can do with them whatever it wants. The selection 0051 * method is not changed in this stage and it will be the same as just before 0052 * the user started selecting. 0053 * 0054 * 3) Once the user finishes selecting, the selection method is updated to reflect 0055 * the current modifier combination 0056 * 0057 * 4) If the user is moving the selection, then changing the modifiers 0058 */ 0059 0060 template <class BaseClass> 0061 class KisToolSelectBase : public BaseClass 0062 { 0063 0064 public: 0065 0066 KisToolSelectBase(KoCanvasBase* canvas, const QString toolName) 0067 : BaseClass(canvas) 0068 , m_widgetHelper(toolName) 0069 , m_selectionActionAlternate(SELECTION_DEFAULT) 0070 { 0071 KisSelectionModifierMapper::instance(); 0072 } 0073 0074 KisToolSelectBase(KoCanvasBase* canvas, const QCursor cursor, const QString toolName) 0075 : BaseClass(canvas, cursor) 0076 , m_widgetHelper(toolName) 0077 , m_selectionActionAlternate(SELECTION_DEFAULT) 0078 { 0079 KisSelectionModifierMapper::instance(); 0080 } 0081 0082 KisToolSelectBase(KoCanvasBase* canvas, QCursor cursor, QString toolName, KoToolBase *delegateTool) 0083 : BaseClass(canvas, cursor, delegateTool) 0084 , m_widgetHelper(toolName) 0085 , m_selectionActionAlternate(SELECTION_DEFAULT) 0086 { 0087 KisSelectionModifierMapper::instance(); 0088 } 0089 0090 enum SampleLayersMode 0091 { 0092 SampleAllLayers, 0093 SampleCurrentLayer, 0094 SampleColorLabeledLayers, 0095 }; 0096 0097 void updateActionShortcutToolTips() { 0098 KisSelectionOptions *widget = m_widgetHelper.optionWidget(); 0099 if (widget) { 0100 widget->updateActionButtonToolTip( 0101 SELECTION_REPLACE, 0102 this->action("selection_tool_mode_replace")->shortcut()); 0103 widget->updateActionButtonToolTip( 0104 SELECTION_ADD, 0105 this->action("selection_tool_mode_add")->shortcut()); 0106 widget->updateActionButtonToolTip( 0107 SELECTION_SUBTRACT, 0108 this->action("selection_tool_mode_subtract")->shortcut()); 0109 widget->updateActionButtonToolTip( 0110 SELECTION_INTERSECT, 0111 this->action("selection_tool_mode_intersect")->shortcut()); 0112 } 0113 } 0114 0115 void activate(const QSet<KoShape *> &shapes) override 0116 { 0117 BaseClass::activate(shapes); 0118 0119 m_modeConnections.addUniqueConnection( 0120 this->action("selection_tool_mode_replace"), SIGNAL(triggered()), 0121 &m_widgetHelper, SLOT(slotReplaceModeRequested())); 0122 0123 m_modeConnections.addUniqueConnection( 0124 this->action("selection_tool_mode_add"), SIGNAL(triggered()), 0125 &m_widgetHelper, SLOT(slotAddModeRequested())); 0126 0127 m_modeConnections.addUniqueConnection( 0128 this->action("selection_tool_mode_subtract"), SIGNAL(triggered()), 0129 &m_widgetHelper, SLOT(slotSubtractModeRequested())); 0130 0131 m_modeConnections.addUniqueConnection( 0132 this->action("selection_tool_mode_intersect"), SIGNAL(triggered()), 0133 &m_widgetHelper, SLOT(slotIntersectModeRequested())); 0134 0135 updateActionShortcutToolTips(); 0136 0137 if (m_widgetHelper.optionWidget()) { 0138 if (isPixelOnly()) { 0139 m_widgetHelper.optionWidget()->setModeSectionVisible(false); 0140 m_widgetHelper.optionWidget()->setAdjustmentsSectionVisible( 0141 true); 0142 } 0143 m_widgetHelper.optionWidget()->setReferenceSectionVisible( 0144 usesColorLabels()); 0145 } 0146 } 0147 0148 void deactivate() override 0149 { 0150 BaseClass::deactivate(); 0151 m_modeConnections.clear(); 0152 } 0153 0154 QWidget *createOptionWidget() override 0155 { 0156 m_widgetHelper.createOptionWidget(this->toolId()); 0157 m_widgetHelper.setConfigGroupForExactTool(this->toolId()); 0158 0159 this->connect(this, SIGNAL(isActiveChanged(bool)), &m_widgetHelper, SLOT(slotToolActivatedChanged(bool))); 0160 this->connect(&m_widgetHelper, 0161 SIGNAL(selectionActionChanged(SelectionAction)), 0162 this, 0163 SLOT(resetCursorStyle())); 0164 0165 updateActionShortcutToolTips(); 0166 if (m_widgetHelper.optionWidget()) { 0167 m_widgetHelper.optionWidget()->setContentsMargins(0, 10, 0, 10); 0168 if (isPixelOnly()) { 0169 m_widgetHelper.optionWidget()->setModeSectionVisible(false); 0170 m_widgetHelper.optionWidget()->setAdjustmentsSectionVisible( 0171 true); 0172 } 0173 m_widgetHelper.optionWidget()->setReferenceSectionVisible( 0174 usesColorLabels()); 0175 } 0176 0177 return m_widgetHelper.optionWidget(); 0178 } 0179 0180 SelectionMode selectionMode() const 0181 { 0182 return m_widgetHelper.selectionMode(); 0183 } 0184 0185 SelectionAction selectionAction() const 0186 { 0187 if (alternateSelectionAction() == SELECTION_DEFAULT) { 0188 return m_widgetHelper.selectionAction(); 0189 } 0190 return alternateSelectionAction(); 0191 } 0192 0193 bool antiAliasSelection() const 0194 { 0195 return m_widgetHelper.antiAliasSelection(); 0196 } 0197 0198 int growSelection() const 0199 { 0200 return m_widgetHelper.growSelection(); 0201 } 0202 0203 bool stopGrowingAtDarkestPixel() const 0204 { 0205 return m_widgetHelper.stopGrowingAtDarkestPixel(); 0206 } 0207 0208 int featherSelection() const 0209 { 0210 return m_widgetHelper.featherSelection(); 0211 } 0212 0213 QList<int> colorLabelsSelected() const 0214 { 0215 return m_widgetHelper.selectedColorLabels(); 0216 } 0217 0218 SampleLayersMode sampleLayersMode() const 0219 { 0220 KisSelectionOptions::ReferenceLayers referenceLayers = 0221 m_widgetHelper.referenceLayers(); 0222 if (referenceLayers == KisSelectionOptions::AllLayers) { 0223 return SampleAllLayers; 0224 } else if (referenceLayers == KisSelectionOptions::CurrentLayer) { 0225 return SampleCurrentLayer; 0226 } else if (referenceLayers == KisSelectionOptions::ColorLabeledLayers) { 0227 return SampleColorLabeledLayers; 0228 } 0229 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(true, SampleAllLayers); 0230 return SampleAllLayers; 0231 } 0232 0233 SelectionAction alternateSelectionAction() const 0234 { 0235 return m_selectionActionAlternate; 0236 } 0237 0238 KisSelectionOptions* selectionOptionWidget() 0239 { 0240 return m_widgetHelper.optionWidget(); 0241 } 0242 0243 virtual void setAlternateSelectionAction(SelectionAction action) 0244 { 0245 m_selectionActionAlternate = action; 0246 } 0247 0248 void activateAlternateAction(KisTool::AlternateAction action) override 0249 { 0250 Q_UNUSED(action); 0251 BaseClass::activatePrimaryAction(); 0252 } 0253 0254 void deactivateAlternateAction(KisTool::AlternateAction action) override 0255 { 0256 Q_UNUSED(action); 0257 BaseClass::deactivatePrimaryAction(); 0258 } 0259 0260 void beginAlternateAction(KoPointerEvent *event, 0261 KisTool::AlternateAction action) override 0262 { 0263 Q_UNUSED(action); 0264 beginPrimaryAction(event); 0265 } 0266 0267 void continueAlternateAction(KoPointerEvent *event, 0268 KisTool::AlternateAction action) override 0269 { 0270 Q_UNUSED(action); 0271 continuePrimaryAction(event); 0272 } 0273 0274 void endAlternateAction(KoPointerEvent *event, 0275 KisTool::AlternateAction action) override 0276 { 0277 Q_UNUSED(action); 0278 endPrimaryAction(event); 0279 } 0280 0281 KisNodeSP locateSelectionMaskUnderCursor(const QPointF &pos, Qt::KeyboardModifiers modifiers) { 0282 if (modifiers != Qt::NoModifier) return 0; 0283 0284 KisCanvas2* canvas = dynamic_cast<KisCanvas2*>(this->canvas()); 0285 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas, 0); 0286 0287 KisSelectionSP selection = canvas->viewManager()->selection(); 0288 if (selection && 0289 selection->outlineCacheValid()) { 0290 0291 const qreal handleRadius = qreal(this->handleRadius()) / canvas->coordinatesConverter()->effectiveZoom(); 0292 QPainterPath samplePath; 0293 samplePath.addEllipse(pos, handleRadius, handleRadius); 0294 0295 const QPainterPath selectionPath = selection->outlineCache(); 0296 0297 if (selectionPath.intersects(samplePath) && !selectionPath.contains(samplePath)) { 0298 KisNodeSP parent = selection->parentNode(); 0299 if (parent && parent->isEditable()) { 0300 return parent; 0301 } 0302 } 0303 } 0304 0305 return 0; 0306 } 0307 0308 void keyPressEvent(QKeyEvent *event) override 0309 { 0310 const Qt::Key key = KisExtendedModifiersMapper::workaroundShiftAltMetaHell(event); 0311 // Assume all the modifiers were unpressed... 0312 m_currentModifiers = Qt::NoModifier; 0313 // ...and add those which are right now 0314 if (key == Qt::Key_Control || event->modifiers().testFlag(Qt::ControlModifier)) { 0315 m_currentModifiers.setFlag(Qt::ControlModifier); 0316 } 0317 if (key == Qt::Key_Shift || event->modifiers().testFlag(Qt::ShiftModifier)) { 0318 m_currentModifiers.setFlag(Qt::ShiftModifier); 0319 } 0320 if (key == Qt::Key_Alt || event->modifiers().testFlag(Qt::AltModifier)) { 0321 m_currentModifiers.setFlag(Qt::AltModifier); 0322 } 0323 0324 // Avoid changing the selection mode and cursor if the user is interacting 0325 if (isSelecting()) { 0326 BaseClass::keyPressEvent(event); 0327 return; 0328 } 0329 if (isMovingSelection()) { 0330 return; 0331 } 0332 0333 setAlternateSelectionAction(KisSelectionModifierMapper::map(m_currentModifiers)); 0334 this->resetCursorStyle(); 0335 } 0336 0337 void keyReleaseEvent(QKeyEvent *event) override 0338 { 0339 const Qt::Key key = KisExtendedModifiersMapper::workaroundShiftAltMetaHell(event); 0340 // Assume all the modifiers were pressed... 0341 m_currentModifiers = Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier; 0342 // ...and remove those which aren't right now 0343 if (key == Qt::Key_Control || !event->modifiers().testFlag(Qt::ControlModifier)) { 0344 m_currentModifiers.setFlag(Qt::ControlModifier, false); 0345 } 0346 if (key == Qt::Key_Shift || !event->modifiers().testFlag(Qt::ShiftModifier)) { 0347 m_currentModifiers.setFlag(Qt::ShiftModifier, false); 0348 } 0349 if (key == Qt::Key_Alt || !event->modifiers().testFlag(Qt::AltModifier)) { 0350 m_currentModifiers.setFlag(Qt::AltModifier, false); 0351 } 0352 0353 // Avoid changing the selection mode and cursor if the user is interacting 0354 if (isSelecting()) { 0355 BaseClass::keyReleaseEvent(event); 0356 return; 0357 } 0358 if (isMovingSelection()) { 0359 return; 0360 } 0361 0362 setAlternateSelectionAction(KisSelectionModifierMapper::map(m_currentModifiers)); 0363 if (m_currentModifiers == Qt::NoModifier) { 0364 KisNodeSP selectionMask = locateSelectionMaskUnderCursor(m_currentPos, m_currentModifiers); 0365 if (selectionMask) { 0366 this->useCursor(KisCursor::moveSelectionCursor()); 0367 } else { 0368 this->resetCursorStyle(); 0369 } 0370 } else { 0371 this->resetCursorStyle(); 0372 } 0373 } 0374 0375 void mouseMoveEvent(KoPointerEvent *event) override 0376 { 0377 m_currentPos = this->convertToPixelCoord(event->point); 0378 0379 if (isSelecting()) { 0380 BaseClass::mouseMoveEvent(event); 0381 return; 0382 } 0383 if (isMovingSelection()) { 0384 return; 0385 } 0386 0387 KisNodeSP selectionMask = locateSelectionMaskUnderCursor(m_currentPos, event->modifiers()); 0388 if (selectionMask) { 0389 this->useCursor(KisCursor::moveSelectionCursor()); 0390 } else { 0391 setAlternateSelectionAction(KisSelectionModifierMapper::map(m_currentModifiers)); 0392 this->resetCursorStyle(); 0393 } 0394 } 0395 0396 void beginPrimaryAction(KoPointerEvent *event) override 0397 { 0398 if (isSelecting()) { 0399 BaseClass::beginPrimaryAction(event); 0400 return; 0401 } 0402 if (isMovingSelection()) { 0403 return; 0404 } 0405 0406 const QPointF pos = this->convertToPixelCoord(event->point); 0407 KisCanvas2* canvas = dynamic_cast<KisCanvas2*>(this->canvas()); 0408 KIS_SAFE_ASSERT_RECOVER_RETURN(canvas); 0409 0410 KisNodeSP selectionMask = locateSelectionMaskUnderCursor(pos, event->modifiers()); 0411 if (selectionMask) { 0412 if (this->beginMoveSelectionInteraction()) { 0413 KisStrokeStrategy *strategy = new MoveStrokeStrategy({selectionMask}, this->image().data(), this->image().data()); 0414 m_moveStrokeId = this->image()->startStroke(strategy); 0415 m_dragStartPos = pos; 0416 m_didMove = true; 0417 return; 0418 } 0419 } 0420 0421 m_didMove = false; 0422 BaseClass::beginPrimaryAction(event); 0423 } 0424 0425 void continuePrimaryAction(KoPointerEvent *event) override 0426 { 0427 if (isMovingSelection()) { 0428 const QPointF pos = this->convertToPixelCoord(event->point); 0429 const QPoint offset((pos - m_dragStartPos).toPoint()); 0430 0431 this->image()->addJob(m_moveStrokeId, new MoveStrokeStrategy::Data(offset)); 0432 return; 0433 } 0434 0435 BaseClass::continuePrimaryAction(event); 0436 } 0437 0438 void endPrimaryAction(KoPointerEvent *event) override 0439 { 0440 if (isMovingSelection()) { 0441 this->image()->endStroke(m_moveStrokeId); 0442 m_moveStrokeId.clear(); 0443 this->endMoveSelectionInteraction(); 0444 return; 0445 } 0446 0447 BaseClass::endPrimaryAction(event); 0448 } 0449 0450 bool selectionDidMove() const 0451 { 0452 return m_didMove; 0453 } 0454 0455 QMenu *popupActionsMenu() override 0456 { 0457 if (isSelecting()) { 0458 return BaseClass::popupActionsMenu(); 0459 } 0460 0461 KisCanvas2 * kisCanvas = dynamic_cast<KisCanvas2*>(canvas()); 0462 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(kisCanvas, 0); 0463 0464 return KisSelectionToolHelper::getSelectionContextMenu(kisCanvas); 0465 } 0466 0467 KisPopupWidgetInterface* popupWidget() override 0468 { 0469 if (isSelecting()) { 0470 return BaseClass::popupWidget(); 0471 } 0472 return nullptr; 0473 } 0474 0475 bool beginMoveSelectionInteraction() { 0476 if (m_currentInteraction != Interaction_None) { 0477 return false; 0478 } 0479 m_currentInteraction = Interaction_MoveSelection; 0480 return true; 0481 } 0482 0483 bool endMoveSelectionInteraction() { 0484 if (!isMovingSelection()) { 0485 return false; 0486 } 0487 m_currentInteraction = Interaction_None; 0488 updateCursorDelayed(); 0489 return true; 0490 } 0491 0492 bool beginSelectInteraction() { 0493 if (m_currentInteraction != Interaction_None) { 0494 return false; 0495 } 0496 m_currentInteraction = Interaction_Select; 0497 return true; 0498 } 0499 0500 bool endSelectInteraction() { 0501 if (!isSelecting()) { 0502 return false; 0503 } 0504 m_currentInteraction = Interaction_None; 0505 updateCursorDelayed(); 0506 return true; 0507 } 0508 0509 bool isMovingSelection() const { 0510 return m_currentInteraction == Interaction_MoveSelection; 0511 } 0512 0513 bool isSelecting() const { 0514 return m_currentInteraction == Interaction_Select; 0515 } 0516 0517 void updateCursorDelayed() { 0518 setAlternateSelectionAction(KisSelectionModifierMapper::map(m_currentModifiers)); 0519 QTimer::singleShot(100, 0520 this, 0521 [this]() 0522 { 0523 KisNodeSP selectionMask = locateSelectionMaskUnderCursor(m_currentPos, m_currentModifiers); 0524 if (selectionMask) { 0525 this->useCursor(KisCursor::moveSelectionCursor()); 0526 } else { 0527 this->resetCursorStyle(); 0528 } 0529 } 0530 ); 0531 } 0532 0533 protected: 0534 using BaseClass::canvas; 0535 KisSelectionToolConfigWidgetHelper m_widgetHelper; 0536 SelectionAction m_selectionActionAlternate; 0537 0538 virtual bool isPixelOnly() const { 0539 return false; 0540 } 0541 0542 virtual bool usesColorLabels() const { 0543 return false; 0544 } 0545 0546 private: 0547 enum Interaction 0548 { 0549 Interaction_None, 0550 Interaction_Select, 0551 Interaction_MoveSelection 0552 }; 0553 0554 Interaction m_currentInteraction{Interaction_None}; 0555 0556 Qt::KeyboardModifiers m_currentModifiers; 0557 0558 QPointF m_dragStartPos; 0559 QPointF m_currentPos; 0560 KisStrokeId m_moveStrokeId; 0561 bool m_didMove = false; 0562 0563 KisSignalAutoConnectionsStore m_modeConnections; 0564 }; 0565 0566 struct FakeBaseTool : KisTool 0567 { 0568 FakeBaseTool(KoCanvasBase* canvas) 0569 : KisTool(canvas, QCursor()) 0570 { 0571 } 0572 0573 FakeBaseTool(KoCanvasBase* canvas, const QString &toolName) 0574 : KisTool(canvas, QCursor()) 0575 { 0576 Q_UNUSED(toolName); 0577 } 0578 0579 FakeBaseTool(KoCanvasBase* canvas, const QCursor &cursor) 0580 : KisTool(canvas, cursor) 0581 { 0582 } 0583 }; 0584 0585 0586 typedef KisToolSelectBase<FakeBaseTool> KisToolSelect; 0587 0588 0589 #endif // KISTOOLSELECTBASE_H