File indexing completed on 2024-12-22 04:12:42

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2012 Arjen Hiemstra <ahiemstra@heimr.nl>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kis_rotate_canvas_action.h"
0008 
0009 #include <QApplication>
0010 #include <QNativeGestureEvent>
0011 #include <klocalizedstring.h>
0012 
0013 #include "kis_cursor.h"
0014 #include "kis_canvas_controller.h"
0015 #include <kis_canvas2.h>
0016 #include "kis_input_manager.h"
0017 
0018 #include <math.h>
0019 
0020 constexpr qreal DISCRETE_ANGLE_STEP = 15.0;  // discrete rotation snapping angle
0021 
0022 class KisRotateCanvasAction::Private
0023 {
0024 public:
0025     Private() {}
0026 
0027     // Coverity requires sane defaults for all variables (CID 36429)
0028     Shortcut mode {RotateModeShortcut};
0029 
0030     qreal previousAngle {0.0};
0031     qreal snapRotation {0.0};
0032     qreal touchRotation {0.0};
0033     bool allowRotation {false};
0034 };
0035 
0036 
0037 KisRotateCanvasAction::KisRotateCanvasAction()
0038     : KisAbstractInputAction("Rotate Canvas")
0039     , d(new Private())
0040 {
0041     setName(i18n("Rotate Canvas"));
0042     setDescription(i18n("The <i>Rotate Canvas</i> action rotates the canvas."));
0043 
0044     QHash<QString, int> shortcuts;
0045     shortcuts.insert(i18n("Rotate Mode"), RotateModeShortcut);
0046     shortcuts.insert(i18n("Discrete Rotate Mode"), DiscreteRotateModeShortcut);
0047     shortcuts.insert(i18n("Rotate Left"), RotateLeftShortcut);
0048     shortcuts.insert(i18n("Rotate Right"), RotateRightShortcut);
0049     shortcuts.insert(i18n("Reset Rotation"), RotateResetShortcut);
0050     setShortcutIndexes(shortcuts);
0051 }
0052 
0053 KisRotateCanvasAction::~KisRotateCanvasAction()
0054 {
0055     delete d;
0056 }
0057 
0058 int KisRotateCanvasAction::priority() const
0059 {
0060     return 3;
0061 }
0062 
0063 void KisRotateCanvasAction::activate(int shortcut)
0064 {
0065     if (shortcut == DiscreteRotateModeShortcut) {
0066         QApplication::setOverrideCursor(KisCursor::rotateCanvasDiscreteCursor());
0067     } else /* if (shortcut == SmoothRotateModeShortcut) */ {
0068         QApplication::setOverrideCursor(KisCursor::rotateCanvasSmoothCursor());
0069     }
0070 }
0071 
0072 void KisRotateCanvasAction::deactivate(int shortcut)
0073 {
0074     Q_UNUSED(shortcut);
0075     QApplication::restoreOverrideCursor();
0076 }
0077 
0078 void KisRotateCanvasAction::begin(int shortcut, QEvent *event)
0079 {
0080     KisAbstractInputAction::begin(shortcut, event);
0081     d->allowRotation = false;
0082     d->previousAngle = 0;
0083     d->snapRotation = 0;
0084     d->touchRotation = 0;
0085 
0086     KisCanvasController *canvasController =
0087         dynamic_cast<KisCanvasController*>(inputManager()->canvas()->canvasController());
0088     KIS_SAFE_ASSERT_RECOVER_RETURN(canvasController);
0089 
0090     d->mode = (Shortcut)shortcut;
0091 
0092     switch(shortcut) {
0093         case RotateModeShortcut:
0094         case DiscreteRotateModeShortcut: {
0095             // If the canvas has been rotated to an angle that is not an exact multiple of DISCRETE_ANGLE_STEP,
0096             // we need to adjust the final discrete rotation by that angle difference.
0097             // trunc() is used to round the negative numbers towards zero.
0098             const qreal startRotation = inputManager()->canvas()->rotationAngle();
0099             d->snapRotation = startRotation - std::trunc(startRotation / DISCRETE_ANGLE_STEP) * DISCRETE_ANGLE_STEP;
0100             canvasController->beginCanvasRotation();
0101             break;
0102         }
0103         case RotateLeftShortcut:
0104             canvasController->rotateCanvasLeft15();
0105             break;
0106         case RotateRightShortcut:
0107             canvasController->rotateCanvasRight15();
0108             break;
0109         case RotateResetShortcut:
0110             canvasController->resetCanvasRotation();
0111             break;
0112     }
0113 }
0114 
0115 void KisRotateCanvasAction::end(QEvent *event)
0116 {
0117     Q_UNUSED(event);
0118 
0119     KisCanvasController *canvasController =
0120         dynamic_cast<KisCanvasController*>(inputManager()->canvas()->canvasController());
0121     KIS_SAFE_ASSERT_RECOVER_RETURN(canvasController);
0122 
0123     switch(d->mode) {
0124     case RotateModeShortcut:
0125     case DiscreteRotateModeShortcut:
0126         canvasController->endCanvasRotation();
0127         break;
0128     default:
0129         break;
0130     }
0131 }
0132 
0133 void KisRotateCanvasAction::cursorMovedAbsolute(const QPointF &startPos, const QPointF &pos)
0134 {
0135     if (d->mode == RotateResetShortcut) {
0136         return;
0137     }
0138 
0139     const KisCoordinatesConverter *converter = inputManager()->canvas()->coordinatesConverter();
0140     const QPointF centerPoint = converter->flakeToWidget(converter->flakeCenterPoint());
0141     const QPointF startPoint = startPos - centerPoint;
0142     const QPointF newPoint = pos - centerPoint;
0143 
0144     const qreal oldAngle = atan2(startPoint.y(), startPoint.x());
0145     const qreal newAngle = atan2(newPoint.y(), newPoint.x());
0146 
0147     qreal newRotation = (180 / M_PI) * (newAngle - oldAngle);
0148 
0149     if (d->mode == DiscreteRotateModeShortcut) {
0150         // Do not snap unless the user rotated half-way in the desired direction.
0151         if (qAbs(newRotation) > 0.5 * DISCRETE_ANGLE_STEP || d->allowRotation) {
0152             d->allowRotation = true;
0153             newRotation = qRound((newRotation + d->snapRotation) / DISCRETE_ANGLE_STEP) * DISCRETE_ANGLE_STEP - d->snapRotation;
0154         } else {
0155             newRotation = 0.0;
0156         }
0157     }
0158 
0159     KisCanvasController *canvasController =
0160         dynamic_cast<KisCanvasController*>(inputManager()->canvas()->canvasController());
0161     KIS_SAFE_ASSERT_RECOVER_RETURN(canvasController);
0162     canvasController->rotateCanvas(newRotation);
0163 }
0164 
0165 void KisRotateCanvasAction::inputEvent(QEvent* event)
0166 {
0167     switch (event->type()) {
0168         case QEvent::NativeGesture: {
0169             QNativeGestureEvent *gevent = static_cast<QNativeGestureEvent*>(event);
0170             KisCanvas2 *canvas = inputManager()->canvas();
0171             KisCanvasController *controller = static_cast<KisCanvasController*>(canvas->canvasController());
0172 
0173             const float angle = gevent->value();
0174             QPoint widgetPos = canvas->canvasWidget()->mapFromGlobal(gevent->globalPos());
0175             controller->rotateCanvas(angle, widgetPos, true);
0176             return;
0177         }
0178         case QEvent::TouchUpdate: {
0179             QTouchEvent *touchEvent = static_cast<QTouchEvent*>(event);
0180 
0181             if (touchEvent->touchPoints().count() != 2)
0182                 break;
0183 
0184             QTouchEvent::TouchPoint tp0 = touchEvent->touchPoints().at(0);
0185             QTouchEvent::TouchPoint tp1 = touchEvent->touchPoints().at(1);
0186 
0187             if (tp0.state() == Qt::TouchPointReleased ||
0188                 tp1.state() == Qt::TouchPointReleased)
0189             {
0190                 // Workaround: on some devices, the coordinates of TouchPoints
0191                 // in state TouchPointReleased are not reliable, and can
0192                 // "jump" by a significant distance. So we just stop handling
0193                 // the rotation as soon as the user's finger leaves the tablet.
0194                 break;
0195             }
0196 
0197             QPointF p0 = tp0.pos();
0198             QPointF p1 = tp1.pos();
0199 
0200             if ((p0-p1).manhattanLength() < 10)
0201             {
0202                 // The TouchPoints are too close together. Don't update the
0203                 // rotation as the angle will likely be off. This also deals
0204                 // with a glitch where a newly pressed TouchPoint incorrectly
0205                 // reports the existing TouchPoint's coordinates instead of its
0206                 // own.
0207                 break;
0208             }
0209 
0210             // high school (y2 - y1) / (x2 - x1)
0211             QPointF slope = p1 - p0;
0212             qreal newAngle = atan2(slope.y(), slope.x());
0213 
0214             // We must have the previous angle measurement to calculate the delta.
0215             if (d->allowRotation)
0216             {
0217                 qreal delta = (180 / M_PI) * (newAngle - d->previousAngle);
0218 
0219                 // Rotate by the effective angle from the beginning of the action.
0220                 d->touchRotation += delta;
0221 
0222                 KisCanvas2 *canvas = inputManager()->canvas();
0223                 KisCanvasController *controller = static_cast<KisCanvasController*>(canvas->canvasController());
0224                 controller->rotateCanvas(d->touchRotation);
0225             }
0226             else
0227             {
0228                 d->allowRotation = true;
0229             }
0230 
0231             d->previousAngle = newAngle;
0232             return;
0233         }
0234         default:
0235             break;
0236     }
0237     KisAbstractInputAction::inputEvent(event);
0238 }
0239 
0240 KisInputActionGroup KisRotateCanvasAction::inputActionGroup(int shortcut) const
0241 {
0242     Q_UNUSED(shortcut);
0243     return ViewTransformActionGroup;
0244 }
0245 
0246 bool KisRotateCanvasAction::supportsHiResInputEvents(int shortcut) const
0247 {
0248     Q_UNUSED(shortcut);
0249     return true;
0250 }