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