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