File indexing completed on 2025-02-02 04:26:09

0001 /*
0002  *  SPDX-FileCopyrightText: 2022 Marco Martin <mart@kde.org>
0003  *  SPDX-License-Identifier: LGPL-2.0-or-later
0004  */
0005 
0006 #include "AnnotationViewport.h"
0007 
0008 #include <QCursor>
0009 #include <QPainter>
0010 #include <QScreen>
0011 #include <utility>
0012 
0013 QList<AnnotationViewport *> AnnotationViewport::s_viewportInstances = {};
0014 static bool s_synchronizingAnyPressed = false;
0015 static bool s_isAnyPressed = false;
0016 
0017 AnnotationViewport::AnnotationViewport(QQuickItem *parent)
0018     : QQuickPaintedItem(parent)
0019 {
0020     s_viewportInstances.append(this);
0021     setFlag(ItemIsFocusScope);
0022     setAcceptHoverEvents(true);
0023     setAcceptedMouseButtons(Qt::LeftButton);
0024 }
0025 
0026 AnnotationViewport::~AnnotationViewport() noexcept
0027 {
0028     setPressed(false);
0029     s_viewportInstances.removeOne(this);
0030 }
0031 
0032 QRectF AnnotationViewport::viewportRect() const
0033 {
0034     return m_viewportRect;
0035 }
0036 
0037 void AnnotationViewport::setViewportRect(const QRectF &rect)
0038 {
0039     if (rect == m_viewportRect) {
0040         return;
0041     }
0042     m_viewportRect = rect;
0043     Q_EMIT viewportRectChanged();
0044     updateTransforms();
0045     update();
0046 }
0047 
0048 void AnnotationViewport::setZoom(qreal zoom)
0049 {
0050     if (zoom == m_zoom || qFuzzyIsNull(zoom)) {
0051         return;
0052     }
0053 
0054     m_zoom = zoom;
0055     Q_EMIT zoomChanged();
0056     updateTransforms();
0057     update();
0058 }
0059 
0060 qreal AnnotationViewport::zoom() const
0061 {
0062     return m_zoom;
0063 }
0064 
0065 AnnotationDocument *AnnotationViewport::document() const
0066 {
0067     return m_document;
0068 }
0069 
0070 void AnnotationViewport::setDocument(AnnotationDocument *doc)
0071 {
0072     if (m_document == doc) {
0073         return;
0074     }
0075 
0076     if (m_document) {
0077         disconnect(m_document, nullptr, this, nullptr);
0078     }
0079 
0080     m_document = doc;
0081     connect(doc, &AnnotationDocument::repaintNeeded, this, &AnnotationViewport::repaintDocument);
0082     connect(doc->tool(), &AnnotationTool::typeChanged, this, &AnnotationViewport::setCursorForToolType);
0083     Q_EMIT documentChanged();
0084     update();
0085 }
0086 
0087 QPointF AnnotationViewport::hoverPosition() const
0088 {
0089     return m_localHoverPosition;
0090 }
0091 
0092 void AnnotationViewport::setHoverPosition(const QPointF &point)
0093 {
0094     if (m_localHoverPosition == point) {
0095         return;
0096     }
0097     m_localHoverPosition = point;
0098     Q_EMIT hoverPositionChanged();
0099 }
0100 
0101 bool AnnotationViewport::isHovered() const
0102 {
0103     return m_isHovered;
0104 }
0105 
0106 void AnnotationViewport::setHovered(bool hovered)
0107 {
0108     if (m_isHovered == hovered) {
0109         return;
0110     }
0111 
0112     m_isHovered = hovered;
0113     Q_EMIT hoveredChanged();
0114 }
0115 
0116 void setHovered(bool hovered);
0117 
0118 QPointF AnnotationViewport::pressPosition() const
0119 {
0120     return m_localPressPosition;
0121 }
0122 
0123 void AnnotationViewport::setPressPosition(const QPointF &point)
0124 {
0125     if (m_localPressPosition == point) {
0126         return;
0127     }
0128     m_localPressPosition = point;
0129     Q_EMIT pressPositionChanged();
0130 }
0131 
0132 bool AnnotationViewport::isPressed() const
0133 {
0134     return m_isPressed;
0135 }
0136 
0137 void AnnotationViewport::setPressed(bool pressed)
0138 {
0139     if (m_isPressed == pressed) {
0140         return;
0141     }
0142 
0143     m_isPressed = pressed;
0144     Q_EMIT pressedChanged();
0145     setAnyPressed();
0146 }
0147 
0148 bool AnnotationViewport::isAnyPressed() const
0149 {
0150     return s_isAnyPressed;
0151 }
0152 
0153 void AnnotationViewport::setAnyPressed()
0154 {
0155     if (s_synchronizingAnyPressed || s_isAnyPressed == m_isPressed) {
0156         return;
0157     }
0158     s_synchronizingAnyPressed = true;
0159     // If pressed is true, anyPressed is guaranteed to be true.
0160     // If pressed is false, anyPressed may still be true if another viewport is pressed.
0161     const bool oldAnyPressed = s_isAnyPressed;
0162     if (m_isPressed) {
0163         s_isAnyPressed = m_isPressed;
0164     } else {
0165         for (const auto viewport : std::as_const(s_viewportInstances)) {
0166             s_isAnyPressed = viewport->m_isPressed;
0167             if (s_isAnyPressed) {
0168                 break;
0169             }
0170         }
0171     }
0172     // Don't emit if s_isAnyPressed still hasn't changed
0173     if (oldAnyPressed != s_isAnyPressed) {
0174         for (const auto viewport : std::as_const(s_viewportInstances)) {
0175             Q_EMIT viewport->anyPressedChanged();
0176         }
0177     }
0178     s_synchronizingAnyPressed = false;
0179 }
0180 
0181 QPainterPath AnnotationViewport::hoveredMousePath() const
0182 {
0183     return m_hoveredMousePath;
0184 }
0185 
0186 void AnnotationViewport::setHoveredMousePath(const QPainterPath &path)
0187 {
0188     if (path == m_hoveredMousePath) {
0189         return;
0190     }
0191     m_hoveredMousePath = path;
0192     Q_EMIT hoveredMousePathChanged();
0193 }
0194 
0195 QMatrix4x4 AnnotationViewport::localToDocument() const
0196 {
0197     return m_localToDocument;
0198 }
0199 
0200 void AnnotationViewport::updateTransforms()
0201 {
0202     QMatrix4x4 localToDocument;
0203     // translate() is affected by existing scales,
0204     // but scale() does not affect existing translations
0205     localToDocument.scale(1 / m_zoom, 1 / m_zoom);
0206     localToDocument.translate(m_viewportRect.x(), m_viewportRect.y());
0207 
0208     if (m_localToDocument != localToDocument) {
0209         m_localToDocument = localToDocument;
0210         Q_EMIT localToDocumentChanged();
0211     }
0212     QMatrix4x4 documentToLocal;
0213     documentToLocal.scale(m_zoom, m_zoom);
0214     documentToLocal.translate(-m_viewportRect.x(), -m_viewportRect.y());
0215     if (m_documentToLocal != documentToLocal) {
0216         m_documentToLocal = documentToLocal;
0217         Q_EMIT documentToLocalChanged();
0218     }
0219 }
0220 
0221 QMatrix4x4 AnnotationViewport::documentToLocal() const
0222 {
0223     return m_documentToLocal;
0224 }
0225 
0226 void AnnotationViewport::paint(QPainter *painter)
0227 {
0228     if (!m_document || m_viewportRect.isEmpty()) {
0229         return;
0230     }
0231 
0232     m_document->paint(painter, {m_viewportRect, m_zoom});
0233 }
0234 
0235 void AnnotationViewport::hoverEnterEvent(QHoverEvent *event)
0236 {
0237     if (shouldIgnoreInput()) {
0238         QQuickItem::hoverEnterEvent(event);
0239         return;
0240     }
0241     setHoverPosition(event->position());
0242     setHovered(true);
0243 }
0244 
0245 void AnnotationViewport::hoverMoveEvent(QHoverEvent *event)
0246 {
0247     if (shouldIgnoreInput()) {
0248         QQuickItem::hoverMoveEvent(event);
0249         return;
0250     }
0251     setHoverPosition(event->position());
0252 
0253     if (m_document->tool()->type() == AnnotationTool::SelectTool) {
0254         // Without as_const, QMatrix4x4 will go to General mode with operator()(row, column).
0255         // keep margin the same number of pixels regardless of zoom level.
0256         auto margin = 4 * std::as_const(m_documentToLocal)(0, 0); // m11/x scale
0257         QRectF forgivingRect{event->position(), QSizeF{0, 0}};
0258         forgivingRect.adjust(-margin, -margin, margin, margin);
0259         if (auto item = m_document->itemAt(m_localToDocument.mapRect(forgivingRect))) {
0260             auto &geometry = std::get<Traits::Geometry::Opt>(item->traits());
0261             setHoveredMousePath(geometry->mousePath);
0262         } else {
0263             setHoveredMousePath({});
0264         }
0265     } else {
0266         setHoveredMousePath({});
0267     }
0268 }
0269 
0270 void AnnotationViewport::hoverLeaveEvent(QHoverEvent *event)
0271 {
0272     if (shouldIgnoreInput()) {
0273         QQuickItem::hoverLeaveEvent(event);
0274         return;
0275     }
0276     setHovered(false);
0277 }
0278 
0279 void AnnotationViewport::mousePressEvent(QMouseEvent *event)
0280 {
0281     if (shouldIgnoreInput() || event->buttons() & ~acceptedMouseButtons()) {
0282         QQuickItem::mousePressEvent(event);
0283         return;
0284     }
0285 
0286     auto toolType = m_document->tool()->type();
0287     auto wrapper = m_document->selectedItemWrapper();
0288     auto pressPos = event->position();
0289     m_lastDocumentPressPos = m_localToDocument.map(pressPos);
0290 
0291     if (toolType == AnnotationTool::SelectTool) {
0292         auto margin = 4 * std::as_const(m_documentToLocal)(0, 0); // m11/x scale
0293         QRectF forgivingRect{pressPos, QSizeF{0, 0}};
0294         forgivingRect.adjust(-margin, -margin, margin, margin);
0295         m_document->selectItem(m_localToDocument.mapRect(forgivingRect));
0296     } else {
0297         wrapper->commitChanges();
0298         m_document->beginItem(m_lastDocumentPressPos);
0299     }
0300 
0301     m_allowDraggingSelection = toolType == AnnotationTool::SelectTool && wrapper->hasSelection();
0302 
0303     setHoveredMousePath({});
0304     setPressPosition(pressPos);
0305     setPressed(true);
0306     event->accept();
0307 }
0308 
0309 void AnnotationViewport::mouseMoveEvent(QMouseEvent *event)
0310 {
0311     if (shouldIgnoreInput() || event->buttons() & ~acceptedMouseButtons()) {
0312         QQuickItem::mouseMoveEvent(event);
0313         return;
0314     }
0315 
0316     auto toolType = m_document->tool()->type();
0317     auto mousePos = event->position();
0318     auto wrapper = m_document->selectedItemWrapper();
0319     if (toolType == AnnotationTool::SelectTool && wrapper->hasSelection() && m_allowDraggingSelection) {
0320         auto documentMousePos = m_localToDocument.map(event->position());
0321         auto dx = documentMousePos.x() - m_lastDocumentPressPos.x();
0322         auto dy = documentMousePos.y() - m_lastDocumentPressPos.y();
0323         wrapper->transform(dx, dy);
0324     } else if (toolType > AnnotationTool::SelectTool) {
0325         using ContinueOptions = AnnotationDocument::ContinueOptions;
0326         using ContinueOption = AnnotationDocument::ContinueOption;
0327         ContinueOptions options;
0328         if (event->modifiers() & Qt::ShiftModifier) {
0329             options |= ContinueOption::SnapAngle;
0330         }
0331         if (event->modifiers() & Qt::ControlModifier) {
0332             options |= ContinueOption::CenterResize;
0333         }
0334         m_document->continueItem(m_localToDocument.map(mousePos), options);
0335     }
0336 
0337     setHoveredMousePath({});
0338     event->accept();
0339 }
0340 
0341 void AnnotationViewport::mouseReleaseEvent(QMouseEvent *event)
0342 {
0343     if (shouldIgnoreInput() || event->buttons() & ~acceptedMouseButtons()) {
0344         QQuickItem::mouseReleaseEvent(event);
0345         return;
0346     }
0347 
0348     m_document->finishItem();
0349 
0350     auto toolType = m_document->tool()->type();
0351     auto wrapper = m_document->selectedItemWrapper();
0352     auto selectedOptions = wrapper->options();
0353     if (!selectedOptions.testFlag(AnnotationTool::TextOption) //
0354         && !m_document->isCurrentItemValid()) {
0355         m_document->popCurrentItem();
0356     } else if (toolType == AnnotationTool::SelectTool && wrapper->hasSelection()) {
0357         wrapper->commitChanges();
0358     } else if (!selectedOptions.testFlag(AnnotationTool::TextOption)) {
0359         m_document->deselectItem();
0360     }
0361 
0362     setPressed(false);
0363     event->accept();
0364 }
0365 
0366 void AnnotationViewport::keyPressEvent(QKeyEvent *event)
0367 {
0368     // For some reason, events are already accepted when they arrive.
0369     QQuickItem::keyPressEvent(event);
0370     if (shouldIgnoreInput()) {
0371         m_acceptKeyReleaseEvents = false;
0372         return;
0373     }
0374 
0375     const auto wrapper = m_document->selectedItemWrapper();
0376     const auto selectedOptions = wrapper->options();
0377     const auto toolType = m_document->tool()->type();
0378     if (wrapper->hasSelection()) {
0379         if (event->matches(QKeySequence::Cancel)) {
0380             m_document->deselectItem();
0381             if (!m_document->isCurrentItemValid()) {
0382                 m_document->popCurrentItem();
0383             }
0384             event->accept();
0385         } else if (event->matches(QKeySequence::Delete) //
0386                    && toolType == AnnotationTool::SelectTool //
0387                    && (!selectedOptions.testFlag(AnnotationTool::TextOption)
0388                        || wrapper->text().isEmpty())) {
0389             // Only use delete shortcut when not using the text tool.
0390             // We don't want users trying to delete text to accidentally delete the item.
0391             m_document->deleteSelectedItem();
0392             event->accept();
0393         }
0394     }
0395     m_acceptKeyReleaseEvents = event->isAccepted();
0396 }
0397 
0398 void AnnotationViewport::keyReleaseEvent(QKeyEvent *event)
0399 {
0400     // For some reason, events are already accepted when they arrive.
0401     if (shouldIgnoreInput()) {
0402         QQuickItem::keyReleaseEvent(event);
0403     } else {
0404         event->setAccepted(m_acceptKeyReleaseEvents);
0405     }
0406     m_acceptKeyReleaseEvents = false;
0407 }
0408 
0409 bool AnnotationViewport::shouldIgnoreInput() const
0410 {
0411     return !isEnabled() || !m_document || m_document->tool()->type() == AnnotationTool::NoTool;
0412 }
0413 
0414 void AnnotationViewport::repaintDocument(const QRectF &documentRect)
0415 {
0416     if (documentRect.isEmpty()) {
0417         update();
0418         return;
0419     }
0420 
0421     repaintDocumentRect(documentRect);
0422 }
0423 
0424 void AnnotationViewport::repaintDocumentRect(const QRectF &documentRect)
0425 {
0426     auto localRect = m_documentToLocal.mapRect(documentRect).toAlignedRect();
0427     // intersects returns false if either rect is empty
0428     if (boundingRect().intersects(localRect)) {
0429         update(localRect);
0430     }
0431 }
0432 
0433 void AnnotationViewport::setCursorForToolType()
0434 {
0435     if (m_document && isEnabled()) {
0436         if (m_document->tool()->type() == AnnotationTool::SelectTool) {
0437             setCursor(Qt::ArrowCursor);
0438         } else {
0439             setCursor(Qt::CrossCursor);
0440         }
0441     } else {
0442         unsetCursor();
0443     }
0444 }
0445 
0446 #include <moc_AnnotationViewport.cpp>