File indexing completed on 2024-04-28 16:13:35

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