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 }