1 /*
2     Scan Tailor - Interactive post-processing tool for scanned pages.
3     Copyright (C)  Joseph Artsimovich <joseph.artsimovich@gmail.com>
4 
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "ImageViewBase.h"
20 #include <QApplication>
21 #include <QGLWidget>
22 #include <QMouseEvent>
23 #include <QPaintEngine>
24 #include <QPointer>
25 #include <QPainterPath>
26 #include <QScrollBar>
27 #include <QSettings>
28 #include <QtWidgets/QMainWindow>
29 #include <QtWidgets/QStatusBar>
30 #include "BackgroundExecutor.h"
31 #include "ColorSchemeManager.h"
32 #include "Dpm.h"
33 #include "ImagePresentation.h"
34 #include "OpenGLSupport.h"
35 #include "PixmapRenderer.h"
36 #include "ScopedIncDec.h"
37 #include "UnitsProvider.h"
38 #include "Utils.h"
39 #include "imageproc/PolygonUtils.h"
40 #include "imageproc/Transform.h"
41 
42 using namespace imageproc;
43 
44 class ImageViewBase::HqTransformTask : public AbstractCommand<intrusive_ptr<AbstractCommand<void>>>, public QObject {
45   DECLARE_NON_COPYABLE(HqTransformTask)
46 
47  public:
48   HqTransformTask(ImageViewBase* image_view, const QImage& image, const QTransform& xform, const QSize& target_size);
49 
cancel()50   void cancel() { m_result->cancel(); }
51 
isCancelled() const52   const bool isCancelled() const { return m_result->isCancelled(); }
53 
54   intrusive_ptr<AbstractCommand<void>> operator()() override;
55 
56  private:
57   class Result : public AbstractCommand<void> {
58    public:
59     explicit Result(ImageViewBase* image_view);
60 
61     void setData(const QPoint& origin, const QImage& hq_image);
62 
cancel()63     void cancel() { m_cancelFlag.fetchAndStoreRelaxed(1); }
64 
isCancelled() const65     bool isCancelled() const { return m_cancelFlag.fetchAndAddRelaxed(0) != 0; }
66 
67     void operator()() override;
68 
69    private:
70     QPointer<ImageViewBase> m_imageView;
71     QPoint m_origin;
72     QImage m_hqImage;
73     mutable QAtomicInt m_cancelFlag;
74   };
75 
76 
77   intrusive_ptr<Result> m_result;
78   QImage m_image;
79   QTransform m_xform;
80   QSize m_targetSize;
81 };
82 
83 
84 /**
85  * \brief Temporarily adjust the widget focal point, then change it back.
86  *
87  * When adjusting and restoring the widget focal point, the pixmap
88  * focal point is recalculated accordingly.
89  */
90 class ImageViewBase::TempFocalPointAdjuster {
91  public:
92   /**
93    * Change the widget focal point to obj.centeredWidgetFocalPoint().
94    */
95   explicit TempFocalPointAdjuster(ImageViewBase& obj);
96 
97   /**
98    * Change the widget focal point to \p temp_widget_fp
99    */
100   TempFocalPointAdjuster(ImageViewBase& obj, QPointF temp_widget_fp);
101 
102   /**
103    * Restore the widget focal point.
104    */
105   ~TempFocalPointAdjuster();
106 
107  private:
108   ImageViewBase& m_obj;
109   QPointF m_origWidgetFP;
110 };
111 
112 
113 class ImageViewBase::TransformChangeWatcher {
114  public:
115   explicit TransformChangeWatcher(ImageViewBase& owner);
116 
117   ~TransformChangeWatcher();
118 
119  private:
120   ImageViewBase& m_owner;
121   QTransform m_imageToVirtual;
122   QTransform m_virtualToWidget;
123   QRectF m_virtualDisplayArea;
124 };
125 
126 
ImageViewBase(const QImage & image,const ImagePixmapUnion & downscaled_version,const ImagePresentation & presentation,const Margins & margins)127 ImageViewBase::ImageViewBase(const QImage& image,
128                              const ImagePixmapUnion& downscaled_version,
129                              const ImagePresentation& presentation,
130                              const Margins& margins)
131     : m_image(image),
132       m_virtualImageCropArea(presentation.cropArea()),
133       m_virtualDisplayArea(presentation.displayArea()),
134       m_imageToVirtual(presentation.transform()),
135       m_virtualToImage(presentation.transform().inverted()),
136       m_lastMaximumViewportSize(maximumViewportSize()),
137       m_margins(margins),
138       m_zoom(1.0),
139       m_transformChangeWatchersActive(0),
140       m_ignoreScrollEvents(0),
141       m_ignoreResizeEvents(0),
142       m_hqTransformEnabled(true),
143       m_infoProvider(Dpm(m_image)) {
144   /* For some reason, the default viewport fills background with
145    * a color different from QPalette::Window at the first show on Windows.
146    * Here we make it not fill it automatically at all
147    * doing the work in paintEvent().
148    */
149   setAttribute(Qt::WA_OpaquePaintEvent);
150 
151   /* For some reason, the default viewport fills background with
152    * a color different from QPalette::Window. Here we make it not
153    * fill it at all, assuming QMainWindow will do that anyway
154    * (with the correct color). Note that an attempt to do the same
155    * to an OpenGL viewport produces "black hole" artefacts. Therefore,
156    * we do this before setting an OpenGL viewport rather than after.
157    */
158   viewport()->setAutoFillBackground(false);
159 
160   if (QSettings().value("settings/use_3d_acceleration", false) != false) {
161     if (OpenGLSupport::supported()) {
162       QGLFormat format;
163       format.setSampleBuffers(true);
164       format.setStencil(true);
165       format.setAlpha(true);
166       format.setRgba(true);
167       format.setDepth(false);
168 
169       // Most of hardware refuses to work for us with direct rendering enabled.
170       format.setDirectRendering(false);
171 
172       setViewport(new QGLWidget(format));
173     }
174   }
175 
176   setFrameShape(QFrame::NoFrame);
177   viewport()->setFocusPolicy(Qt::WheelFocus);
178 
179   if (downscaled_version.isNull()) {
180     m_pixmap = QPixmap::fromImage(createDownscaledImage(image));
181   } else if (downscaled_version.pixmap().isNull()) {
182     m_pixmap = QPixmap::fromImage(downscaled_version.image());
183   } else {
184     m_pixmap = downscaled_version.pixmap();
185   }
186 
187   m_pixmapToImage.scale((double) m_image.width() / m_pixmap.width(), (double) m_image.height() / m_pixmap.height());
188 
189   m_widgetFocalPoint = centeredWidgetFocalPoint();
190   m_pixmapFocalPoint = m_virtualToImage.map(virtualDisplayRect().center());
191 
192   m_timer.setSingleShot(true);
193   m_timer.setInterval(150);  // msec
194   connect(&m_timer, SIGNAL(timeout()), this, SLOT(initiateBuildingHqVersion()));
195 
196   setMouseTracking(true);
197   m_cursorTrackerTimer.setSingleShot(true);
198   m_cursorTrackerTimer.setInterval(150);  // msec
199   connect(&m_cursorTrackerTimer, &QTimer::timeout, [this]() {
200     QPointF cursorPos;
201     if (!m_cursorPos.isNull()) {
202       cursorPos = m_widgetToVirtual.map(m_cursorPos) - m_virtualImageCropArea.boundingRect().topLeft();
203     }
204     m_infoProvider.setMousePos(cursorPos);
205   });
206 
207   updatePhysSize();
208 
209   updateWidgetTransformAndFixFocalPoint(CENTER_IF_FITS);
210 
211   interactionState().setDefaultStatusTip(tr("Use the mouse wheel or +/- to zoom.  When zoomed, dragging is possible."));
212   ensureStatusTip(interactionState().statusTip());
213 
214   connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(updateScrollBars()));
215   connect(verticalScrollBar(), SIGNAL(sliderReleased()), SLOT(updateScrollBars()));
216   connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(reactToScrollBars()));
217   connect(verticalScrollBar(), SIGNAL(valueChanged(int)), SLOT(reactToScrollBars()));
218 }
219 
220 ImageViewBase::~ImageViewBase() = default;
221 
hqTransformSetEnabled(const bool enabled)222 void ImageViewBase::hqTransformSetEnabled(const bool enabled) {
223   if (!enabled && m_hqTransformEnabled) {
224     // Turning off.
225     m_hqTransformEnabled = false;
226     if (m_hqTransformTask) {
227       m_hqTransformTask->cancel();
228       m_hqTransformTask.reset();
229     }
230     if (!m_hqPixmap.isNull()) {
231       m_hqPixmap = QPixmap();
232       update();
233     }
234   } else if (enabled && !m_hqTransformEnabled) {
235     // Turning on.
236     m_hqTransformEnabled = true;
237     update();
238   }
239 }
240 
createDownscaledImage(const QImage & image)241 QImage ImageViewBase::createDownscaledImage(const QImage& image) {
242   assert(!image.isNull());
243 
244   // Original and downscaled DPM.
245   const Dpm o_dpm(image);
246   const Dpm d_dpm(Dpi(200, 200));
247 
248   const int o_w = image.width();
249   const int o_h = image.height();
250 
251   int d_w = o_w * d_dpm.horizontal() / o_dpm.horizontal();
252   int d_h = o_h * d_dpm.vertical() / o_dpm.vertical();
253   d_w = qBound(1, d_w, o_w);
254   d_h = qBound(1, d_h, o_h);
255 
256   if ((d_w * 1.2 > o_w) || (d_h * 1.2 > o_h)) {
257     // Sizes are close - no point in downscaling.
258     return image;
259   }
260 
261   QTransform xform;
262   xform.scale((double) d_w / o_w, (double) d_h / o_h);
263 
264   return transform(image, xform, QRect(0, 0, d_w, d_h), OutsidePixels::assumeColor(Qt::white));
265 }
266 
maxViewportRect() const267 QRectF ImageViewBase::maxViewportRect() const {
268   const QRectF viewport_rect(QPointF(0, 0), maximumViewportSize());
269   QRectF r(viewport_rect);
270   r.adjust(m_margins.left(), m_margins.top(), -m_margins.right(), -m_margins.bottom());
271   if (r.isEmpty()) {
272     return QRectF(viewport_rect.center(), viewport_rect.center());
273   }
274 
275   return r;
276 }
277 
dynamicViewportRect() const278 QRectF ImageViewBase::dynamicViewportRect() const {
279   const QRectF viewport_rect(viewport()->rect());
280   QRectF r(viewport_rect);
281   r.adjust(m_margins.left(), m_margins.top(), -m_margins.right(), -m_margins.bottom());
282   if (r.isEmpty()) {
283     return QRectF(viewport_rect.center(), viewport_rect.center());
284   }
285 
286   return r;
287 }
288 
getOccupiedWidgetRect() const289 QRectF ImageViewBase::getOccupiedWidgetRect() const {
290   const QRectF widget_rect(m_virtualToWidget.mapRect(virtualDisplayRect()));
291 
292   return widget_rect.intersected(dynamicViewportRect());
293 }
294 
setWidgetFocalPoint(const QPointF & widget_fp)295 void ImageViewBase::setWidgetFocalPoint(const QPointF& widget_fp) {
296   setNewWidgetFP(widget_fp, /*update =*/true);
297 }
298 
adjustAndSetWidgetFocalPoint(const QPointF & widget_fp)299 void ImageViewBase::adjustAndSetWidgetFocalPoint(const QPointF& widget_fp) {
300   adjustAndSetNewWidgetFP(widget_fp, /*update=*/true);
301 }
302 
setZoomLevel(double zoom)303 void ImageViewBase::setZoomLevel(double zoom) {
304   if (m_zoom != zoom) {
305     m_zoom = zoom;
306     updateWidgetTransform();
307     update();
308   }
309 }
310 
moveTowardsIdealPosition(const double pixel_length)311 void ImageViewBase::moveTowardsIdealPosition(const double pixel_length) {
312   if (pixel_length <= 0) {
313     // The name implies we are moving *towards* the ideal position.
314     return;
315   }
316 
317   const QPointF ideal_widget_fp(getIdealWidgetFocalPoint(CENTER_IF_FITS));
318   if (ideal_widget_fp == m_widgetFocalPoint) {
319     return;
320   }
321 
322   QPointF vec(ideal_widget_fp - m_widgetFocalPoint);
323   const double max_length = std::sqrt(vec.x() * vec.x() + vec.y() * vec.y());
324   if (pixel_length >= max_length) {
325     m_widgetFocalPoint = ideal_widget_fp;
326   } else {
327     vec *= pixel_length / max_length;
328     m_widgetFocalPoint += vec;
329   }
330 
331   updateWidgetTransform();
332   update();
333 }
334 
updateTransform(const ImagePresentation & presentation)335 void ImageViewBase::updateTransform(const ImagePresentation& presentation) {
336   const TransformChangeWatcher watcher(*this);
337   const TempFocalPointAdjuster temp_fp(*this);
338 
339   m_imageToVirtual = presentation.transform();
340   m_virtualToImage = m_imageToVirtual.inverted();
341   m_virtualImageCropArea = presentation.cropArea();
342   m_virtualDisplayArea = presentation.displayArea();
343 
344   updateWidgetTransform();
345   update();
346   updatePhysSize();
347 }
348 
updateTransformAndFixFocalPoint(const ImagePresentation & presentation,const FocalPointMode mode)349 void ImageViewBase::updateTransformAndFixFocalPoint(const ImagePresentation& presentation, const FocalPointMode mode) {
350   const TransformChangeWatcher watcher(*this);
351   const TempFocalPointAdjuster temp_fp(*this);
352 
353   m_imageToVirtual = presentation.transform();
354   m_virtualToImage = m_imageToVirtual.inverted();
355   m_virtualImageCropArea = presentation.cropArea();
356   m_virtualDisplayArea = presentation.displayArea();
357 
358   updateWidgetTransformAndFixFocalPoint(mode);
359   update();
360   updatePhysSize();
361 }
362 
updateTransformPreservingScale(const ImagePresentation & presentation)363 void ImageViewBase::updateTransformPreservingScale(const ImagePresentation& presentation) {
364   const TransformChangeWatcher watcher(*this);
365   const TempFocalPointAdjuster temp_fp(*this);
366 
367   // An arbitrary line in image coordinates.
368   const QLineF image_line(0.0, 0.0, 1.0, 1.0);
369 
370   const QLineF widget_line_before((m_imageToVirtual * m_virtualToWidget).map(image_line));
371 
372   m_imageToVirtual = presentation.transform();
373   m_virtualToImage = m_imageToVirtual.inverted();
374   m_virtualImageCropArea = presentation.cropArea();
375   m_virtualDisplayArea = presentation.displayArea();
376 
377   updateWidgetTransform();
378 
379   const QLineF widget_line_after((m_imageToVirtual * m_virtualToWidget).map(image_line));
380 
381   m_zoom *= widget_line_before.length() / widget_line_after.length();
382   updateWidgetTransform();
383 
384   update();
385   updatePhysSize();
386 }
387 
ensureStatusTip(const QString & status_tip)388 void ImageViewBase::ensureStatusTip(const QString& status_tip) {
389   const QString cur_status_tip(statusTip());
390   if (cur_status_tip.constData() == status_tip.constData()) {
391     return;
392   }
393   if (cur_status_tip == status_tip) {
394     return;
395   }
396 
397   viewport()->setStatusTip(status_tip);
398 
399   if (viewport()->underMouse()) {
400     // Note that setStatusTip() alone is not enough,
401     // as it's only taken into account when the mouse
402     // enters the widget.
403     // Also note that we use postEvent() rather than sendEvent(),
404     // because sendEvent() may immediately process other events.
405     QApplication::postEvent(viewport(), new QStatusTipEvent(status_tip));
406   }
407 }
408 
paintEvent(QPaintEvent * event)409 void ImageViewBase::paintEvent(QPaintEvent* event) {
410   QPainter painter(viewport());
411 
412   // Fill the background as Qt::WA_OpaquePaintEvent attribute is enabled.
413   painter.fillRect(viewport()->rect(), palette().color(backgroundRole()));
414 
415   painter.save();
416 
417   const double xscale = m_virtualToWidget.m11();
418 
419   // Width of a source pixel in mm, as it's displayed on screen.
420   const double pixel_width = widthMM() * xscale / width();
421 
422   // Make clipping smooth.
423   painter.setRenderHint(QPainter::Antialiasing, true);
424 
425   // Disable antialiasing for large zoom levels.
426   painter.setRenderHint(QPainter::SmoothPixmapTransform, pixel_width < 0.5);
427 
428   if (validateHqPixmap()) {
429     // HQ pixmap maps one to one to screen pixels, so antialiasing is not necessary.
430     painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
431 
432     QPainterPath clip_path;
433     clip_path.addPolygon(m_virtualToWidget.map(m_virtualImageCropArea));
434     painter.setClipPath(clip_path);
435 
436     painter.drawPixmap(m_hqPixmapPos, m_hqPixmap);
437   } else {
438     scheduleHqVersionRebuild();
439 
440     const QTransform pixmap_to_virtual(m_pixmapToImage * m_imageToVirtual);
441     painter.setWorldTransform(pixmap_to_virtual * m_virtualToWidget);
442 
443     QPainterPath clip_path;
444     clip_path.addPolygon(pixmap_to_virtual.inverted().map(m_virtualImageCropArea));
445     painter.setClipPath(clip_path);
446 
447     PixmapRenderer::drawPixmap(painter, m_pixmap);
448   }
449 
450   painter.restore();
451 
452   painter.setWorldTransform(m_virtualToWidget);
453 
454   m_interactionState.resetProximity();
455   if (!m_interactionState.captured()) {
456     m_rootInteractionHandler.proximityUpdate(QPointF(0.5, 0.5) + mapFromGlobal(QCursor::pos()), m_interactionState);
457     updateStatusTipAndCursor();
458   }
459 
460   m_rootInteractionHandler.paint(painter, m_interactionState);
461   maybeQueueRedraw();
462 }  // ImageViewBase::paintEvent
463 
keyPressEvent(QKeyEvent * event)464 void ImageViewBase::keyPressEvent(QKeyEvent* event) {
465   event->setAccepted(false);
466   m_rootInteractionHandler.keyPressEvent(event, m_interactionState);
467   updateStatusTipAndCursor();
468   maybeQueueRedraw();
469 }
470 
keyReleaseEvent(QKeyEvent * event)471 void ImageViewBase::keyReleaseEvent(QKeyEvent* event) {
472   event->setAccepted(false);
473   m_rootInteractionHandler.keyReleaseEvent(event, m_interactionState);
474   updateStatusTipAndCursor();
475   maybeQueueRedraw();
476 }
477 
mousePressEvent(QMouseEvent * event)478 void ImageViewBase::mousePressEvent(QMouseEvent* event) {
479   m_interactionState.resetProximity();
480   if (!m_interactionState.captured()) {
481     m_rootInteractionHandler.proximityUpdate(QPointF(0.5, 0.5) + event->pos(), m_interactionState);
482   }
483 
484   event->setAccepted(false);
485   m_rootInteractionHandler.mousePressEvent(event, m_interactionState);
486   event->setAccepted(true);
487   updateStatusTipAndCursor();
488   maybeQueueRedraw();
489 }
490 
mouseReleaseEvent(QMouseEvent * event)491 void ImageViewBase::mouseReleaseEvent(QMouseEvent* event) {
492   m_interactionState.resetProximity();
493   if (!m_interactionState.captured()) {
494     m_rootInteractionHandler.proximityUpdate(QPointF(0.5, 0.5) + event->pos(), m_interactionState);
495   }
496 
497   event->setAccepted(false);
498   m_rootInteractionHandler.mouseReleaseEvent(event, m_interactionState);
499   event->setAccepted(true);
500   updateStatusTipAndCursor();
501   maybeQueueRedraw();
502 }
503 
mouseDoubleClickEvent(QMouseEvent * event)504 void ImageViewBase::mouseDoubleClickEvent(QMouseEvent* event) {
505   m_interactionState.resetProximity();
506   if (!m_interactionState.captured()) {
507     m_rootInteractionHandler.proximityUpdate(QPointF(0.5, 0.5) + event->pos(), m_interactionState);
508   }
509 
510   event->setAccepted(false);
511   m_rootInteractionHandler.mouseDoubleClickEvent(event, m_interactionState);
512   event->setAccepted(true);
513   updateStatusTipAndCursor();
514   maybeQueueRedraw();
515 }
516 
mouseMoveEvent(QMouseEvent * event)517 void ImageViewBase::mouseMoveEvent(QMouseEvent* event) {
518   m_interactionState.resetProximity();
519   if (!m_interactionState.captured()) {
520     m_rootInteractionHandler.proximityUpdate(QPointF(0.5, 0.5) + event->pos(), m_interactionState);
521   }
522 
523   event->setAccepted(false);
524   m_rootInteractionHandler.mouseMoveEvent(event, m_interactionState);
525   event->setAccepted(true);
526   updateStatusTipAndCursor();
527   maybeQueueRedraw();
528   updateCursorPos(event->localPos());
529 }
530 
wheelEvent(QWheelEvent * event)531 void ImageViewBase::wheelEvent(QWheelEvent* event) {
532   event->setAccepted(false);
533   m_rootInteractionHandler.wheelEvent(event, m_interactionState);
534   event->setAccepted(true);
535   updateStatusTipAndCursor();
536   maybeQueueRedraw();
537 }
538 
contextMenuEvent(QContextMenuEvent * event)539 void ImageViewBase::contextMenuEvent(QContextMenuEvent* event) {
540   event->setAccepted(false);
541   m_rootInteractionHandler.contextMenuEvent(event, m_interactionState);
542   event->setAccepted(true);
543   updateStatusTipAndCursor();
544   maybeQueueRedraw();
545 }
546 
resizeEvent(QResizeEvent * event)547 void ImageViewBase::resizeEvent(QResizeEvent* event) {
548   QAbstractScrollArea::resizeEvent(event);
549 
550   if (m_ignoreResizeEvents) {
551     return;
552   }
553 
554   const ScopedIncDec<int> guard(m_ignoreScrollEvents);
555 
556   if (maximumViewportSize() != m_lastMaximumViewportSize) {
557     m_lastMaximumViewportSize = maximumViewportSize();
558     m_widgetFocalPoint = centeredWidgetFocalPoint();
559     updateWidgetTransform();
560   } else {
561     const TransformChangeWatcher watcher(*this);
562     const TempFocalPointAdjuster temp_fp(*this, QPointF(0, 0));
563     updateTransformPreservingScale(ImagePresentation(m_imageToVirtual, m_virtualImageCropArea, m_virtualDisplayArea));
564   }
565 }
566 
enterEvent(QEvent * event)567 void ImageViewBase::enterEvent(QEvent* event) {
568   viewport()->setFocus();
569   QAbstractScrollArea::enterEvent(event);
570 }
571 
leaveEvent(QEvent * event)572 void ImageViewBase::leaveEvent(QEvent* event) {
573   updateCursorPos(QPointF());
574   QAbstractScrollArea::leaveEvent(event);
575 }
576 
showEvent(QShowEvent * event)577 void ImageViewBase::showEvent(QShowEvent* event) {
578   QWidget::showEvent(event);
579 
580   if (auto* mainWindow = dynamic_cast<QMainWindow*>(window())) {
581     if (auto* infoObserver = Utils::castOrFindChild<ImageViewInfoObserver*>(mainWindow->statusBar())) {
582       infoObserver->setInfoProvider(&infoProvider());
583     }
584   }
585 }
586 
587 /**
588  * Called when any of the transformations change.
589  */
transformChanged()590 void ImageViewBase::transformChanged() {
591   updateScrollBars();
592 }
593 
updateScrollBars()594 void ImageViewBase::updateScrollBars() {
595   if (verticalScrollBar()->isSliderDown() || horizontalScrollBar()->isSliderDown()) {
596     return;
597   }
598 
599   const ScopedIncDec<int> guard1(m_ignoreScrollEvents);
600   const ScopedIncDec<int> guard2(m_ignoreResizeEvents);
601 
602   const QRectF picture(m_virtualToWidget.mapRect(virtualDisplayRect()));
603   const QPointF viewport_center(maxViewportRect().center());
604   const QPointF picture_center(picture.center());
605   QRectF viewport(maxViewportRect());
606 
607   // Introduction of one scrollbar will decrease the available size in
608   // another direction, which may cause a scrollbar in that direction
609   // to become necessary.  For this reason, we have a loop here.
610   for (int i = 0; i < 2; ++i) {
611     const double xval = picture_center.x();
612     double xmin, xmax;  // Minimum and maximum positions for picture center.
613     if (picture_center.x() < viewport_center.x()) {
614       xmin = std::min<double>(xval, viewport.right() - 0.5 * picture.width());
615       xmax = std::max<double>(viewport_center.x(), viewport.left() + 0.5 * picture.width());
616     } else {
617       xmax = std::max<double>(xval, viewport.left() + 0.5 * picture.width());
618       xmin = std::min<double>(viewport_center.x(), viewport.right() - 0.5 * picture.width());
619     }
620 
621     const double yval = picture_center.y();
622     double ymin, ymax;  // Minimum and maximum positions for picture center.
623     if (picture_center.y() < viewport_center.y()) {
624       ymin = std::min<double>(yval, viewport.bottom() - 0.5 * picture.height());
625       ymax = std::max<double>(viewport_center.y(), viewport.top() + 0.5 * picture.height());
626     } else {
627       ymax = std::max<double>(yval, viewport.top() + 0.5 * picture.height());
628       ymin = std::min<double>(viewport_center.y(), viewport.bottom() - 0.5 * picture.height());
629     }
630 
631     const auto xrange = (int) std::ceil(xmax - xmin);
632     const auto yrange = (int) std::ceil(ymax - ymin);
633     const int xfirst = 0;
634     const int xlast = xrange - 1;
635     const int yfirst = 0;
636     const int ylast = yrange - 1;
637 
638     // We are going to map scrollbar coordinates to widget coordinates
639     // of the central point of the display area using a linear function.
640     // f(x) = ax + b
641 
642     // xmin = xa * xlast + xb
643     // xmax = xa * xfirst + xb
644     const double xa = (xfirst == xlast) ? 1 : (xmax - xmin) / (xfirst - xlast);
645     const double xb = xmax - xa * xfirst;
646     const double ya = (yfirst == ylast) ? 1 : (ymax - ymin) / (yfirst - ylast);
647     const double yb = ymax - ya * yfirst;
648 
649     // Inverse transformation.
650     // xlast = ixa * xmin + ixb
651     // xfirst = ixa * xmax + ixb
652     const double ixa = (xmax == xmin) ? 1 : (xfirst - xlast) / (xmax - xmin);
653     const double ixb = xfirst - ixa * xmax;
654     const double iya = (ymax == ymin) ? 1 : (yfirst - ylast) / (ymax - ymin);
655     const double iyb = yfirst - iya * ymax;
656 
657     m_scrollTransform.setMatrix(xa, 0, 0, 0, ya, 0, xb, yb, 1);
658 
659     const int xcur = qRound(ixa * xval + ixb);
660     const int ycur = qRound(iya * yval + iyb);
661 
662     horizontalScrollBar()->setRange(xfirst, xlast);
663     verticalScrollBar()->setRange(yfirst, ylast);
664 
665     horizontalScrollBar()->setValue(xcur);
666     verticalScrollBar()->setValue(ycur);
667 
668     horizontalScrollBar()->setPageStep(qRound(viewport.width()));
669     verticalScrollBar()->setPageStep(qRound(viewport.height()));
670     // XXX: a hack to force immediate update of viewport()->rect(),
671     // which is used by dynamicViewportRect() below.
672     // Note that it involves a resize event being sent not only to
673     // the viewport, but for some reason also to the containing
674     // QAbstractScrollArea, that is to this object.
675     setHorizontalScrollBarPolicy(horizontalScrollBarPolicy());
676 
677     const QRectF old_viewport(viewport);
678     viewport = dynamicViewportRect();
679     if (viewport == old_viewport) {
680       break;
681     }
682   }
683 }  // ImageViewBase::updateScrollBars
684 
reactToScrollBars()685 void ImageViewBase::reactToScrollBars() {
686   if (m_ignoreScrollEvents) {
687     return;
688   }
689 
690   const TransformChangeWatcher watcher(*this);
691 
692   const QPointF raw_position(horizontalScrollBar()->value(), verticalScrollBar()->value());
693   const QPointF new_fp(m_scrollTransform.map(raw_position));
694   const QPointF old_fp(getWidgetFocalPoint());
695 
696   m_pixmapFocalPoint = m_virtualToImage.map(m_virtualDisplayArea.center());
697   m_widgetFocalPoint = new_fp;
698   updateWidgetTransform();
699 
700   setWidgetFocalPointWithoutMoving(old_fp);
701 }
702 
703 /**
704  * Updates m_virtualToWidget and m_widgetToVirtual.\n
705  * To be called whenever any of the following is modified:
706  * m_imageToVirt, m_widgetFocalPoint, m_pixmapFocalPoint, m_zoom.
707  * Modifying both m_widgetFocalPoint and m_pixmapFocalPoint in a way
708  * that doesn't cause image movement doesn't require calling this method.
709  */
updateWidgetTransform()710 void ImageViewBase::updateWidgetTransform() {
711   const TransformChangeWatcher watcher(*this);
712 
713   const QRectF virt_rect(virtualDisplayRect());
714   const QPointF virt_origin(m_imageToVirtual.map(m_pixmapFocalPoint));
715   const QPointF widget_origin(m_widgetFocalPoint);
716 
717   QSizeF zoom1_widget_size(virt_rect.size());
718   zoom1_widget_size.scale(maxViewportRect().size(), Qt::KeepAspectRatio);
719 
720   const double zoom1_x = zoom1_widget_size.width() / virt_rect.width();
721   const double zoom1_y = zoom1_widget_size.height() / virt_rect.height();
722 
723   QTransform xform;
724   xform.translate(-virt_origin.x(), -virt_origin.y());
725   xform *= QTransform().scale(zoom1_x * m_zoom, zoom1_y * m_zoom);
726   xform *= QTransform().translate(widget_origin.x(), widget_origin.y());
727 
728   m_virtualToWidget = xform;
729   m_widgetToVirtual = m_virtualToWidget.inverted();
730 }
731 
732 /**
733  * Updates m_virtualToWidget and m_widgetToVirtual and adjusts
734  * the focal point if necessary.\n
735  * To be called whenever m_imageToVirt is modified in such a way that
736  * may invalidate the focal point.
737  */
updateWidgetTransformAndFixFocalPoint(const FocalPointMode mode)738 void ImageViewBase::updateWidgetTransformAndFixFocalPoint(const FocalPointMode mode) {
739   const TransformChangeWatcher watcher(*this);
740 
741   // This must go before getIdealWidgetFocalPoint(), as it
742   // recalculates m_virtualToWidget, that is used by
743   // getIdealWidgetFocalPoint().
744   updateWidgetTransform();
745 
746   const QPointF ideal_widget_fp(getIdealWidgetFocalPoint(mode));
747   if (ideal_widget_fp != m_widgetFocalPoint) {
748     m_widgetFocalPoint = ideal_widget_fp;
749     updateWidgetTransform();
750   }
751 }
752 
753 /**
754  * Returns a proposed value for m_widgetFocalPoint to minimize the
755  * unused widget space.  Unused widget space indicates one or both
756  * of the following:
757  * \li The image is smaller than the display area.
758  * \li Parts of the image are outside of the display area.
759  *
760  * \param mode If set to CENTER_IF_FITS, then the returned focal point
761  *        will center the image if it completely fits into the widget.
762  *        This works in horizontal and vertical directions independently.\n
763  *        If \p mode is set to DONT_CENTER and the image completely fits
764  *        the widget, then the returned focal point will cause a minimal
765  *        move to force the whole image to be visible.
766  *
767  * In case there is no unused widget space, the returned focal point
768  * is equal to the current focal point (m_widgetFocalPoint).  This works
769  * in horizontal and vertical dimensions independently.
770  */
getIdealWidgetFocalPoint(const FocalPointMode mode) const771 QPointF ImageViewBase::getIdealWidgetFocalPoint(const FocalPointMode mode) const {
772   // Widget rect reduced by margins.
773   const QRectF display_area(maxViewportRect());
774 
775   // The virtual image rectangle in widget coordinates.
776   const QRectF image_area(m_virtualToWidget.mapRect(virtualDisplayRect()));
777   // Unused display space from each side.
778   const double left_margin = image_area.left() - display_area.left();
779   const double right_margin = display_area.right() - image_area.right();
780   const double top_margin = image_area.top() - display_area.top();
781   const double bottom_margin = display_area.bottom() - image_area.bottom();
782 
783   QPointF widget_focal_point(m_widgetFocalPoint);
784 
785   if ((mode == CENTER_IF_FITS) && (left_margin + right_margin >= 0.0)) {
786     // Image fits horizontally, so center it in that direction
787     // by equalizing its left and right margins.
788     const double new_margins = 0.5 * (left_margin + right_margin);
789     widget_focal_point.rx() += new_margins - left_margin;
790   } else if ((left_margin < 0.0) && (right_margin > 0.0)) {
791     // Move image to the right so that either left_margin or
792     // right_margin becomes zero, whichever requires less movement.
793     const double movement = std::min(std::fabs(left_margin), std::fabs(right_margin));
794     widget_focal_point.rx() += movement;
795   } else if ((right_margin < 0.0) && (left_margin > 0.0)) {
796     // Move image to the left so that either left_margin or
797     // right_margin becomes zero, whichever requires less movement.
798     const double movement = std::min(std::fabs(left_margin), std::fabs(right_margin));
799     widget_focal_point.rx() -= movement;
800   }
801 
802   if ((mode == CENTER_IF_FITS) && (top_margin + bottom_margin >= 0.0)) {
803     // Image fits vertically, so center it in that direction
804     // by equalizing its top and bottom margins.
805     const double new_margins = 0.5 * (top_margin + bottom_margin);
806     widget_focal_point.ry() += new_margins - top_margin;
807   } else if ((top_margin < 0.0) && (bottom_margin > 0.0)) {
808     // Move image down so that either top_margin or bottom_margin
809     // becomes zero, whichever requires less movement.
810     const double movement = std::min(std::fabs(top_margin), std::fabs(bottom_margin));
811     widget_focal_point.ry() += movement;
812   } else if ((bottom_margin < 0.0) && (top_margin > 0.0)) {
813     // Move image up so that either top_margin or bottom_margin
814     // becomes zero, whichever requires less movement.
815     const double movement = std::min(std::fabs(top_margin), std::fabs(bottom_margin));
816     widget_focal_point.ry() -= movement;
817   }
818 
819   return widget_focal_point;
820 }  // ImageViewBase::getIdealWidgetFocalPoint
821 
setNewWidgetFP(const QPointF widget_fp,const bool update)822 void ImageViewBase::setNewWidgetFP(const QPointF widget_fp, const bool update) {
823   if (widget_fp != m_widgetFocalPoint) {
824     m_widgetFocalPoint = widget_fp;
825     updateWidgetTransform();
826     if (update) {
827       this->update();
828     }
829   }
830 }
831 
832 /**
833  * Used when dragging the image.  It adjusts the movement to disallow
834  * dragging it away from the ideal position (determined by
835  * getIdealWidgetFocalPoint()).  Movement towards the ideal position
836  * is permitted.  This works independently in horizontal and vertical
837  * direction.
838  *
839  * \param proposed_widget_fp The proposed value for m_widgetFocalPoint.
840  * \param update Whether to call this->update() in case the focal point
841  *        has changed.
842  */
adjustAndSetNewWidgetFP(const QPointF proposed_widget_fp,const bool update)843 void ImageViewBase::adjustAndSetNewWidgetFP(const QPointF proposed_widget_fp, const bool update) {
844   // We first apply the proposed focal point, and only then
845   // calculate the ideal one.  That's done because
846   // the ideal focal point is the current focal point when
847   // no widget space is wasted (image covers the whole widget).
848   // We don't want the ideal focal point to be equal to the current
849   // one, as that would disallow any movements.
850   const QPointF old_widget_fp(m_widgetFocalPoint);
851   setNewWidgetFP(proposed_widget_fp, update);
852 
853   const QPointF ideal_widget_fp(getIdealWidgetFocalPoint(CENTER_IF_FITS));
854 
855   const QPointF towards_ideal(ideal_widget_fp - old_widget_fp);
856   const QPointF towards_proposed(proposed_widget_fp - old_widget_fp);
857 
858   QPointF movement(towards_proposed);
859 
860   // Horizontal movement.
861   if (towards_ideal.x() * towards_proposed.x() < 0.0) {
862     // Wrong direction - no movement at all.
863     movement.setX(0.0);
864   } else if (std::fabs(towards_proposed.x()) > std::fabs(towards_ideal.x())) {
865     // Too much movement - limit it.
866     movement.setX(towards_ideal.x());
867   }
868   // Vertical movement.
869   if (towards_ideal.y() * towards_proposed.y() < 0.0) {
870     // Wrong direction - no movement at all.
871     movement.setY(0.0);
872   } else if (std::fabs(towards_proposed.y()) > std::fabs(towards_ideal.y())) {
873     // Too much movement - limit it.
874     movement.setY(towards_ideal.y());
875   }
876 
877   const QPointF adjusted_widget_fp(old_widget_fp + movement);
878   if (adjusted_widget_fp != m_widgetFocalPoint) {
879     m_widgetFocalPoint = adjusted_widget_fp;
880     updateWidgetTransform();
881     if (update) {
882       this->update();
883     }
884   }
885 }  // ImageViewBase::adjustAndSetNewWidgetFP
886 
887 /**
888  * Returns the center point of the available display area.
889  */
centeredWidgetFocalPoint() const890 QPointF ImageViewBase::centeredWidgetFocalPoint() const {
891   return maxViewportRect().center();
892 }
893 
setWidgetFocalPointWithoutMoving(const QPointF new_widget_fp)894 void ImageViewBase::setWidgetFocalPointWithoutMoving(const QPointF new_widget_fp) {
895   m_widgetFocalPoint = new_widget_fp;
896   m_pixmapFocalPoint = m_virtualToImage.map(m_widgetToVirtual.map(m_widgetFocalPoint));
897 }
898 
899 /**
900  * Returns true if m_hqPixmap is valid and up to date.
901  */
validateHqPixmap() const902 bool ImageViewBase::validateHqPixmap() const {
903   if (!m_hqTransformEnabled) {
904     return false;
905   }
906 
907   if (m_hqPixmap.isNull()) {
908     return false;
909   }
910 
911   if (m_hqSourceId != m_image.cacheKey()) {
912     return false;
913   }
914 
915   if (m_hqXform != m_imageToVirtual * m_virtualToWidget) {
916     return false;
917   }
918 
919   return true;
920 }
921 
scheduleHqVersionRebuild()922 void ImageViewBase::scheduleHqVersionRebuild() {
923   const QTransform xform(m_imageToVirtual * m_virtualToWidget);
924 
925   if (!m_timer.isActive() || (m_potentialHqXform != xform)) {
926     if (m_hqTransformTask) {
927       m_hqTransformTask->cancel();
928       m_hqTransformTask.reset();
929     }
930     m_potentialHqXform = xform;
931   }
932   m_timer.start();
933 }
934 
initiateBuildingHqVersion()935 void ImageViewBase::initiateBuildingHqVersion() {
936   if (validateHqPixmap()) {
937     return;
938   }
939 
940   m_hqPixmap = QPixmap();
941 
942   if (m_hqTransformTask) {
943     m_hqTransformTask->cancel();
944     m_hqTransformTask.reset();
945   }
946 
947   const QTransform xform(m_imageToVirtual * m_virtualToWidget);
948   const auto task = make_intrusive<HqTransformTask>(this, m_image, xform, viewport()->size());
949 
950   backgroundExecutor().enqueueTask(task);
951 
952   m_hqTransformTask = task;
953   m_hqXform = xform;
954   m_hqSourceId = m_image.cacheKey();
955 }
956 
957 /**
958  * Gets called from HqTransformationTask::Result.
959  */
hqVersionBuilt(const QPoint & origin,const QImage & image)960 void ImageViewBase::hqVersionBuilt(const QPoint& origin, const QImage& image) {
961   if (!m_hqTransformEnabled) {
962     return;
963   }
964 
965   m_hqPixmap = QPixmap::fromImage(image);
966   m_hqPixmapPos = origin;
967   m_hqTransformTask.reset();
968   update();
969 }
970 
updateStatusTipAndCursor()971 void ImageViewBase::updateStatusTipAndCursor() {
972   updateStatusTip();
973   updateCursor();
974 }
975 
updateStatusTip()976 void ImageViewBase::updateStatusTip() {
977   ensureStatusTip(m_interactionState.statusTip());
978 }
979 
updateCursor()980 void ImageViewBase::updateCursor() {
981   viewport()->setCursor(m_interactionState.cursor());
982 }
983 
maybeQueueRedraw()984 void ImageViewBase::maybeQueueRedraw() {
985   if (m_interactionState.redrawRequested()) {
986     m_interactionState.setRedrawRequested(false);
987     update();
988   }
989 }
990 
backgroundExecutor()991 BackgroundExecutor& ImageViewBase::backgroundExecutor() {
992   static BackgroundExecutor executor;
993 
994   return executor;
995 }
996 
updateCursorPos(const QPointF & pos)997 void ImageViewBase::updateCursorPos(const QPointF& pos) {
998   if (pos != m_cursorPos) {
999     m_cursorPos = pos;
1000     if (!m_cursorTrackerTimer.isActive()) {
1001       // report cursor pos once in 150 msec
1002       m_cursorTrackerTimer.start(150);
1003     }
1004   }
1005 }
1006 
updatePhysSize()1007 void ImageViewBase::updatePhysSize() {
1008   m_infoProvider.setPhysSize(m_virtualImageCropArea.boundingRect().size());
1009 }
1010 
infoProvider()1011 ImageViewInfoProvider& ImageViewBase::infoProvider() {
1012   return m_infoProvider;
1013 }
1014 
1015 /*==================== ImageViewBase::HqTransformTask ======================*/
1016 
HqTransformTask(ImageViewBase * image_view,const QImage & image,const QTransform & xform,const QSize & target_size)1017 ImageViewBase::HqTransformTask::HqTransformTask(ImageViewBase* image_view,
1018                                                 const QImage& image,
1019                                                 const QTransform& xform,
1020                                                 const QSize& target_size)
1021     : m_result(new Result(image_view)), m_image(image), m_xform(xform), m_targetSize(target_size) {}
1022 
operator ()()1023 intrusive_ptr<AbstractCommand<void>> ImageViewBase::HqTransformTask::operator()() {
1024   if (isCancelled()) {
1025     return nullptr;
1026   }
1027 
1028   const QRect target_rect(
1029       m_xform.map(QRectF(m_image.rect())).boundingRect().toRect().intersected(QRect(QPoint(0, 0), m_targetSize)));
1030 
1031   QImage hq_image(
1032       transform(m_image, m_xform, target_rect, OutsidePixels::assumeWeakColor(Qt::white), QSizeF(0.0, 0.0)));
1033 
1034   // In many cases m_image and therefore hq_image are grayscale with
1035   // a palette, but given that hq_image will be converted to a QPixmap
1036   // on the GUI thread, it's better to convert it to RGB as a preparation
1037   // step while we are still in a background thread.
1038   hq_image = hq_image.convertToFormat(hq_image.hasAlphaChannel() ? QImage::Format_ARGB32_Premultiplied
1039                                                                  : QImage::Format_RGB32);
1040 
1041   m_result->setData(target_rect.topLeft(), hq_image);
1042 
1043   return m_result;
1044 }
1045 
1046 /*================ ImageViewBase::HqTransformTask::Result ================*/
1047 
Result(ImageViewBase * image_view)1048 ImageViewBase::HqTransformTask::Result::Result(ImageViewBase* image_view) : m_imageView(image_view) {}
1049 
setData(const QPoint & origin,const QImage & hq_image)1050 void ImageViewBase::HqTransformTask::Result::setData(const QPoint& origin, const QImage& hq_image) {
1051   m_hqImage = hq_image;
1052   m_origin = origin;
1053 }
1054 
operator ()()1055 void ImageViewBase::HqTransformTask::Result::operator()() {
1056   if (m_imageView && !isCancelled()) {
1057     m_imageView->hqVersionBuilt(m_origin, m_hqImage);
1058   }
1059 }
1060 
1061 /*================= ImageViewBase::TempFocalPointAdjuster =================*/
1062 
TempFocalPointAdjuster(ImageViewBase & obj)1063 ImageViewBase::TempFocalPointAdjuster::TempFocalPointAdjuster(ImageViewBase& obj)
1064     : m_obj(obj), m_origWidgetFP(obj.getWidgetFocalPoint()) {
1065   obj.setWidgetFocalPointWithoutMoving(obj.centeredWidgetFocalPoint());
1066 }
1067 
TempFocalPointAdjuster(ImageViewBase & obj,const QPointF temp_widget_fp)1068 ImageViewBase::TempFocalPointAdjuster::TempFocalPointAdjuster(ImageViewBase& obj, const QPointF temp_widget_fp)
1069     : m_obj(obj), m_origWidgetFP(obj.getWidgetFocalPoint()) {
1070   obj.setWidgetFocalPointWithoutMoving(temp_widget_fp);
1071 }
1072 
~TempFocalPointAdjuster()1073 ImageViewBase::TempFocalPointAdjuster::~TempFocalPointAdjuster() {
1074   m_obj.setWidgetFocalPointWithoutMoving(m_origWidgetFP);
1075 }
1076 
1077 /*================== ImageViewBase::TransformChangeWatcher ================*/
1078 
TransformChangeWatcher(ImageViewBase & owner)1079 ImageViewBase::TransformChangeWatcher::TransformChangeWatcher(ImageViewBase& owner)
1080     : m_owner(owner),
1081       m_imageToVirtual(owner.m_imageToVirtual),
1082       m_virtualToWidget(owner.m_virtualToWidget),
1083       m_virtualDisplayArea(owner.m_virtualDisplayArea) {
1084   ++m_owner.m_transformChangeWatchersActive;
1085 }
1086 
~TransformChangeWatcher()1087 ImageViewBase::TransformChangeWatcher::~TransformChangeWatcher() {
1088   if (--m_owner.m_transformChangeWatchersActive == 0) {
1089     if ((m_imageToVirtual != m_owner.m_imageToVirtual) || (m_virtualToWidget != m_owner.m_virtualToWidget)
1090         || (m_virtualDisplayArea != m_owner.m_virtualDisplayArea)) {
1091       m_owner.transformChanged();
1092     }
1093   }
1094 }
1095