1 /*
2  *  Copyright (c) 2012 Dmitry Kazakov <dimula73@gmail.com>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "kis_zoom_and_pan_test.h"
20 
21 #include <cmath>
22 #include <QTest>
23 
24 #include <kis_filter_configuration.h>
25 #include "testutil.h"
26 #include "qimage_based_test.h"
27 
28 #include <kactioncollection.h>
29 
30 #include "kis_config.h"
31 
32 #include "KisMainWindow.h"
33 #include "KoZoomController.h"
34 #include "KisDocument.h"
35 #include "KisPart.h"
36 #include "KisViewManager.h"
37 #include "KisView.h"
38 #include "kis_canvas2.h"
39 #include "kis_canvas_controller.h"
40 #include "kis_coordinates_converter.h"
41 #include "kis_filter_strategy.h"
42 
43 #include "kistest.h"
44 
45 class ZoomAndPanTester : public TestUtil::QImageBasedTest
46 {
47 public:
ZoomAndPanTester()48     ZoomAndPanTester()
49         // we are not going to use our own QImage sets,so
50         // just exploit the set of the selection manager test
51         : QImageBasedTest("selection_manager_test")
52     {
53         m_undoStore = new KisSurrogateUndoStore();
54         m_image = createImage(m_undoStore);
55         m_image->initialRefreshGraph();
56         QVERIFY(checkLayersInitial(m_image));
57 
58         m_doc = KisPart::instance()->createDocument();
59 
60         m_doc->setCurrentImage(m_image);
61 
62         m_mainWindow = KisPart::instance()->createMainWindow();
63         m_view = new KisView(m_doc, m_mainWindow->viewManager(), m_mainWindow);
64 
65         m_image->refreshGraph();
66 
67         m_mainWindow->show();
68     }
69 
~ZoomAndPanTester()70     ~ZoomAndPanTester() {
71         m_image->waitForDone();
72         QApplication::processEvents();
73 
74         delete m_mainWindow;
75         delete m_doc;
76 
77         /**
78          * The event queue may have up to 200k events
79          * by the time all the tests are finished. Removing
80          * all of them may last forever, so clear them after
81          * every single test is finished
82          */
83         QApplication::removePostedEvents(0);
84     }
85 
view()86     QPointer<KisView> view() {
87         return m_view;
88     }
89 
mainWindow()90     KisMainWindow* mainWindow() {
91         return m_mainWindow;
92     }
93 
image()94     KisImageWSP image() {
95         return m_image;
96     }
97 
canvas()98     KisCanvas2* canvas() {
99         return m_view->canvasBase();
100     }
101 
canvasWidget()102     QWidget* canvasWidget() {
103         return m_view->canvasBase()->canvasWidget();
104     }
105 
zoomController()106     KoZoomController* zoomController() {
107         return m_view->zoomController();
108     }
109 
canvasController()110     KisCanvasController* canvasController() {
111         return dynamic_cast<KisCanvasController*>(m_view->canvasController());
112     }
113 
coordinatesConverter()114     const KisCoordinatesConverter* coordinatesConverter() {
115         return m_view->canvasBase()->coordinatesConverter();
116     }
117 
118 private:
119     KisSurrogateUndoStore *m_undoStore;
120     KisImageSP m_image;
121     KisDocument *m_doc;
122     QPointer<KisView>m_view;
123     KisMainWindow *m_mainWindow;
124 };
125 
126 template<class P, class T>
compareWithRounding(const P & pt0,const P & pt1,T tolerance)127 inline bool compareWithRounding(const P &pt0, const P &pt1, T tolerance)
128 {
129     return qAbs(pt0.x() - pt1.x()) <= tolerance &&
130         qAbs(pt0.y() - pt1.y()) <= tolerance;
131 }
132 
verifyOffset(ZoomAndPanTester & t,const QPoint & offset)133 bool verifyOffset(ZoomAndPanTester &t, const QPoint &offset) {
134 
135     if (t.coordinatesConverter()->documentOffset() != offset) {
136             dbgKrita << "########################";
137             dbgKrita << "Expected Offset:" << offset;
138             dbgKrita << "Actual values:";
139             dbgKrita << "Offset:" << t.coordinatesConverter()->documentOffset();
140             dbgKrita << "wsize:"  << t.canvasWidget()->size();
141             dbgKrita << "vport:"  << t.canvasController()->viewportSize();
142             dbgKrita << "pref:"  << t.canvasController()->preferredCenter();
143             dbgKrita << "########################";
144     }
145 
146     return t.coordinatesConverter()->documentOffset() == offset;
147 }
148 
checkPan(ZoomAndPanTester & t,QPoint shift)149 bool KisZoomAndPanTest::checkPan(ZoomAndPanTester &t, QPoint shift)
150 {
151     QPoint oldOffset = t.coordinatesConverter()->documentOffset();
152     QPointF oldPrefCenter = t.canvasController()->preferredCenter();
153 
154     t.canvasController()->pan(shift);
155 
156     QPoint newOffset  = t.coordinatesConverter()->documentOffset();
157     QPointF newPrefCenter = t.canvasController()->preferredCenter();
158     QPointF newTopLeft = t.coordinatesConverter()->imageRectInWidgetPixels().topLeft();
159 
160     QPoint expectedOffset  = oldOffset + shift;
161     QPointF expectedPrefCenter = oldPrefCenter + shift;
162 
163 
164     // no tolerance accepted for pan
165     bool offsetAsExpected = newOffset == expectedOffset;
166 
167     // rounding can happen due to the scroll bars being the main
168     // source of the offset
169     bool preferredCenterAsExpected =
170         compareWithRounding(expectedPrefCenter, newPrefCenter, 1.0);
171 
172     bool topLeftAsExpected = newTopLeft.toPoint() == -newOffset;
173 
174     if (!offsetAsExpected ||
175         !preferredCenterAsExpected ||
176         !topLeftAsExpected) {
177 
178         dbgKrita << "***** PAN *****************";
179 
180         if(!offsetAsExpected) {
181             dbgKrita << " ### Offset invariant broken";
182         }
183 
184         if(!preferredCenterAsExpected) {
185             dbgKrita << " ### Preferred center invariant broken";
186         }
187 
188         if(!topLeftAsExpected) {
189             dbgKrita << " ### TopLeft invariant broken";
190         }
191 
192         dbgKrita << ppVar(expectedOffset);
193         dbgKrita << ppVar(expectedPrefCenter);
194         dbgKrita << ppVar(oldOffset) << ppVar(newOffset);
195         dbgKrita << ppVar(oldPrefCenter) << ppVar(newPrefCenter);
196         dbgKrita << ppVar(newTopLeft);
197         dbgKrita << "***************************";
198     }
199 
200     return offsetAsExpected && preferredCenterAsExpected && topLeftAsExpected;
201 }
202 
checkInvariants(const QPointF & baseFlakePoint,const QPoint & oldOffset,const QPointF & oldPreferredCenter,qreal oldZoom,const QPoint & newOffset,const QPointF & newPreferredCenter,qreal newZoom,const QPointF & newTopLeft,const QSize & oldDocumentSize)203 bool KisZoomAndPanTest::checkInvariants(const QPointF &baseFlakePoint,
204                                         const QPoint &oldOffset,
205                                         const QPointF &oldPreferredCenter,
206                                         qreal oldZoom,
207                                         const QPoint &newOffset,
208                                         const QPointF &newPreferredCenter,
209                                         qreal newZoom,
210                                         const QPointF &newTopLeft,
211                                         const QSize &oldDocumentSize)
212 {
213     qreal k = newZoom / oldZoom;
214 
215     QPointF expectedOffset = oldOffset + (k - 1) * baseFlakePoint;
216     QPointF expectedPreferredCenter = oldPreferredCenter + (k - 1) * baseFlakePoint;
217 
218     qreal oldPreferredCenterFractionX = 1.0 * oldPreferredCenter.x() / oldDocumentSize.width();
219     qreal oldPreferredCenterFractionY = 1.0 * oldPreferredCenter.y() / oldDocumentSize.height();
220 
221     qreal roundingTolerance =
222         qMax(qreal(1.0), qMax(oldPreferredCenterFractionX, oldPreferredCenterFractionY) / k);
223 
224     /**
225      * In the computation of the offset two roundings happen:
226      * first for the computation of oldOffset and the second
227      * for the computation of newOffset. So the maximum tolerance
228      * should equal 2.
229      */
230     bool offsetAsExpected =
231         compareWithRounding(expectedOffset, QPointF(newOffset), 2 * roundingTolerance);
232 
233     /**
234      * Rounding for the preferred center happens due to the rounding
235      * of the document size while zooming. The wider the step of the
236      * zooming, the bigger tolerance should be
237      */
238     bool preferredCenterAsExpected =
239         compareWithRounding(expectedPreferredCenter, newPreferredCenter,
240                             roundingTolerance);
241 
242     bool topLeftAsExpected = newTopLeft.toPoint() == -newOffset;
243 
244     if (!offsetAsExpected ||
245         !preferredCenterAsExpected ||
246         !topLeftAsExpected) {
247 
248         dbgKrita << "***** ZOOM ****************";
249 
250         if(!offsetAsExpected) {
251             dbgKrita << " ### Offset invariant broken";
252         }
253 
254         if(!preferredCenterAsExpected) {
255             dbgKrita << " ### Preferred center invariant broken";
256         }
257 
258         if(!topLeftAsExpected) {
259             dbgKrita << " ### TopLeft invariant broken";
260         }
261 
262         dbgKrita << ppVar(expectedOffset);
263         dbgKrita << ppVar(expectedPreferredCenter);
264         dbgKrita << ppVar(oldOffset) << ppVar(newOffset);
265         dbgKrita << ppVar(oldPreferredCenter) << ppVar(newPreferredCenter);
266         dbgKrita << ppVar(oldPreferredCenterFractionX);
267         dbgKrita << ppVar(oldPreferredCenterFractionY);
268         dbgKrita << ppVar(oldZoom) << ppVar(newZoom);
269         dbgKrita << ppVar(baseFlakePoint);
270         dbgKrita << ppVar(newTopLeft);
271         dbgKrita << ppVar(roundingTolerance);
272         dbgKrita << "***************************";
273     }
274 
275     return offsetAsExpected && preferredCenterAsExpected && topLeftAsExpected;
276 }
277 
checkZoomWithAction(ZoomAndPanTester & t,qreal newZoom,bool limitedZoom)278 bool KisZoomAndPanTest::checkZoomWithAction(ZoomAndPanTester &t, qreal newZoom, bool limitedZoom)
279 {
280     QPoint oldOffset = t.coordinatesConverter()->documentOffset();
281     QPointF oldPrefCenter = t.canvasController()->preferredCenter();
282     qreal oldZoom = t.zoomController()->zoomAction()->effectiveZoom();
283     QSize oldDocumentSize = t.canvasController()->documentSize().toSize();
284 
285     t.zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, newZoom);
286 
287     QPointF newTopLeft = t.coordinatesConverter()->imageRectInWidgetPixels().topLeft();
288 
289     return checkInvariants(oldPrefCenter,
290                            oldOffset,
291                            oldPrefCenter,
292                            oldZoom,
293                            t.coordinatesConverter()->documentOffset(),
294                            t.canvasController()->preferredCenter(),
295                            limitedZoom ? oldZoom : newZoom,
296                            newTopLeft,
297                            oldDocumentSize);
298 }
299 
checkZoomWithWheel(ZoomAndPanTester & t,const QPoint & widgetPoint,qreal zoomCoeff,bool limitedZoom)300 bool KisZoomAndPanTest::checkZoomWithWheel(ZoomAndPanTester &t, const QPoint &widgetPoint, qreal zoomCoeff, bool limitedZoom)
301 {
302     QPoint oldOffset = t.coordinatesConverter()->documentOffset();
303     QPointF oldPrefCenter = t.canvasController()->preferredCenter();
304     qreal oldZoom = t.zoomController()->zoomAction()->effectiveZoom();
305     QSize oldDocumentSize = t.canvasController()->documentSize().toSize();
306 
307     t.canvasController()->zoomRelativeToPoint(widgetPoint, zoomCoeff);
308 
309     QPointF newTopLeft = t.coordinatesConverter()->imageRectInWidgetPixels().topLeft();
310 
311     return checkInvariants(oldOffset + widgetPoint,
312                            oldOffset,
313                            oldPrefCenter,
314                            oldZoom,
315                            t.coordinatesConverter()->documentOffset(),
316                            t.canvasController()->preferredCenter(),
317                            limitedZoom ? oldZoom : zoomCoeff * oldZoom,
318                            newTopLeft,
319                            oldDocumentSize);
320 }
321 
testZoom100ChangingWidgetSize()322 void KisZoomAndPanTest::testZoom100ChangingWidgetSize()
323 {
324     ZoomAndPanTester t;
325 
326     QCOMPARE(t.image()->size(), QSize(640,441));
327     QCOMPARE(t.image()->xRes(), 1.0);
328     QCOMPARE(t.image()->yRes(), 1.0);
329 
330     t.canvasController()->resize(QSize(1000,1000));
331     t.zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, 1.0);
332     t.canvasController()->setPreferredCenter(QPoint(320,220));
333 
334     QCOMPARE(t.canvasWidget()->size(), QSize(983,983));
335     QCOMPARE(t.canvasWidget()->size(), t.canvasController()->viewportSize().toSize());
336     QVERIFY(verifyOffset(t, QPoint(-171,-271)));
337 
338     t.canvasController()->resize(QSize(700,700));
339 
340     QCOMPARE(t.canvasWidget()->size(), QSize(683,683));
341     QCOMPARE(t.canvasWidget()->size(), t.canvasController()->viewportSize().toSize());
342     QVERIFY(verifyOffset(t, QPoint(-171,-271)));
343 
344     t.canvasController()->setPreferredCenter(QPoint(320,220));
345 
346     QVERIFY(verifyOffset(t, QPoint(-21,-121)));
347 
348     t.canvasController()->resize(QSize(400,400));
349 
350     QCOMPARE(t.canvasWidget()->size(), QSize(383,383));
351     QCOMPARE(t.canvasWidget()->size(), t.canvasController()->viewportSize().toSize());
352     QVERIFY(verifyOffset(t, QPoint(-21,-121)));
353 
354     t.canvasController()->setPreferredCenter(QPoint(320,220));
355 
356     QVERIFY(verifyOffset(t, QPoint(129,29)));
357 
358     t.canvasController()->pan(QPoint(100,100));
359 
360     QVERIFY(verifyOffset(t, QPoint(229,129)));
361 }
362 
initializeViewport(ZoomAndPanTester & t,bool fullscreenMode,bool rotate,bool mirror)363 void KisZoomAndPanTest::initializeViewport(ZoomAndPanTester &t, bool fullscreenMode, bool rotate, bool mirror)
364 {
365     QCOMPARE(t.image()->size(), QSize(640,441));
366     QCOMPARE(t.image()->xRes(), 1.0);
367     QCOMPARE(t.image()->yRes(), 1.0);
368 
369     t.canvasController()->resize(QSize(500,500));
370     t.zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, 1.0);
371     t.canvasController()->setPreferredCenter(QPoint(320,220));
372 
373     QCOMPARE(t.canvasWidget()->size(), QSize(483,483));
374     QCOMPARE(t.canvasWidget()->size(), t.canvasController()->viewportSize().toSize());
375     QVERIFY(verifyOffset(t, QPoint(79,-21)));
376 
377     if (fullscreenMode) {
378         QCOMPARE(t.canvasController()->preferredCenter(), QPointF(320,220));
379 
380         QAction *action = t.view()->viewManager()->actionCollection()->action("view_show_canvas_only");
381         action->setChecked(true);
382 
383         QVERIFY(verifyOffset(t, QPoint(79,-21)));
384         QCOMPARE(t.canvasController()->preferredCenter(), QPointF(329,220));
385 
386 
387         t.canvasController()->resize(QSize(483,483));
388         QCOMPARE(t.canvasWidget()->size(), QSize(483,483));
389         QCOMPARE(t.canvasWidget()->size(), t.canvasController()->viewportSize().toSize());
390         QVERIFY(verifyOffset(t, QPoint(79,-21)));
391 
392 
393         /**
394          * FIXME: here is a small flaw in KoCanvasControllerWidget
395          * We cannot set the center point explicitly, because it'll be rounded
396          * up by recenterPreferred function, so real center point will be
397          * different. Make the preferredCenter() return real center of the
398          * image instead of the set value
399          */
400         QCOMPARE(t.canvasController()->preferredCenter(), QPointF(320.5,220));
401     }
402 
403     if (rotate) {
404         t.canvasController()->rotateCanvas(90);
405         QVERIFY(verifyOffset(t, QPoint(-21,79)));
406         QVERIFY(compareWithRounding(QPointF(220,320), t.canvasController()->preferredCenter(), 2));
407         QCOMPARE(t.coordinatesConverter()->imageRectInWidgetPixels().topLeft().toPoint(), -t.coordinatesConverter()->documentOffset());
408     }
409 
410     if (mirror) {
411         t.canvasController()->mirrorCanvas(true);
412         QVERIFY(verifyOffset(t, QPoint(78, -21)));
413         QVERIFY(compareWithRounding(QPointF(320,220), t.canvasController()->preferredCenter(), 2));
414         QCOMPARE(t.coordinatesConverter()->imageRectInWidgetPixels().topLeft().toPoint(), -t.coordinatesConverter()->documentOffset());
415     }
416 }
417 
testSequentialActionZoomAndPan(bool fullscreenMode,bool rotate,bool mirror)418 void KisZoomAndPanTest::testSequentialActionZoomAndPan(bool fullscreenMode, bool rotate, bool mirror)
419 {
420     ZoomAndPanTester t;
421     initializeViewport(t, fullscreenMode, rotate, mirror);
422 
423     QVERIFY(checkZoomWithAction(t, 0.5));
424     QVERIFY(checkPan(t, QPoint(100,100)));
425 
426     QVERIFY(checkZoomWithAction(t, 0.25));
427     QVERIFY(checkPan(t, QPoint(-100,-100)));
428 
429     QVERIFY(checkZoomWithAction(t, 0.35));
430     QVERIFY(checkPan(t, QPoint(100,100)));
431 
432     QVERIFY(checkZoomWithAction(t, 0.45));
433     QVERIFY(checkPan(t, QPoint(100,100)));
434 
435     QVERIFY(checkZoomWithAction(t, 0.85));
436     QVERIFY(checkPan(t, QPoint(-100,-100)));
437 
438     QVERIFY(checkZoomWithAction(t, 2.35));
439     QVERIFY(checkPan(t, QPoint(100,100)));
440 }
441 
testSequentialWheelZoomAndPan(bool fullscreenMode,bool rotate,bool mirror)442 void KisZoomAndPanTest::testSequentialWheelZoomAndPan(bool fullscreenMode, bool rotate, bool mirror)
443 {
444     ZoomAndPanTester t;
445     initializeViewport(t, fullscreenMode, rotate, mirror);
446 
447     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 0.5));
448     QVERIFY(checkPan(t, QPoint(100,100)));
449 
450     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 0.5));
451     QVERIFY(checkPan(t, QPoint(-100,-100)));
452 
453     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 1.25));
454     QVERIFY(checkPan(t, QPoint(100,100)));
455 
456     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 1.5));
457     QVERIFY(checkPan(t, QPoint(100,100)));
458 
459     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 2.5));
460     QVERIFY(checkPan(t, QPoint(-100,-100)));
461 
462     // check one point which is outside the widget
463     QVERIFY(checkZoomWithWheel(t, QPoint(-100,100), 2.5));
464     QVERIFY(checkPan(t, QPoint(-100,-100)));
465 
466     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 0.5));
467     QVERIFY(checkPan(t, QPoint(-100,-100)));
468 }
469 
testSequentialActionZoomAndPan()470 void KisZoomAndPanTest::testSequentialActionZoomAndPan()
471 {
472     testSequentialActionZoomAndPan(false, false, false);
473 }
474 
testSequentialActionZoomAndPanFullscreen()475 void KisZoomAndPanTest::testSequentialActionZoomAndPanFullscreen()
476 {
477     testSequentialActionZoomAndPan(true, false, false);
478 }
479 
testSequentialActionZoomAndPanRotate()480 void KisZoomAndPanTest::testSequentialActionZoomAndPanRotate()
481 {
482     testSequentialActionZoomAndPan(false, true, false);
483 }
484 
testSequentialActionZoomAndPanRotateFullscreen()485 void KisZoomAndPanTest::testSequentialActionZoomAndPanRotateFullscreen()
486 {
487     testSequentialActionZoomAndPan(true, true, false);
488 }
489 
testSequentialActionZoomAndPanMirror()490 void KisZoomAndPanTest::testSequentialActionZoomAndPanMirror()
491 {
492     testSequentialActionZoomAndPan(false, false, true);
493 }
494 
testSequentialWheelZoomAndPan()495 void KisZoomAndPanTest::testSequentialWheelZoomAndPan()
496 {
497     testSequentialWheelZoomAndPan(false, false, false);
498 }
499 
testSequentialWheelZoomAndPanFullscreen()500 void KisZoomAndPanTest::testSequentialWheelZoomAndPanFullscreen()
501 {
502     testSequentialWheelZoomAndPan(true, false, false);
503 }
504 
testSequentialWheelZoomAndPanRotate()505 void KisZoomAndPanTest::testSequentialWheelZoomAndPanRotate()
506 {
507     testSequentialWheelZoomAndPan(false, true, false);
508 }
509 
testSequentialWheelZoomAndPanRotateFullscreen()510 void KisZoomAndPanTest::testSequentialWheelZoomAndPanRotateFullscreen()
511 {
512     testSequentialWheelZoomAndPan(true, true, false);
513 }
514 
testSequentialWheelZoomAndPanMirror()515 void KisZoomAndPanTest::testSequentialWheelZoomAndPanMirror()
516 {
517     testSequentialWheelZoomAndPan(false, false, true);
518 }
519 
testZoomOnBorderZoomLevels()520 void KisZoomAndPanTest::testZoomOnBorderZoomLevels()
521 {
522     ZoomAndPanTester t;
523     initializeViewport(t, false, false, false);
524 
525 //    QPoint widgetPoint(100,100);
526 
527     warnKrita << "WARNING: testZoomOnBorderZoomLevels() is disabled due to some changes in KoZoomMode::minimum/maximumZoom()";
528     return;
529 
530     // test min zoom level
531     t.zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, KoZoomMode::minimumZoom());
532     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 0.5, true));
533     QVERIFY(checkZoomWithAction(t, KoZoomMode::minimumZoom() * 0.5, true));
534 
535     // test max zoom level
536     t.zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, KoZoomMode::maximumZoom());
537     QVERIFY(checkZoomWithWheel(t, QPoint(100,100), 2.0, true));
538     QVERIFY(checkZoomWithAction(t, KoZoomMode::maximumZoom() * 2.0, true));
539 }
540 
correctionMatrix(qreal angle)541 inline QTransform correctionMatrix(qreal angle)
542 {
543     return QTransform(0,0,0,sin(M_PI * angle / 180),0,0,0,0,1);
544 }
545 
checkRotation(ZoomAndPanTester & t,qreal angle)546 bool KisZoomAndPanTest::checkRotation(ZoomAndPanTester &t, qreal angle)
547 {
548     // save old values
549     QPoint oldOffset = t.coordinatesConverter()->documentOffset();
550     QPointF oldCenteringCorrection = t.coordinatesConverter()->centeringCorrection();
551     QPointF oldPreferredCenter = t.canvasController()->preferredCenter();
552     QPointF oldRealCenterPoint = t.coordinatesConverter()->widgetToImage(t.coordinatesConverter()->widgetCenterPoint());
553     QSize oldDocumentSize = t.canvasController()->documentSize().toSize();
554 
555     qreal baseAngle = t.coordinatesConverter()->rotationAngle();
556     t.canvasController()->rotateCanvas(angle);
557 
558     // save result values
559     QPoint newOffset = t.coordinatesConverter()->documentOffset();
560     QPointF newCenteringCorrection = t.coordinatesConverter()->centeringCorrection();
561     QPointF newPreferredCenter = t.canvasController()->preferredCenter();
562     QPointF newRealCenterPoint = t.coordinatesConverter()->widgetToImage(t.coordinatesConverter()->widgetCenterPoint());
563     QSize newDocumentSize = t.canvasController()->documentSize().toSize();
564 
565 
566     // calculate theoretical preferred center
567     QTransform rot;
568     rot.rotate(angle);
569 
570     QSizeF dSize = t.coordinatesConverter()->imageSizeInFlakePixels();
571     QPointF dPoint(dSize.width(), dSize.height());
572 
573     QPointF expectedPreferredCenter =
574         (oldPreferredCenter - dPoint * correctionMatrix(baseAngle)) * rot +
575          dPoint * correctionMatrix(baseAngle + angle);
576 
577     // calculate theoretical offset based on the real preferred center
578     QPointF wPoint(t.canvasWidget()->size().width(), t.canvasWidget()->size().height());
579     QPointF expectedOldOffset = oldPreferredCenter - 0.5 * wPoint;
580     QPointF expectedNewOffset = newPreferredCenter - 0.5 * wPoint;
581 
582     bool preferredCenterAsExpected =
583         compareWithRounding(expectedPreferredCenter, newPreferredCenter, 2);
584     bool oldOffsetAsExpected =
585         compareWithRounding(expectedOldOffset + oldCenteringCorrection, QPointF(oldOffset), 2);
586     bool newOffsetAsExpected =
587         compareWithRounding(expectedNewOffset + newCenteringCorrection, QPointF(newOffset), 3);
588 
589     qreal zoom = t.zoomController()->zoomAction()->effectiveZoom();
590     bool realCenterPointAsExpected =
591         compareWithRounding(oldRealCenterPoint, newRealCenterPoint, 2/zoom);
592 
593 
594     if (!oldOffsetAsExpected ||
595         !newOffsetAsExpected ||
596         !preferredCenterAsExpected ||
597         !realCenterPointAsExpected) {
598 
599         dbgKrita << "***** ROTATE **************";
600 
601         if(!oldOffsetAsExpected) {
602             dbgKrita << " ### Old offset invariant broken";
603         }
604 
605         if(!newOffsetAsExpected) {
606             dbgKrita << " ### New offset invariant broken";
607         }
608 
609         if(!preferredCenterAsExpected) {
610             dbgKrita << " ### Preferred center invariant broken";
611         }
612 
613         if(!realCenterPointAsExpected) {
614             dbgKrita << " ### *Real* center invariant broken";
615         }
616 
617         dbgKrita << ppVar(expectedOldOffset);
618         dbgKrita << ppVar(expectedNewOffset);
619         dbgKrita << ppVar(expectedPreferredCenter);
620         dbgKrita << ppVar(oldOffset) << ppVar(newOffset);
621         dbgKrita << ppVar(oldCenteringCorrection) << ppVar(newCenteringCorrection);
622         dbgKrita << ppVar(oldPreferredCenter) << ppVar(newPreferredCenter);
623         dbgKrita << ppVar(oldRealCenterPoint) << ppVar(newRealCenterPoint);
624         dbgKrita << ppVar(oldDocumentSize) << ppVar(newDocumentSize);
625         dbgKrita << ppVar(baseAngle) << "deg";
626         dbgKrita << ppVar(angle) << "deg";
627         dbgKrita << "***************************";
628     }
629 
630     return preferredCenterAsExpected && oldOffsetAsExpected && newOffsetAsExpected && realCenterPointAsExpected;
631 }
632 
testRotation(qreal vastScrolling,qreal zoom)633 void KisZoomAndPanTest::testRotation(qreal vastScrolling, qreal zoom)
634 {
635     KisConfig cfg(false);
636     cfg.setVastScrolling(vastScrolling);
637 
638     ZoomAndPanTester t;
639 
640     QCOMPARE(t.image()->size(), QSize(640,441));
641     QCOMPARE(t.image()->xRes(), 1.0);
642     QCOMPARE(t.image()->yRes(), 1.0);
643 
644     QPointF preferredCenter = zoom * t.image()->bounds().center();
645 
646     t.canvasController()->resize(QSize(500,500));
647     t.zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, zoom);
648     t.canvasController()->setPreferredCenter(preferredCenter.toPoint());
649 
650     QCOMPARE(t.canvasWidget()->size(), QSize(483,483));
651     QCOMPARE(t.canvasWidget()->size(), t.canvasController()->viewportSize().toSize());
652 
653     QPointF realCenterPoint = t.coordinatesConverter()->widgetToImage(t.coordinatesConverter()->widgetCenterPoint());
654     QPointF expectedCenterPoint = QPointF(t.image()->bounds().center());
655 
656     if(!compareWithRounding(realCenterPoint, expectedCenterPoint, 2/zoom)) {
657         dbgKrita << "Failed to set initial center point";
658         dbgKrita << ppVar(expectedCenterPoint) << ppVar(realCenterPoint);
659         QFAIL("FAIL: Failed to set initial center point");
660     }
661 
662     QVERIFY(checkRotation(t, 30));
663     QVERIFY(checkRotation(t, 20));
664     QVERIFY(checkRotation(t, 10));
665     QVERIFY(checkRotation(t, 5));
666     QVERIFY(checkRotation(t, 5));
667     QVERIFY(checkRotation(t, 5));
668 
669     if(vastScrolling < 0.5 && zoom < 1) {
670         warnKrita << "Disabling a few tests for vast scrolling ="
671                    << vastScrolling << ". See comment for more";
672         /**
673          * We have to disable a couple of tests here for the case when
674          * vastScrolling value is 0.2. The problem is that the centering
675          * correction applied  to the offset in
676          * KisCanvasController::rotateCanvas pollutes the preferredCenter
677          * value, because KoCnvasControllerWidget has no access to this
678          * correction and cannot calculate the real value of the center of
679          * the image. To fix this bug the calculation of correction
680          * (aka "origin") should be moved to the KoCanvasControllerWidget
681          * itself which would cause quite huge changes (including the change
682          * of the external interface of it). Namely, we would have to
683          * *calculate* offset from the value of the scroll bars, but not
684          * use their values directly:
685          *
686          * offset = scrollBarValue - origin
687          *
688          * So now we just disable these unittests and allow a couple
689          * of "jumping" bugs appear in vastScrolling < 0.5 modes, which
690          * is, actually, not the default case.
691          */
692 
693     } else {
694         QVERIFY(checkRotation(t, 5));
695         QVERIFY(checkRotation(t, 5));
696         QVERIFY(checkRotation(t, 5));
697     }
698 }
699 
testRotation_VastScrolling_1_0()700 void KisZoomAndPanTest::testRotation_VastScrolling_1_0()
701 {
702     testRotation(0.9, 1.0);
703 }
704 
testRotation_VastScrolling_0_5()705 void KisZoomAndPanTest::testRotation_VastScrolling_0_5()
706 {
707     testRotation(0.9, 0.5);
708 }
709 
testRotation_NoVastScrolling_1_0()710 void KisZoomAndPanTest::testRotation_NoVastScrolling_1_0()
711 {
712     testRotation(0.2, 1.0);
713 }
714 
testRotation_NoVastScrolling_0_5()715 void KisZoomAndPanTest::testRotation_NoVastScrolling_0_5()
716 {
717     testRotation(0.2, 0.5);
718 }
719 
testImageRescaled_0_5()720 void KisZoomAndPanTest::testImageRescaled_0_5()
721 {
722     ZoomAndPanTester t;
723     QApplication::processEvents();
724     initializeViewport(t, false, false, false);
725     QApplication::processEvents();
726     QVERIFY(checkPan(t, QPoint(200,200)));
727     QApplication::processEvents();
728 
729     QPointF oldStillPoint =
730         t.coordinatesConverter()->imageRectInWidgetPixels().center();
731 
732     KisFilterStrategy *strategy = new KisBilinearFilterStrategy();
733     t.image()->scaleImage(QSize(320, 220), t.image()->xRes(), t.image()->yRes(), strategy);
734     t.image()->waitForDone();
735     QApplication::processEvents();
736     delete strategy;
737 
738     QPointF newStillPoint =
739         t.coordinatesConverter()->imageRectInWidgetPixels().center();
740 
741     QVERIFY(compareWithRounding(oldStillPoint, newStillPoint, 1.0));
742 }
743 
testImageCropped()744 void KisZoomAndPanTest::testImageCropped()
745 {
746     ZoomAndPanTester t;
747     QApplication::processEvents();
748     initializeViewport(t, false, false, false);
749     QApplication::processEvents();
750     QVERIFY(checkPan(t, QPoint(-150,-150)));
751     QApplication::processEvents();
752 
753     QPointF oldStillPoint =
754         t.coordinatesConverter()->imageToWidget(QPointF(150,150));
755 
756     t.image()->cropImage(QRect(100,100,100,100));
757     t.image()->waitForDone();
758     QApplication::processEvents();
759 
760     QPointF newStillPoint =
761         t.coordinatesConverter()->imageToWidget(QPointF(50,50));
762 
763     QVERIFY(compareWithRounding(oldStillPoint, newStillPoint, 1.0));
764 }
765 
766 KISTEST_MAIN(KisZoomAndPanTest)
767