File indexing completed on 2024-05-12 12:58:01
0001 /* 0002 * This file is part of the KDE project 0003 * 0004 * SPDX-FileCopyrightText: 2013 Arjen Hiemstra <ahiemstra@heimr.nl> 0005 * 0006 * SPDX-License-Identifier: LGPL-2.0-or-later 0007 * 0008 */ 0009 0010 #include "ViewController.h" 0011 #include <KoToolManager.h> 0012 0013 #include <QDebug> 0014 #include <QTimer> 0015 #include <QQuickWindow> 0016 #include <QPainter> 0017 #include <QSGTransformNode> 0018 #include <QSGSimpleTextureNode> 0019 0020 #include <KoCanvasController.h> 0021 #include <KoCanvasBase.h> 0022 #include <KoShapeManager.h> 0023 #include <KoZoomMode.h> 0024 #include "gemini/ViewModeSwitchEvent.h" 0025 0026 #include "Document.h" 0027 #include "View.h" 0028 0029 using namespace Calligra::Components; 0030 0031 class ViewController::Private 0032 { 0033 public: 0034 Private() 0035 : view{nullptr} 0036 , flickable{nullptr} 0037 , canvasController{nullptr} 0038 , lastX{0.f} 0039 , lastY{0.f} 0040 , ignoreOffsetChange{false} 0041 , ignoreFlickableChange{false} 0042 , minimumZoom{.5f} 0043 , minimumZoomFitsWidth{false} 0044 , zoom{1.f} 0045 , zoomChange{0.f} 0046 , maximumZoom{2.f} 0047 , useZoomProxy{true} 0048 , zoomProxy{nullptr} 0049 , zoomTimer{nullptr} 0050 { } 0051 0052 ViewController* q; 0053 0054 View* view; 0055 QQuickItem* flickable; 0056 0057 KoCanvasController* canvasController; 0058 0059 float lastX; 0060 float lastY; 0061 bool ignoreOffsetChange; 0062 bool ignoreFlickableChange; 0063 0064 float newX; 0065 float newY; 0066 0067 float minimumZoom; 0068 bool minimumZoomFitsWidth; 0069 float zoom; 0070 float zoomChange; 0071 float maximumZoom; 0072 0073 bool useZoomProxy; 0074 QImage* zoomProxy; 0075 QTimer* zoomTimer; 0076 QVector3D zoomCenter; 0077 0078 QSizeF documentSize; 0079 }; 0080 0081 ViewController::ViewController(QQuickItem* parent) 0082 : QQuickItem{parent}, d{new Private} 0083 { 0084 setFlag(QQuickItem::ItemHasContents, true); 0085 0086 KoZoomMode::setMinimumZoom(d->minimumZoom); 0087 KoZoomMode::setMaximumZoom(d->maximumZoom); 0088 0089 d->zoomTimer = new QTimer{this}; 0090 d->zoomTimer->setInterval(500); 0091 d->zoomTimer->setSingleShot(true); 0092 connect(d->zoomTimer, &QTimer::timeout, this, &ViewController::zoomTimeout); 0093 } 0094 0095 ViewController::~ViewController() 0096 { 0097 delete d; 0098 } 0099 0100 View* ViewController::view() const 0101 { 0102 return d->view; 0103 } 0104 0105 void ViewController::setView(View* newView) 0106 { 0107 if(newView != d->view) { 0108 if(d->view) { 0109 if(d->view->document()) { 0110 if(d->canvasController) { 0111 disconnect(d->canvasController->proxyObject, &KoCanvasControllerProxyObject::moveDocumentOffset, this, &ViewController::documentOffsetChanged); 0112 } 0113 d->view->document()->disconnect(this); 0114 } 0115 disconnect(d->view, &View::documentChanged, this, &ViewController::documentChanged); 0116 } 0117 0118 d->view = newView; 0119 connect(d->view, &View::documentChanged, this, &ViewController::documentChanged); 0120 0121 if(d->view->document()) { 0122 documentChanged(); 0123 } else { 0124 d->canvasController = nullptr; 0125 } 0126 0127 emit viewChanged(); 0128 } 0129 } 0130 0131 QQuickItem* ViewController::flickable() const 0132 { 0133 return d->flickable; 0134 } 0135 0136 void ViewController::setFlickable(QQuickItem* item) 0137 { 0138 if(item != d->flickable) { 0139 if(item && item->metaObject()->indexOfProperty("contentWidth") == -1) { 0140 qWarning() << Q_FUNC_INFO << "Item does not seem to be a flickable, ignoring."; 0141 return; 0142 } 0143 0144 flickableWidthChanged(); 0145 0146 d->flickable = item; 0147 0148 if(item) { 0149 documentSizeChanged(); 0150 connect(d->flickable, SIGNAL(contentXChanged()), this, SLOT(contentPositionChanged()) ); 0151 connect(d->flickable, SIGNAL(contentYChanged()), this, SLOT(contentPositionChanged()) ); 0152 connect(d->flickable, &QQuickItem::widthChanged, this, &ViewController::flickableWidthChanged); 0153 } 0154 emit flickableChanged(); 0155 } 0156 } 0157 0158 float ViewController::minimumZoom() const 0159 { 0160 return d->minimumZoom; 0161 } 0162 0163 void ViewController::setMinimumZoom(float newValue) 0164 { 0165 if(newValue != d->minimumZoom) { 0166 d->minimumZoom = newValue; 0167 KoZoomMode::setMinimumZoom(newValue); 0168 emit minimumZoomChanged(); 0169 } 0170 } 0171 0172 bool ViewController::minimumZoomFitsWidth() const 0173 { 0174 return d->minimumZoomFitsWidth; 0175 } 0176 0177 void ViewController::setMinimumZoomFitsWidth(bool newValue) 0178 { 0179 if(newValue != d->minimumZoomFitsWidth) { 0180 d->minimumZoomFitsWidth = newValue; 0181 0182 flickableWidthChanged(); 0183 0184 emit minimumZoomFitsWidthChanged(); 0185 } 0186 } 0187 0188 float ViewController::zoom() const 0189 { 0190 if(d->useZoomProxy && d->zoomProxy) { 0191 return d->zoom + d->zoomChange; 0192 } 0193 0194 return d->zoom; 0195 } 0196 0197 void ViewController::setZoom(float newZoom) 0198 { 0199 newZoom = qBound(d->minimumZoom, newZoom, d->maximumZoom); 0200 if(newZoom != d->zoom) { 0201 if(d->useZoomProxy && d->view) { 0202 if(!d->zoomProxy) { 0203 d->zoomProxy = new QImage{int(d->flickable->width()), int(d->flickable->height()), QImage::Format_ARGB32}; 0204 0205 QPainter p; 0206 p.begin(d->zoomProxy); 0207 d->view->paint(&p); 0208 p.end(); 0209 0210 d->view->setVisible(false); 0211 } 0212 0213 if(d->zoomCenter.isNull()) { 0214 d->zoomCenter = QVector3D{ float(d->flickable->width()) / 2.f, float(d->flickable->height()) / 2.f, 0.f }; 0215 } 0216 d->zoomChange = newZoom - d->zoom; 0217 update(); 0218 d->zoomTimer->start(); 0219 } else { 0220 d->zoom = newZoom; 0221 0222 if(d->view) { 0223 d->view->setZoom(d->zoom); 0224 } 0225 } 0226 0227 emit zoomChanged(); 0228 } 0229 } 0230 0231 float ViewController::maximumZoom() const 0232 { 0233 return d->maximumZoom; 0234 } 0235 0236 void ViewController::setMaximumZoom(float newValue) 0237 { 0238 if(newValue != d->maximumZoom) { 0239 d->maximumZoom = newValue; 0240 KoZoomMode::setMaximumZoom(newValue); 0241 emit maximumZoomChanged(); 0242 } 0243 } 0244 0245 bool ViewController::useZoomProxy() const 0246 { void updateMinimumZoom(); 0247 0248 return d->useZoomProxy; 0249 } 0250 0251 void ViewController::setUseZoomProxy(bool proxy) 0252 { 0253 if(proxy != d->useZoomProxy) { 0254 d->useZoomProxy = proxy; 0255 0256 if(!d->useZoomProxy && d->zoomProxy) { 0257 delete d->zoomProxy; 0258 d->zoomProxy = nullptr; 0259 update(); 0260 } 0261 0262 emit useZoomProxyChanged(); 0263 } 0264 } 0265 0266 bool ViewController::event(QEvent* event) 0267 { 0268 switch(static_cast<int>(event->type())) { 0269 case ViewModeSwitchEvent::AboutToSwitchViewModeEvent: { 0270 ViewModeSynchronisationObject* syncObject = static_cast<ViewModeSwitchEvent*>(event)->synchronisationObject(); 0271 0272 if (d->canvasController) { 0273 syncObject->scrollBarValue = d->canvasController->documentOffset(); 0274 syncObject->zoomLevel = zoom(); 0275 syncObject->activeToolId = KoToolManager::instance()->activeToolId(); 0276 syncObject->shapes = d->canvasController->canvas()->shapeManager()->shapes(); 0277 syncObject->currentIndex = d->view->document()->currentIndex(); 0278 syncObject->initialized = true; 0279 } 0280 0281 return true; 0282 } 0283 case ViewModeSwitchEvent::SwitchedToTouchModeEvent: { 0284 ViewModeSynchronisationObject* syncObject = static_cast<ViewModeSwitchEvent*>(event)->synchronisationObject(); 0285 0286 if (d->canvasController && syncObject->initialized) { 0287 d->canvasController->canvas()->shapeManager()->setShapes(syncObject->shapes); 0288 0289 KoToolManager::instance()->switchToolRequested("PageToolFactory_ID"); 0290 qApp->processEvents(); 0291 0292 setZoom(syncObject->zoomLevel); 0293 0294 qApp->processEvents(); 0295 if(syncObject->scrollBarValue.isNull()) { 0296 d->view->document()->setCurrentIndex(syncObject->currentIndex); 0297 } 0298 else { 0299 d->canvasController->setScrollBarValue(syncObject->scrollBarValue); 0300 } 0301 emit d->view->document()->requestViewUpdate(); 0302 } 0303 0304 return true; 0305 } 0306 } 0307 return QQuickItem::event(event); 0308 } 0309 0310 void ViewController::zoomAroundPoint(float amount, float x, float y) 0311 { 0312 d->zoomCenter = QVector3D{-1 * x, y, 0.f}; 0313 setZoom(zoom() + amount); 0314 } 0315 0316 void ViewController::zoomToFitWidth(float width) 0317 { 0318 if( width < 0.01f ) 0319 return; 0320 0321 if( d->zoom < 0.01f ) 0322 return; 0323 0324 if( d->documentSize.width() > 0.f && d->documentSize.width() < 2e6 ) 0325 setZoom( width / ( d->documentSize.width() / d->zoom ) ); 0326 } 0327 0328 QSGNode* ViewController::updatePaintNode(QSGNode* node, QQuickItem::UpdatePaintNodeData* ) 0329 { 0330 if(!d->zoomProxy) { 0331 if(node) { 0332 delete node; 0333 } 0334 return 0; 0335 } 0336 0337 auto root = static_cast<QSGTransformNode*>(node); 0338 if(!root) { 0339 root = new QSGTransformNode{}; 0340 } 0341 0342 QMatrix4x4 itemToView; 0343 itemToView.translate(QVector3D{d->flickable->property("contentX").toFloat(), d->flickable->property("contentY").toFloat(), 0.f} + d->zoomCenter); 0344 root->setMatrix(itemToView); 0345 0346 auto center = static_cast<QSGTransformNode*>(root->firstChild()); 0347 if(!center) { 0348 center = new QSGTransformNode{}; 0349 root->appendChildNode(center); 0350 } 0351 0352 float newScale = 1.f + d->zoomChange; 0353 0354 QMatrix4x4 centerToView; 0355 centerToView.scale(newScale); 0356 0357 float newWidth = d->zoomProxy->width() * newScale; 0358 float newHeight = d->zoomProxy->height() * newScale; 0359 0360 float left = -newWidth * (d->zoomCenter.x() / newWidth); 0361 float top = -newHeight * (d->zoomCenter.y() / newHeight); 0362 0363 d->newX = -left; 0364 d->newY = -top; 0365 0366 centerToView.translate(left, top); 0367 center->setMatrix(centerToView); 0368 0369 auto texNode = static_cast<QSGSimpleTextureNode*>(center->firstChild()); 0370 if(!texNode) { 0371 texNode = new QSGSimpleTextureNode{}; 0372 center->appendChildNode(texNode); 0373 } 0374 texNode->setRect(d->zoomProxy->rect()); 0375 0376 auto texture = window()->createTextureFromImage(*d->zoomProxy); 0377 if(texNode->texture()) { 0378 delete texNode->texture(); 0379 } 0380 texNode->setTexture(texture); 0381 0382 return root; 0383 } 0384 0385 void ViewController::contentPositionChanged() 0386 { 0387 if(!d->canvasController || d->ignoreFlickableChange) 0388 return; 0389 0390 float newX = d->flickable->property("contentX").toFloat(); 0391 float newY = d->flickable->property("contentY").toFloat(); 0392 0393 //TODO: The rounding here causes some issues at edges. Need to investigate how to fix it. 0394 QPointF diff = QPointF{newX - d->lastX, newY - d->lastY}; 0395 d->ignoreOffsetChange = true; 0396 d->canvasController->pan(diff.toPoint()); 0397 d->ignoreOffsetChange = false; 0398 0399 d->lastX = newX; 0400 d->lastY = newY; 0401 0402 emit d->view->document()->requestViewUpdate(); 0403 } 0404 0405 void ViewController::documentChanged() 0406 { 0407 connect(d->view->document(), &Document::statusChanged, this, &ViewController::documentStatusChanged); 0408 connect(d->view->document(), &Document::documentSizeChanged, this, &ViewController::documentSizeChanged); 0409 documentStatusChanged(); 0410 documentSizeChanged(); 0411 } 0412 0413 void ViewController::documentSizeChanged() 0414 { 0415 if(d->view && d->view->document() && d->flickable) { 0416 if(!d->canvasController) { 0417 d->canvasController = d->view->document()->canvasController(); 0418 } 0419 0420 d->documentSize = d->view->document()->documentSize(); 0421 //Limit the size of this item to always be at least the same size 0422 //as the flickable, since otherwise we can end up with situations 0423 //where children cannot interact, for example when using the 0424 //LinkArea as a child of this item. 0425 setWidth(qMax(d->flickable->width() - 1, d->documentSize.width())); 0426 setHeight(qMax(d->flickable->height() - 1, d->documentSize.height())); 0427 0428 d->flickable->setProperty("contentWidth", width()); 0429 d->flickable->setProperty("contentHeight", height()); 0430 0431 flickableWidthChanged(); 0432 } 0433 } 0434 0435 void ViewController::documentStatusChanged() 0436 { 0437 if(d->view->document()->status() == DocumentStatus::Loaded) { 0438 d->canvasController = d->view->document()->canvasController(); 0439 connect(d->canvasController->proxyObject, &KoCanvasControllerProxyObject::moveDocumentOffset, this, &ViewController::documentOffsetChanged); 0440 } 0441 } 0442 0443 void ViewController::documentOffsetChanged(const QPoint& offset) 0444 { 0445 if(d->ignoreOffsetChange || !d->flickable) { 0446 return; 0447 } 0448 0449 d->ignoreFlickableChange = true; 0450 d->flickable->setProperty("contentX", offset.x()); 0451 d->flickable->setProperty("contentY", offset.y()); 0452 d->ignoreFlickableChange = false; 0453 0454 d->lastX = offset.x(); 0455 d->lastY = offset.y(); 0456 0457 QMetaObject::invokeMethod(d->flickable, "returnToBounds"); 0458 } 0459 0460 void ViewController::zoomTimeout() 0461 { 0462 delete d->zoomProxy; 0463 d->zoomProxy = nullptr; 0464 0465 float newZoom = d->zoom + d->zoomChange; 0466 0467 float oldX = d->flickable->property("contentX").toReal(); 0468 float oldY = d->flickable->property("contentY").toReal(); 0469 0470 0471 float z = 1.0 + d->zoomChange; 0472 d->flickable->setProperty("contentX", oldX + ((d->zoomCenter.x() * z - d->zoomCenter.x())) ); 0473 d->flickable->setProperty("contentY", oldY + ((d->zoomCenter.y() * z - d->zoomCenter.y())) ); 0474 0475 QMetaObject::invokeMethod(d->flickable, "returnToBounds"); 0476 0477 d->zoom = newZoom; 0478 0479 d->ignoreOffsetChange = true; 0480 d->view->setZoom(newZoom); 0481 d->ignoreOffsetChange = false; 0482 0483 d->view->setVisible(true); 0484 d->zoomCenter = QVector3D{}; 0485 update(); 0486 } 0487 0488 void ViewController::flickableWidthChanged() 0489 { 0490 if(d->minimumZoomFitsWidth && d->flickable && d->documentSize.width() > 0.f) { 0491 setMinimumZoom(d->flickable->width() / (d->documentSize.width() / d->zoom)); 0492 setZoom(d->zoom); 0493 } 0494 }