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>