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 "ImageView.h"
20 #include <Despeckle.h>
21 #include <EmptyTaskStatus.h>
22 #include <ImageViewInfoProvider.h>
23 #include <UnitsConverter.h>
24 #include <imageproc/Binarize.h>
25 #include <imageproc/GrayImage.h>
26 #include <imageproc/PolygonRasterizer.h>
27 #include <imageproc/Transform.h>
28 #include <QMouseEvent>
29 #include <QPainter>
30 #include <QPainterPath>
31 #include <boost/bind.hpp>
32 #include <boost/lambda/lambda.hpp>
33 #include "ImagePresentation.h"
34 #include "OptionsWidget.h"
35 #include "Params.h"
36 #include "Settings.h"
37 #include "Utils.h"
38 #include "imageproc/PolygonUtils.h"
39 
40 using namespace imageproc;
41 
42 namespace page_layout {
ImageView(const intrusive_ptr<Settings> & settings,const PageId & page_id,const QImage & image,const QImage & downscaled_image,const imageproc::GrayImage & gray_image,const ImageTransformation & xform,const QRectF & adapted_content_rect,const OptionsWidget & opt_widget)43 ImageView::ImageView(const intrusive_ptr<Settings>& settings,
44                      const PageId& page_id,
45                      const QImage& image,
46                      const QImage& downscaled_image,
47                      const imageproc::GrayImage& gray_image,
48                      const ImageTransformation& xform,
49                      const QRectF& adapted_content_rect,
50                      const OptionsWidget& opt_widget)
51     : ImageViewBase(image,
52                     downscaled_image,
53                     ImagePresentation(xform.transform(), xform.resultingPreCropArea()),
54                     Margins(5, 5, 5, 5)),
55       m_xform(xform),
56       m_dragHandler(*this),
57       m_zoomHandler(*this),
58       m_settings(settings),
59       m_pageId(page_id),
60       m_pixelsToMmXform(UnitsConverter(xform.origDpi()).transform(PIXELS, MILLIMETRES)),
61       m_mmToPixelsXform(m_pixelsToMmXform.inverted()),
62       m_innerRect(adapted_content_rect),
63       m_aggregateHardSizeMM(settings->getAggregateHardSizeMM()),
64       m_committedAggregateHardSizeMM(m_aggregateHardSizeMM),
65       m_alignment(opt_widget.alignment()),
66       m_leftRightLinked(opt_widget.leftRightLinked()),
67       m_topBottomLinked(opt_widget.topBottomLinked()),
68       m_contextMenu(new QMenu(this)),
69       m_guidesFreeIndex(0),
70       m_guideUnderMouse(-1),
71       m_innerRectVerticalDragModifier(Qt::ControlModifier),
72       m_innerRectHorizontalDragModifier(Qt::ShiftModifier),
73       m_nullContentRect((m_innerRect.width() < 1) && (m_innerRect.height() < 1)) {
74   setMouseTracking(true);
75 
76   interactionState().setDefaultStatusTip(tr("Resize margins by dragging any of the solid lines."));
77 
78   // Setup interaction stuff.
79   static const int masks_by_edge[] = {TOP, RIGHT, BOTTOM, LEFT};
80   static const int masks_by_corner[] = {TOP | LEFT, TOP | RIGHT, BOTTOM | RIGHT, BOTTOM | LEFT};
81   for (int i = 0; i < 4; ++i) {
82     // Proximity priority - inner rect higher than middle, corners higher than edges.
83     m_innerCorners[i].setProximityPriorityCallback(boost::lambda::constant(4));
84     m_innerEdges[i].setProximityPriorityCallback(boost::lambda::constant(3));
85     m_middleCorners[i].setProximityPriorityCallback(boost::lambda::constant(2));
86     m_middleEdges[i].setProximityPriorityCallback(boost::lambda::constant(1));
87 
88     // Proximity.
89     m_innerCorners[i].setProximityCallback(
90         boost::bind(&ImageView::cornerProximity, this, masks_by_corner[i], &m_innerRect, _1));
91     m_middleCorners[i].setProximityCallback(
92         boost::bind(&ImageView::cornerProximity, this, masks_by_corner[i], &m_middleRect, _1));
93     m_innerEdges[i].setProximityCallback(
94         boost::bind(&ImageView::edgeProximity, this, masks_by_edge[i], &m_innerRect, _1));
95     m_middleEdges[i].setProximityCallback(
96         boost::bind(&ImageView::edgeProximity, this, masks_by_edge[i], &m_middleRect, _1));
97     // Drag initiation.
98     m_innerCorners[i].setDragInitiatedCallback(boost::bind(&ImageView::dragInitiated, this, _1));
99     m_middleCorners[i].setDragInitiatedCallback(boost::bind(&ImageView::dragInitiated, this, _1));
100     m_innerEdges[i].setDragInitiatedCallback(boost::bind(&ImageView::dragInitiated, this, _1));
101     m_middleEdges[i].setDragInitiatedCallback(boost::bind(&ImageView::dragInitiated, this, _1));
102 
103     // Drag continuation.
104     m_innerCorners[i].setDragContinuationCallback(
105         boost::bind(&ImageView::innerRectDragContinuation, this, masks_by_corner[i], _1));
106     m_middleCorners[i].setDragContinuationCallback(
107         boost::bind(&ImageView::middleRectDragContinuation, this, masks_by_corner[i], _1));
108     m_innerEdges[i].setDragContinuationCallback(
109         boost::bind(&ImageView::innerRectDragContinuation, this, masks_by_edge[i], _1));
110     m_middleEdges[i].setDragContinuationCallback(
111         boost::bind(&ImageView::middleRectDragContinuation, this, masks_by_edge[i], _1));
112     // Drag finishing.
113     m_innerCorners[i].setDragFinishedCallback(boost::bind(&ImageView::dragFinished, this));
114     m_middleCorners[i].setDragFinishedCallback(boost::bind(&ImageView::dragFinished, this));
115     m_innerEdges[i].setDragFinishedCallback(boost::bind(&ImageView::dragFinished, this));
116     m_middleEdges[i].setDragFinishedCallback(boost::bind(&ImageView::dragFinished, this));
117 
118     m_innerCornerHandlers[i].setObject(&m_innerCorners[i]);
119     m_middleCornerHandlers[i].setObject(&m_middleCorners[i]);
120     m_innerEdgeHandlers[i].setObject(&m_innerEdges[i]);
121     m_middleEdgeHandlers[i].setObject(&m_middleEdges[i]);
122 
123     Qt::CursorShape corner_cursor = (i & 1) ? Qt::SizeBDiagCursor : Qt::SizeFDiagCursor;
124     m_innerCornerHandlers[i].setProximityCursor(corner_cursor);
125     m_innerCornerHandlers[i].setInteractionCursor(corner_cursor);
126     m_middleCornerHandlers[i].setProximityCursor(corner_cursor);
127     m_middleCornerHandlers[i].setInteractionCursor(corner_cursor);
128 
129     Qt::CursorShape edge_cursor = (i & 1) ? Qt::SizeHorCursor : Qt::SizeVerCursor;
130     m_innerEdgeHandlers[i].setProximityCursor(edge_cursor);
131     m_innerEdgeHandlers[i].setInteractionCursor(edge_cursor);
132     m_middleEdgeHandlers[i].setProximityCursor(edge_cursor);
133     m_middleEdgeHandlers[i].setInteractionCursor(edge_cursor);
134 
135     if (isShowingMiddleRectEnabled()) {
136       makeLastFollower(m_middleCornerHandlers[i]);
137       makeLastFollower(m_middleEdgeHandlers[i]);
138     }
139     if (!m_nullContentRect) {
140       makeLastFollower(m_innerCornerHandlers[i]);
141       makeLastFollower(m_innerEdgeHandlers[i]);
142     }
143   }
144 
145   {
146     m_innerRectArea.setProximityCallback(boost::bind(&ImageView::rectProximity, this, boost::ref(m_innerRect), _1));
147     m_innerRectArea.setDragInitiatedCallback(boost::bind(&ImageView::dragInitiated, this, _1));
148     m_innerRectArea.setDragContinuationCallback(boost::bind(&ImageView::innerRectMoveRequest, this, _1, _2));
149     m_innerRectArea.setDragFinishedCallback(boost::bind(&ImageView::dragFinished, this));
150     m_innerRectAreaHandler.setObject(&m_innerRectArea);
151     m_innerRectAreaHandler.setProximityStatusTip(tr("Hold left mouse button to drag the page content."));
152     m_innerRectAreaHandler.setInteractionStatusTip(tr("Release left mouse button to finish dragging."));
153     m_innerRectAreaHandler.setKeyboardModifiers({m_innerRectVerticalDragModifier, m_innerRectHorizontalDragModifier,
154                                                  m_innerRectVerticalDragModifier | m_innerRectHorizontalDragModifier});
155     Qt::CursorShape cursor = Qt::DragMoveCursor;
156     m_innerRectAreaHandler.setProximityCursor(cursor);
157     m_innerRectAreaHandler.setInteractionCursor(cursor);
158     makeLastFollower(m_innerRectAreaHandler);
159   }
160 
161   rootInteractionHandler().makeLastFollower(*this);
162   rootInteractionHandler().makeLastFollower(m_dragHandler);
163   rootInteractionHandler().makeLastFollower(m_zoomHandler);
164 
165   setupContextMenuInteraction();
166   setupGuides();
167 
168   recalcBoxesAndFit(opt_widget.marginsMM());
169 
170   buildContentImage(gray_image, xform);
171 }
172 
173 ImageView::~ImageView() = default;
174 
marginsSetExternally(const Margins & margins_mm)175 void ImageView::marginsSetExternally(const Margins& margins_mm) {
176   const AggregateSizeChanged changed = commitHardMargins(margins_mm);
177 
178   recalcBoxesAndFit(margins_mm);
179 
180   invalidateThumbnails(changed);
181 }
182 
leftRightLinkToggled(const bool linked)183 void ImageView::leftRightLinkToggled(const bool linked) {
184   m_leftRightLinked = linked;
185   if (linked) {
186     Margins margins_mm(calcHardMarginsMM());
187     if (margins_mm.left() != margins_mm.right()) {
188       const double new_margin = std::min(margins_mm.left(), margins_mm.right());
189       margins_mm.setLeft(new_margin);
190       margins_mm.setRight(new_margin);
191 
192       const AggregateSizeChanged changed = commitHardMargins(margins_mm);
193 
194       recalcBoxesAndFit(margins_mm);
195       emit marginsSetLocally(margins_mm);
196 
197       invalidateThumbnails(changed);
198     }
199   }
200 }
201 
topBottomLinkToggled(const bool linked)202 void ImageView::topBottomLinkToggled(const bool linked) {
203   m_topBottomLinked = linked;
204   if (linked) {
205     Margins margins_mm(calcHardMarginsMM());
206     if (margins_mm.top() != margins_mm.bottom()) {
207       const double new_margin = std::min(margins_mm.top(), margins_mm.bottom());
208       margins_mm.setTop(new_margin);
209       margins_mm.setBottom(new_margin);
210 
211       const AggregateSizeChanged changed = commitHardMargins(margins_mm);
212 
213       recalcBoxesAndFit(margins_mm);
214       emit marginsSetLocally(margins_mm);
215 
216       invalidateThumbnails(changed);
217     }
218   }
219 }
220 
alignmentChanged(const Alignment & alignment)221 void ImageView::alignmentChanged(const Alignment& alignment) {
222   m_alignment = alignment;
223 
224   const Settings::AggregateSizeChanged size_changed = m_settings->setPageAlignment(m_pageId, alignment);
225 
226   recalcBoxesAndFit(calcHardMarginsMM());
227 
228   enableGuidesInteraction(!m_alignment.isNull());
229   forceInscribeGuides();
230 
231   enableMiddleRectInteraction(isShowingMiddleRectEnabled());
232 
233   if (size_changed == Settings::AGGREGATE_SIZE_CHANGED) {
234     emit invalidateAllThumbnails();
235   } else {
236     emit invalidateThumbnail(m_pageId);
237   }
238 }
239 
aggregateHardSizeChanged()240 void ImageView::aggregateHardSizeChanged() {
241   m_aggregateHardSizeMM = m_settings->getAggregateHardSizeMM();
242   m_committedAggregateHardSizeMM = m_aggregateHardSizeMM;
243   recalcOuterRect();
244   updatePresentationTransform(FIT);
245 }
246 
onPaint(QPainter & painter,const InteractionState & interaction)247 void ImageView::onPaint(QPainter& painter, const InteractionState& interaction) {
248   QColor bg_color;
249   QColor fg_color;
250   if (m_alignment.isNull()) {
251     // "Align with other pages" is turned off.
252     // Different color is useful on a thumbnail list to
253     // distinguish "safe" pages from potentially problematic ones.
254     bg_color = QColor(0x58, 0x7f, 0xf4, 70);
255     fg_color = QColor(0x00, 0x52, 0xff);
256   } else {
257     bg_color = QColor(0xbb, 0x00, 0xff, 40);
258     fg_color = QColor(0xbe, 0x5b, 0xec);
259   }
260 
261   QPainterPath outer_outline;
262   outer_outline.addPolygon(PolygonUtils::round(m_alignment.isNull() ? m_middleRect : m_outerRect));
263 
264   QPainterPath content_outline;
265   content_outline.addPolygon(PolygonUtils::round(m_innerRect));
266 
267   painter.setRenderHint(QPainter::Antialiasing, false);
268 
269   painter.setPen(Qt::NoPen);
270   painter.setBrush(bg_color);
271 
272   if (!m_nullContentRect) {
273     painter.drawPath(outer_outline.subtracted(content_outline));
274   } else {
275     painter.drawPath(outer_outline);
276   }
277 
278   QPen pen(fg_color);
279   pen.setCosmetic(true);
280   pen.setWidthF(2.0);
281   painter.setPen(pen);
282   painter.setBrush(Qt::NoBrush);
283 
284   if (isShowingMiddleRectEnabled()) {
285     painter.drawRect(m_middleRect);
286   }
287   if (!m_nullContentRect) {
288     painter.drawRect(m_innerRect);
289   }
290 
291   if (!m_alignment.isNull()) {
292     pen.setStyle(Qt::DashLine);
293     painter.setPen(pen);
294     painter.drawRect(m_outerRect);
295 
296     // Draw guides.
297     if (!m_guides.empty()) {
298       painter.setWorldTransform(QTransform());
299 
300       QPen pen(QColor(0x00, 0x9d, 0x9f));
301       pen.setStyle(Qt::DashLine);
302       pen.setCosmetic(true);
303       pen.setWidthF(2.0);
304       painter.setPen(pen);
305       painter.setBrush(Qt::NoBrush);
306 
307       for (const auto& idxAndGuide : m_guides) {
308         const QLineF guide(widgetGuideLine(idxAndGuide.first));
309         painter.drawLine(guide);
310       }
311     }
312   }
313 }  // ImageView::onPaint
314 
cornerProximity(const int edge_mask,const QRectF * box,const QPointF & mouse_pos) const315 Proximity ImageView::cornerProximity(const int edge_mask, const QRectF* box, const QPointF& mouse_pos) const {
316   const QRectF r(virtualToWidget().mapRect(*box));
317   QPointF pt;
318 
319   if (edge_mask & TOP) {
320     pt.setY(r.top());
321   } else if (edge_mask & BOTTOM) {
322     pt.setY(r.bottom());
323   }
324 
325   if (edge_mask & LEFT) {
326     pt.setX(r.left());
327   } else if (edge_mask & RIGHT) {
328     pt.setX(r.right());
329   }
330 
331   return Proximity(pt, mouse_pos);
332 }
333 
edgeProximity(const int edge_mask,const QRectF * box,const QPointF & mouse_pos) const334 Proximity ImageView::edgeProximity(const int edge_mask, const QRectF* box, const QPointF& mouse_pos) const {
335   const QRectF r(virtualToWidget().mapRect(*box));
336   QLineF line;
337 
338   switch (edge_mask) {
339     case TOP:
340       line.setP1(r.topLeft());
341       line.setP2(r.topRight());
342       break;
343     case BOTTOM:
344       line.setP1(r.bottomLeft());
345       line.setP2(r.bottomRight());
346       break;
347     case LEFT:
348       line.setP1(r.topLeft());
349       line.setP2(r.bottomLeft());
350       break;
351     case RIGHT:
352       line.setP1(r.topRight());
353       line.setP2(r.bottomRight());
354       break;
355     default:
356       assert(!"Unreachable");
357   }
358 
359   return Proximity::pointAndLineSegment(mouse_pos, line);
360 }
361 
dragInitiated(const QPointF & mouse_pos)362 void ImageView::dragInitiated(const QPointF& mouse_pos) {
363   m_beforeResizing.middleWidgetRect = virtualToWidget().mapRect(m_middleRect);
364   m_beforeResizing.virtToWidget = virtualToWidget();
365   m_beforeResizing.widgetToVirt = widgetToVirtual();
366   m_beforeResizing.mousePos = mouse_pos;
367   m_beforeResizing.focalPoint = getWidgetFocalPoint();
368 }
369 
innerRectDragContinuation(int edge_mask,const QPointF & mouse_pos)370 void ImageView::innerRectDragContinuation(int edge_mask, const QPointF& mouse_pos) {
371   // What really happens when we resize the inner box is resizing
372   // the middle box in the opposite direction and moving the scene
373   // on screen so that the object being dragged is still under mouse.
374 
375   const QPointF delta(mouse_pos - m_beforeResizing.mousePos);
376   qreal left_adjust = 0;
377   qreal right_adjust = 0;
378   qreal top_adjust = 0;
379   qreal bottom_adjust = 0;
380 
381   if (edge_mask & LEFT) {
382     left_adjust = delta.x();
383     if (m_leftRightLinked) {
384       right_adjust = -left_adjust;
385     }
386   } else if (edge_mask & RIGHT) {
387     right_adjust = delta.x();
388     if (m_leftRightLinked) {
389       left_adjust = -right_adjust;
390     }
391   }
392   if (edge_mask & TOP) {
393     top_adjust = delta.y();
394     if (m_topBottomLinked) {
395       bottom_adjust = -top_adjust;
396     }
397   } else if (edge_mask & BOTTOM) {
398     bottom_adjust = delta.y();
399     if (m_topBottomLinked) {
400       top_adjust = -bottom_adjust;
401     }
402   }
403 
404   QRectF widget_rect(m_beforeResizing.middleWidgetRect);
405   widget_rect.adjust(-left_adjust, -top_adjust, -right_adjust, -bottom_adjust);
406 
407   m_middleRect = m_beforeResizing.widgetToVirt.mapRect(widget_rect);
408   forceNonNegativeHardMargins(m_middleRect);
409   widget_rect = m_beforeResizing.virtToWidget.mapRect(m_middleRect);
410 
411   qreal effective_dx = 0;
412   qreal effective_dy = 0;
413 
414   const QRectF& old_widget_rect = m_beforeResizing.middleWidgetRect;
415   if (edge_mask & LEFT) {
416     effective_dx = old_widget_rect.left() - widget_rect.left();
417   } else if (edge_mask & RIGHT) {
418     effective_dx = old_widget_rect.right() - widget_rect.right();
419   }
420   if (edge_mask & TOP) {
421     effective_dy = old_widget_rect.top() - widget_rect.top();
422   } else if (edge_mask & BOTTOM) {
423     effective_dy = old_widget_rect.bottom() - widget_rect.bottom();
424   }
425 
426   // Updating the focal point is what makes the image move
427   // as we drag an inner edge.
428   QPointF fp(m_beforeResizing.focalPoint);
429   fp += QPointF(effective_dx, effective_dy);
430   setWidgetFocalPoint(fp);
431 
432   m_aggregateHardSizeMM = m_settings->getAggregateHardSizeMM(m_pageId, origRectToSizeMM(m_middleRect), m_alignment);
433 
434   recalcOuterRect();
435 
436   updatePresentationTransform(DONT_FIT);
437 
438   emit marginsSetLocally(calcHardMarginsMM());
439 }  // ImageView::innerRectDragContinuation
440 
middleRectDragContinuation(const int edge_mask,const QPointF & mouse_pos)441 void ImageView::middleRectDragContinuation(const int edge_mask, const QPointF& mouse_pos) {
442   const QPointF delta(mouse_pos - m_beforeResizing.mousePos);
443   qreal left_adjust = 0;
444   qreal right_adjust = 0;
445   qreal top_adjust = 0;
446   qreal bottom_adjust = 0;
447 
448   const QRectF bounds(maxViewportRect());
449   const QRectF old_middle_rect(m_beforeResizing.middleWidgetRect);
450 
451   if (edge_mask & LEFT) {
452     left_adjust = delta.x();
453     if (old_middle_rect.left() + left_adjust < bounds.left()) {
454       left_adjust = bounds.left() - old_middle_rect.left();
455     }
456     if (m_leftRightLinked) {
457       right_adjust = -left_adjust;
458     }
459   } else if (edge_mask & RIGHT) {
460     right_adjust = delta.x();
461     if (old_middle_rect.right() + right_adjust > bounds.right()) {
462       right_adjust = bounds.right() - old_middle_rect.right();
463     }
464     if (m_leftRightLinked) {
465       left_adjust = -right_adjust;
466     }
467   }
468   if (edge_mask & TOP) {
469     top_adjust = delta.y();
470     if (old_middle_rect.top() + top_adjust < bounds.top()) {
471       top_adjust = bounds.top() - old_middle_rect.top();
472     }
473     if (m_topBottomLinked) {
474       bottom_adjust = -top_adjust;
475     }
476   } else if (edge_mask & BOTTOM) {
477     bottom_adjust = delta.y();
478     if (old_middle_rect.bottom() + bottom_adjust > bounds.bottom()) {
479       bottom_adjust = bounds.bottom() - old_middle_rect.bottom();
480     }
481     if (m_topBottomLinked) {
482       top_adjust = -bottom_adjust;
483     }
484   }
485 
486   {
487     QRectF widget_rect(old_middle_rect);
488     widget_rect.adjust(left_adjust, top_adjust, right_adjust, bottom_adjust);
489 
490     m_middleRect = m_beforeResizing.widgetToVirt.mapRect(widget_rect);
491     forceNonNegativeHardMargins(m_middleRect);  // invalidates widget_rect
492   }
493 
494   m_aggregateHardSizeMM = m_settings->getAggregateHardSizeMM(m_pageId, origRectToSizeMM(m_middleRect), m_alignment);
495 
496   recalcOuterRect();
497 
498   updatePresentationTransform(DONT_FIT);
499 
500   emit marginsSetLocally(calcHardMarginsMM());
501 }  // ImageView::middleRectDragContinuation
502 
dragFinished()503 void ImageView::dragFinished() {
504   const AggregateSizeChanged agg_size_changed(commitHardMargins(calcHardMarginsMM()));
505 
506   const QRectF extended_viewport(maxViewportRect().adjusted(-0.5, -0.5, 0.5, 0.5));
507   if (extended_viewport.contains(m_beforeResizing.middleWidgetRect)) {
508     updatePresentationTransform(FIT);
509   } else {
510     updatePresentationTransform(DONT_FIT);
511   }
512 
513   invalidateThumbnails(agg_size_changed);
514 }
515 
516 /**
517  * Updates m_middleRect and m_outerRect based on \p margins_mm,
518  * m_aggregateHardSizeMM and m_alignment, updates the displayed area.
519  */
recalcBoxesAndFit(const Margins & margins_mm)520 void ImageView::recalcBoxesAndFit(const Margins& margins_mm) {
521   const QTransform virt_to_mm(virtualToImage() * m_pixelsToMmXform);
522   const QTransform mm_to_virt(m_mmToPixelsXform * imageToVirtual());
523 
524   QPolygonF poly_mm(virt_to_mm.map(m_innerRect));
525   Utils::extendPolyRectWithMargins(poly_mm, margins_mm);
526 
527   const QRectF middle_rect(mm_to_virt.map(poly_mm).boundingRect());
528 
529   const QSizeF hard_size_mm(QLineF(poly_mm[0], poly_mm[1]).length(), QLineF(poly_mm[0], poly_mm[3]).length());
530   const Margins soft_margins_mm(Utils::calcSoftMarginsMM(
531       hard_size_mm, m_aggregateHardSizeMM, m_alignment, m_settings->getPageParams(m_pageId)->contentRect(),
532       m_settings->getPageParams(m_pageId)->contentSizeMM(), m_settings->getPageParams(m_pageId)->pageRect()));
533 
534   Utils::extendPolyRectWithMargins(poly_mm, soft_margins_mm);
535 
536   const QRectF outer_rect(mm_to_virt.map(poly_mm).boundingRect());
537   updateTransformAndFixFocalPoint(
538       ImagePresentation(imageToVirtual(), m_xform.resultingPreCropArea().intersected(outer_rect), outer_rect),
539       CENTER_IF_FITS);
540 
541   m_middleRect = middle_rect;
542   m_outerRect = outer_rect;
543 
544   updatePhysSize();
545 
546   forceInscribeGuides();
547 }
548 
549 /**
550  * Updates the virtual image area to be displayed by ImageViewBase,
551  * optionally ensuring that this area completely fits into the view.
552  *
553  * \note virtualToImage() and imageToVirtual() are not affected by this.
554  */
updatePresentationTransform(const FitMode fit_mode)555 void ImageView::updatePresentationTransform(const FitMode fit_mode) {
556   if (fit_mode == DONT_FIT) {
557     updateTransformPreservingScale(
558         ImagePresentation(imageToVirtual(), m_xform.resultingPreCropArea().intersected(m_outerRect), m_outerRect));
559   } else {
560     setZoomLevel(1.0);
561     updateTransformAndFixFocalPoint(
562         ImagePresentation(imageToVirtual(), m_xform.resultingPreCropArea().intersected(m_outerRect), m_outerRect),
563         CENTER_IF_FITS);
564   }
565 }
566 
forceNonNegativeHardMargins(QRectF & middle_rect) const567 void ImageView::forceNonNegativeHardMargins(QRectF& middle_rect) const {
568   if (middle_rect.left() > m_innerRect.left()) {
569     middle_rect.setLeft(m_innerRect.left());
570   }
571   if (middle_rect.right() < m_innerRect.right()) {
572     middle_rect.setRight(m_innerRect.right());
573   }
574   if (middle_rect.top() > m_innerRect.top()) {
575     middle_rect.setTop(m_innerRect.top());
576   }
577   if (middle_rect.bottom() < m_innerRect.bottom()) {
578     middle_rect.setBottom(m_innerRect.bottom());
579   }
580 }
581 
582 /**
583  * \brief Calculates margins in millimeters between m_innerRect and m_middleRect.
584  */
calcHardMarginsMM() const585 Margins ImageView::calcHardMarginsMM() const {
586   const QPointF center(m_innerRect.center());
587 
588   const QLineF top_margin_line(QPointF(center.x(), m_middleRect.top()), QPointF(center.x(), m_innerRect.top()));
589 
590   const QLineF bottom_margin_line(QPointF(center.x(), m_innerRect.bottom()),
591                                   QPointF(center.x(), m_middleRect.bottom()));
592 
593   const QLineF left_margin_line(QPointF(m_middleRect.left(), center.y()), QPointF(m_innerRect.left(), center.y()));
594 
595   const QLineF right_margin_line(QPointF(m_innerRect.right(), center.y()), QPointF(m_middleRect.right(), center.y()));
596 
597   const QTransform virt_to_mm(virtualToImage() * m_pixelsToMmXform);
598 
599   Margins margins;
600   margins.setTop(virt_to_mm.map(top_margin_line).length());
601   margins.setBottom(virt_to_mm.map(bottom_margin_line).length());
602   margins.setLeft(virt_to_mm.map(left_margin_line).length());
603   margins.setRight(virt_to_mm.map(right_margin_line).length());
604 
605   return margins;
606 }  // ImageView::calcHardMarginsMM
607 
608 /**
609  * \brief Recalculates m_outerRect based on m_middleRect, m_aggregateHardSizeMM
610  *        and m_alignment.
611  */
recalcOuterRect()612 void ImageView::recalcOuterRect() {
613   const QTransform virt_to_mm(virtualToImage() * m_pixelsToMmXform);
614   const QTransform mm_to_virt(m_mmToPixelsXform * imageToVirtual());
615 
616   QPolygonF poly_mm(virt_to_mm.map(m_middleRect));
617 
618   const QSizeF hard_size_mm(QLineF(poly_mm[0], poly_mm[1]).length(), QLineF(poly_mm[0], poly_mm[3]).length());
619   const Margins soft_margins_mm(Utils::calcSoftMarginsMM(
620       hard_size_mm, m_aggregateHardSizeMM, m_alignment, m_settings->getPageParams(m_pageId)->contentRect(),
621       m_settings->getPageParams(m_pageId)->contentSizeMM(), m_settings->getPageParams(m_pageId)->pageRect()));
622 
623   Utils::extendPolyRectWithMargins(poly_mm, soft_margins_mm);
624 
625   m_outerRect = mm_to_virt.map(poly_mm).boundingRect();
626   updatePhysSize();
627 
628   forceInscribeGuides();
629 }
630 
origRectToSizeMM(const QRectF & rect) const631 QSizeF ImageView::origRectToSizeMM(const QRectF& rect) const {
632   const QTransform virt_to_mm(virtualToImage() * m_pixelsToMmXform);
633 
634   const QLineF hor_line(rect.topLeft(), rect.topRight());
635   const QLineF vert_line(rect.topLeft(), rect.bottomLeft());
636 
637   const QSizeF size_mm(virt_to_mm.map(hor_line).length(), virt_to_mm.map(vert_line).length());
638 
639   return size_mm;
640 }
641 
commitHardMargins(const Margins & margins_mm)642 ImageView::AggregateSizeChanged ImageView::commitHardMargins(const Margins& margins_mm) {
643   m_settings->setHardMarginsMM(m_pageId, margins_mm);
644   m_aggregateHardSizeMM = m_settings->getAggregateHardSizeMM();
645 
646   AggregateSizeChanged changed = AGGREGATE_SIZE_UNCHANGED;
647   if (m_committedAggregateHardSizeMM != m_aggregateHardSizeMM) {
648     changed = AGGREGATE_SIZE_CHANGED;
649   }
650 
651   m_committedAggregateHardSizeMM = m_aggregateHardSizeMM;
652 
653   return changed;
654 }
655 
invalidateThumbnails(const AggregateSizeChanged agg_size_changed)656 void ImageView::invalidateThumbnails(const AggregateSizeChanged agg_size_changed) {
657   if (agg_size_changed == AGGREGATE_SIZE_CHANGED) {
658     emit invalidateAllThumbnails();
659   } else {
660     emit invalidateThumbnail(m_pageId);
661   }
662 }
663 
updatePhysSize()664 void ImageView::updatePhysSize() {
665   if (m_outerRect.isValid()) {
666     infoProvider().setPhysSize(m_outerRect.size());
667   } else {
668     ImageViewBase::updatePhysSize();
669   }
670 }
671 
setupContextMenuInteraction()672 void ImageView::setupContextMenuInteraction() {
673   m_addHorizontalGuideAction = m_contextMenu->addAction(tr("Add a horizontal guide"));
674   m_addVerticalGuideAction = m_contextMenu->addAction(tr("Add a vertical guide"));
675   m_removeAllGuidesAction = m_contextMenu->addAction(tr("Remove all the guides"));
676   m_removeGuideUnderMouseAction = m_contextMenu->addAction(tr("Remove this guide"));
677   m_guideActionsSeparator = m_contextMenu->addSeparator();
678   m_showMiddleRectAction = m_contextMenu->addAction(tr("Show hard margins rectangle"));
679   m_showMiddleRectAction->setCheckable(true);
680   m_showMiddleRectAction->setChecked(m_settings->isShowingMiddleRectEnabled());
681 
682   connect(m_addHorizontalGuideAction, &QAction::triggered,
683           [this]() { addHorizontalGuide(widgetToGuideCs().map(m_lastContextMenuPos).y()); });
684   connect(m_addVerticalGuideAction, &QAction::triggered,
685           [this]() { addVerticalGuide(widgetToGuideCs().map(m_lastContextMenuPos).x()); });
686   connect(m_removeAllGuidesAction, &QAction::triggered, boost::bind(&ImageView::removeAllGuides, this));
687   connect(m_removeGuideUnderMouseAction, &QAction::triggered, [this]() { removeGuide(m_guideUnderMouse); });
688   connect(m_showMiddleRectAction, &QAction::toggled, [this](bool checked) {
689     if (!m_alignment.isNull() && !m_nullContentRect) {
690       enableMiddleRectInteraction(checked);
691       m_settings->enableShowingMiddleRect(checked);
692     }
693   });
694 }
695 
onContextMenuEvent(QContextMenuEvent * event,InteractionState & interaction)696 void ImageView::onContextMenuEvent(QContextMenuEvent* event, InteractionState& interaction) {
697   if (interaction.captured()) {
698     // No context menus during resizing.
699     return;
700   }
701   if (m_alignment.isNull()) {
702     return;
703   }
704 
705   const QPointF eventPos = QPointF(0.5, 0.5) + event->pos();
706   // No context menus outside the outer rect.
707   if (!m_outerRect.contains(widgetToVirtual().map(eventPos))) {
708     return;
709   }
710 
711   m_guideUnderMouse = getGuideUnderMouse(eventPos, interaction);
712   if (m_guideUnderMouse == -1) {
713     m_addHorizontalGuideAction->setVisible(true);
714     m_addVerticalGuideAction->setVisible(true);
715     m_removeAllGuidesAction->setVisible(!m_guides.empty());
716     m_removeGuideUnderMouseAction->setVisible(false);
717     m_guideActionsSeparator->setVisible(!m_nullContentRect);
718     m_showMiddleRectAction->setVisible(!m_nullContentRect);
719 
720     m_lastContextMenuPos = eventPos;
721   } else {
722     m_addHorizontalGuideAction->setVisible(false);
723     m_addVerticalGuideAction->setVisible(false);
724     m_removeAllGuidesAction->setVisible(false);
725     m_removeGuideUnderMouseAction->setVisible(true);
726     m_guideActionsSeparator->setVisible(false);
727     m_showMiddleRectAction->setVisible(false);
728   }
729 
730   m_contextMenu->popup(event->globalPos());
731 }
732 
setupGuides()733 void ImageView::setupGuides() {
734   for (const Guide& guide : m_settings->guides()) {
735     m_guides[m_guidesFreeIndex] = guide;
736     setupGuideInteraction(m_guidesFreeIndex++);
737   }
738 }
739 
addHorizontalGuide(double y)740 void ImageView::addHorizontalGuide(double y) {
741   if (m_alignment.isNull()) {
742     return;
743   }
744 
745   m_guides[m_guidesFreeIndex] = Guide(Qt::Horizontal, y);
746   setupGuideInteraction(m_guidesFreeIndex++);
747 
748   update();
749   syncGuidesSettings();
750 }
751 
addVerticalGuide(double x)752 void ImageView::addVerticalGuide(double x) {
753   if (m_alignment.isNull()) {
754     return;
755   }
756 
757   m_guides[m_guidesFreeIndex] = Guide(Qt::Vertical, x);
758   setupGuideInteraction(m_guidesFreeIndex++);
759 
760   update();
761   syncGuidesSettings();
762 }
763 
removeAllGuides()764 void ImageView::removeAllGuides() {
765   if (m_alignment.isNull()) {
766     return;
767   }
768 
769   m_draggableGuideHandlers.clear();
770   m_draggableGuides.clear();
771 
772   m_guides.clear();
773   m_guidesFreeIndex = 0;
774 
775   update();
776   syncGuidesSettings();
777 }
778 
removeGuide(const int index)779 void ImageView::removeGuide(const int index) {
780   if (m_alignment.isNull()) {
781     return;
782   }
783   if (m_guides.find(index) == m_guides.end()) {
784     return;
785   }
786 
787   m_draggableGuideHandlers.erase(index);
788   m_draggableGuides.erase(index);
789 
790   m_guides.erase(index);
791 
792   update();
793   syncGuidesSettings();
794 }
795 
widgetToGuideCs() const796 QTransform ImageView::widgetToGuideCs() const {
797   QTransform xform(widgetToVirtual());
798   xform *= QTransform().translate(-m_outerRect.center().x(), -m_outerRect.center().y());
799 
800   return xform;
801 }
802 
guideToWidgetCs() const803 QTransform ImageView::guideToWidgetCs() const {
804   return widgetToGuideCs().inverted();
805 }
806 
syncGuidesSettings()807 void ImageView::syncGuidesSettings() {
808   m_settings->guides().clear();
809   for (const auto& idxAndGuide : m_guides) {
810     m_settings->guides().push_back(idxAndGuide.second);
811   }
812 }
813 
setupGuideInteraction(const int index)814 void ImageView::setupGuideInteraction(const int index) {
815   m_draggableGuides[index].setProximityPriority(1);
816   m_draggableGuides[index].setPositionCallback(boost::bind(&ImageView::guidePosition, this, index));
817   m_draggableGuides[index].setMoveRequestCallback(boost::bind(&ImageView::guideMoveRequest, this, index, _1));
818   m_draggableGuides[index].setDragFinishedCallback(boost::bind(&ImageView::guideDragFinished, this));
819 
820   const Qt::CursorShape cursorShape
821       = (m_guides[index].getOrientation() == Qt::Horizontal) ? Qt::SplitVCursor : Qt::SplitHCursor;
822   m_draggableGuideHandlers[index].setObject(&m_draggableGuides[index]);
823   m_draggableGuideHandlers[index].setProximityCursor(cursorShape);
824   m_draggableGuideHandlers[index].setInteractionCursor(cursorShape);
825   m_draggableGuideHandlers[index].setProximityStatusTip(tr("Drag the guide."));
826   m_draggableGuideHandlers[index].setKeyboardModifiers({Qt::ShiftModifier});
827 
828   if (!m_alignment.isNull()) {
829     makeLastFollower(m_draggableGuideHandlers[index]);
830   }
831 }
832 
guidePosition(const int index) const833 QLineF ImageView::guidePosition(const int index) const {
834   return widgetGuideLine(index);
835 }
836 
guideMoveRequest(const int index,QLineF line)837 void ImageView::guideMoveRequest(const int index, QLineF line) {
838   const QRectF valid_area(virtualToWidget().mapRect(m_outerRect));
839 
840   // Limit movement.
841   if (m_guides[index].getOrientation() == Qt::Horizontal) {
842     const double linePos = line.y1();
843     const double top = valid_area.top() - linePos;
844     const double bottom = linePos - valid_area.bottom();
845     if (top > 0.0) {
846       line.translate(0.0, top);
847     } else if (bottom > 0.0) {
848       line.translate(0.0, -bottom);
849     }
850   } else {
851     const double linePos = line.x1();
852     const double left = valid_area.left() - linePos;
853     const double right = linePos - valid_area.right();
854     if (left > 0.0) {
855       line.translate(left, 0.0);
856     } else if (right > 0.0) {
857       line.translate(-right, 0.0);
858     }
859   }
860 
861   m_guides[index] = widgetToGuideCs().map(line);
862   update();
863 }
864 
guideDragFinished()865 void ImageView::guideDragFinished() {
866   syncGuidesSettings();
867 }
868 
widgetGuideLine(const int index) const869 QLineF ImageView::widgetGuideLine(const int index) const {
870   const QRectF widget_rect(viewport()->rect());
871   const Guide& guide = m_guides.at(index);
872   QLineF guideLine = guideToWidgetCs().map(guide);
873   if (guide.getOrientation() == Qt::Horizontal) {
874     guideLine = QLineF(widget_rect.left(), guideLine.y1(), widget_rect.right(), guideLine.y2());
875   } else {
876     guideLine = QLineF(guideLine.x1(), widget_rect.top(), guideLine.x2(), widget_rect.bottom());
877   }
878 
879   return guideLine;
880 }
881 
getGuideUnderMouse(const QPointF & screenMousePos,const InteractionState & state) const882 int ImageView::getGuideUnderMouse(const QPointF& screenMousePos, const InteractionState& state) const {
883   for (const auto& idxAndGuide : m_guides) {
884     const QLineF guide(widgetGuideLine(idxAndGuide.first));
885     if (Proximity::pointAndLineSegment(screenMousePos, guide) <= state.proximityThreshold()) {
886       return idxAndGuide.first;
887     }
888   }
889 
890   return -1;
891 }
892 
enableGuidesInteraction(const bool state)893 void ImageView::enableGuidesInteraction(const bool state) {
894   if (state) {
895     for (auto& idxAndGuideHandler : m_draggableGuideHandlers) {
896       makeLastFollower(idxAndGuideHandler.second);
897     }
898   } else {
899     for (auto& idxAndGuideHandler : m_draggableGuideHandlers) {
900       idxAndGuideHandler.second.unlink();
901     }
902   }
903 }
904 
forceInscribeGuides()905 void ImageView::forceInscribeGuides() {
906   if (m_alignment.isNull()) {
907     return;
908   }
909 
910   bool need_sync = false;
911   for (const auto& idxAndGuide : m_guides) {
912     const Guide& guide = idxAndGuide.second;
913     const double pos = guide.getPosition();
914     if (guide.getOrientation() == Qt::Vertical) {
915       if (std::abs(pos) > (m_outerRect.width() / 2)) {
916         m_guides[idxAndGuide.first].setPosition(std::copysign(m_outerRect.width() / 2, pos));
917         need_sync = true;
918       }
919     } else {
920       if (std::abs(pos) > (m_outerRect.height() / 2)) {
921         m_guides[idxAndGuide.first].setPosition(std::copysign(m_outerRect.height() / 2, pos));
922         need_sync = true;
923       }
924     }
925   }
926 
927   if (need_sync) {
928     syncGuidesSettings();
929   }
930 }
931 
innerRectMoveRequest(const QPointF & mouse_pos,const Qt::KeyboardModifiers mask)932 void ImageView::innerRectMoveRequest(const QPointF& mouse_pos, const Qt::KeyboardModifiers mask) {
933   QPointF delta(mouse_pos - m_beforeResizing.mousePos);
934   if (mask == m_innerRectVerticalDragModifier) {
935     delta.setX(0);
936   } else if (mask == m_innerRectHorizontalDragModifier) {
937     delta.setY(0);
938   }
939 
940   QRectF widget_rect(m_beforeResizing.middleWidgetRect);
941   widget_rect.translate(-delta);
942   m_middleRect = m_beforeResizing.widgetToVirt.mapRect(widget_rect);
943   forceNonNegativeHardMargins(m_middleRect);
944 
945   // Updating the focal point is what makes the image move
946   // as we drag an inner edge.
947   QPointF fp(m_beforeResizing.focalPoint);
948   fp += delta;
949   setWidgetFocalPoint(fp);
950 
951   m_aggregateHardSizeMM = m_settings->getAggregateHardSizeMM(m_pageId, origRectToSizeMM(m_middleRect), m_alignment);
952 
953   recalcOuterRect();
954 
955   updatePresentationTransform(DONT_FIT);
956 
957   emit marginsSetLocally(calcHardMarginsMM());
958 }
959 
rectProximity(const QRectF & box,const QPointF & mouse_pos) const960 Proximity ImageView::rectProximity(const QRectF& box, const QPointF& mouse_pos) const {
961   double value = virtualToWidget().mapRect(box).contains(mouse_pos) ? 0 : std::numeric_limits<double>::max();
962   return Proximity::fromSqDist(value);
963 }
964 
buildContentImage(const GrayImage & gray_image,const ImageTransformation & xform)965 void ImageView::buildContentImage(const GrayImage& gray_image, const ImageTransformation& xform) {
966   ImageTransformation xform_150dpi(xform);
967   xform_150dpi.preScaleToDpi(Dpi(150, 150));
968 
969   if (xform_150dpi.resultingRect().toRect().isEmpty()) {
970     return;
971   }
972 
973   QImage gray150(transformToGray(gray_image, xform_150dpi.transform(), xform_150dpi.resultingRect().toRect(),
974                                  OutsidePixels::assumeColor(Qt::white)));
975 
976   m_contentImage = binarizeWolf(gray150, QSize(51, 51), 50);
977 
978   PolygonRasterizer::fillExcept(m_contentImage, WHITE, xform_150dpi.resultingPreCropArea(), Qt::WindingFill);
979 
980   Despeckle::despeckleInPlace(m_contentImage, Dpi(150, 150), Despeckle::NORMAL, EmptyTaskStatus());
981 
982   m_originalToContentImage = xform_150dpi.transform();
983   m_contentImageToOriginal = m_originalToContentImage.inverted();
984 }
985 
findContentInArea(const QRect & area) const986 QRect ImageView::findContentInArea(const QRect& area) const {
987   const uint32_t* image_line = m_contentImage.data();
988   const int image_stride = m_contentImage.wordsPerLine();
989   const uint32_t msb = uint32_t(1) << 31;
990 
991   int top = std::numeric_limits<int>::max();
992   int left = std::numeric_limits<int>::max();
993   int bottom = std::numeric_limits<int>::min();
994   int right = std::numeric_limits<int>::min();
995 
996   image_line += area.top() * image_stride;
997   for (int y = area.top(); y <= area.bottom(); ++y) {
998     for (int x = area.left(); x <= area.right(); ++x) {
999       if (image_line[x >> 5] & (msb >> (x & 31))) {
1000         top = std::min(top, y);
1001         left = std::min(left, x);
1002         bottom = std::max(bottom, y);
1003         right = std::max(right, x);
1004       }
1005     }
1006     image_line += image_stride;
1007   }
1008 
1009   if (top > bottom) {
1010     return QRect();
1011   }
1012 
1013   QRect found_area = QRect(left, top, right - left + 1, bottom - top + 1);
1014   found_area.adjust(-1, -1, 1, 1);
1015 
1016   return found_area;
1017 }
1018 
onMouseDoubleClickEvent(QMouseEvent * event,InteractionState & interaction)1019 void ImageView::onMouseDoubleClickEvent(QMouseEvent* event, InteractionState& interaction) {
1020   if (event->button() == Qt::LeftButton) {
1021     if (!m_alignment.isNull() && !m_guides.empty()) {
1022       attachContentToNearestGuide(QPointF(0.5, 0.5) + event->pos(), event->modifiers());
1023     }
1024   }
1025 }
1026 
attachContentToNearestGuide(const QPointF & pos,const Qt::KeyboardModifiers mask)1027 void ImageView::attachContentToNearestGuide(const QPointF& pos, const Qt::KeyboardModifiers mask) {
1028   const QTransform widget_to_content_image(widgetToImage() * m_originalToContentImage);
1029   const QTransform content_image_to_virtual(m_contentImageToOriginal * imageToVirtual());
1030 
1031   const QPointF content_pos = widget_to_content_image.map(pos);
1032 
1033   QRect finding_area((content_pos - QPointF(15, 15)).toPoint(), QSize(30, 30));
1034   finding_area = finding_area.intersected(m_contentImage.rect());
1035   if (finding_area.isEmpty()) {
1036     return;
1037   }
1038 
1039   QRect found_area = findContentInArea(finding_area);
1040   if (found_area.isEmpty()) {
1041     return;
1042   }
1043 
1044   const QRectF found_area_in_virtual = content_image_to_virtual.mapRect(QRectF(found_area)).intersected(m_innerRect);
1045   if (found_area_in_virtual.isEmpty()) {
1046     return;
1047   }
1048 
1049   QPointF delta;
1050   {
1051     const bool only_horizontal_direction = (mask == m_innerRectHorizontalDragModifier);
1052     const bool only_vertical_direction = (mask == m_innerRectVerticalDragModifier);
1053     const bool both_directions = (mask == (m_innerRectVerticalDragModifier | m_innerRectHorizontalDragModifier));
1054 
1055     double min_dist_x = std::numeric_limits<int>::max();
1056     double min_dist_y = std::numeric_limits<int>::max();
1057     for (const auto& idxAndGuide : m_guides) {
1058       const Guide& guide = idxAndGuide.second;
1059       if (guide.getOrientation() == Qt::Vertical) {
1060         if (only_vertical_direction) {
1061           continue;
1062         }
1063 
1064         const double guide_pos_in_virtual = guide.getPosition() + m_outerRect.center().x();
1065         const double diff_left = guide_pos_in_virtual - found_area_in_virtual.left();
1066         const double diff_right = guide_pos_in_virtual - found_area_in_virtual.right();
1067         const double diff = std::abs(diff_left) <= std::abs(diff_right) ? diff_left : diff_right;
1068         const double dist = std::abs(diff);
1069         const double min_dist = (both_directions) ? min_dist_x : std::min(min_dist_x, min_dist_y);
1070         if (dist < min_dist) {
1071           min_dist_x = dist;
1072           delta.setX(diff);
1073           if (!both_directions) {
1074             delta.setY(0.0);
1075           }
1076         }
1077       } else {
1078         if (only_horizontal_direction) {
1079           continue;
1080         }
1081 
1082         const double guide_pos_in_virtual = guide.getPosition() + m_outerRect.center().y();
1083         const double diff_top = guide_pos_in_virtual - found_area_in_virtual.top();
1084         const double diff_bottom = guide_pos_in_virtual - found_area_in_virtual.bottom();
1085         const double diff = std::abs(diff_top) <= std::abs(diff_bottom) ? diff_top : diff_bottom;
1086         const double dist = std::abs(diff);
1087         const double min_dist = (both_directions) ? min_dist_y : std::min(min_dist_x, min_dist_y);
1088         if (dist < min_dist) {
1089           min_dist_y = dist;
1090           delta.setY(diff);
1091           if (!both_directions) {
1092             delta.setX(0.0);
1093           }
1094         }
1095       }
1096     }
1097   }
1098   if (delta.isNull()) {
1099     return;
1100   }
1101 
1102   {
1103     QRectF corrected_middle_rect = m_middleRect;
1104     corrected_middle_rect.translate(-delta);
1105     corrected_middle_rect |= m_innerRect;
1106 
1107     {
1108       // Correct the delta in case of the middle rect size changed.
1109       // It means that the center is shifted resulting
1110       // the guide will change its position, so we need an extra addition to delta.
1111       const double x_correction
1112           = std::copysign(std::max(0.0, corrected_middle_rect.width() - m_middleRect.width()), delta.x());
1113       const double y_correction
1114           = std::copysign(std::max(0.0, corrected_middle_rect.height() - m_middleRect.height()), delta.y());
1115       delta.setX(delta.x() + x_correction);
1116       delta.setY(delta.y() + y_correction);
1117 
1118       corrected_middle_rect.translate(-x_correction, -y_correction);
1119       corrected_middle_rect |= m_innerRect;
1120     }
1121 
1122     {
1123       // Restrict the delta value in order not to be out of the outer rect.
1124       const double x_correction
1125           = std::copysign(std::max(0.0, corrected_middle_rect.width() - m_outerRect.width()), delta.x());
1126       const double y_correction
1127           = std::copysign(std::max(0.0, corrected_middle_rect.height() - m_outerRect.height()), delta.y());
1128       delta.setX(delta.x() - x_correction);
1129       delta.setY(delta.y() - y_correction);
1130     }
1131   }
1132 
1133   // Move the page content.
1134   dragInitiated(virtualToWidget().map(QPointF(0, 0)));
1135   innerRectMoveRequest(virtualToWidget().map(delta));
1136   dragFinished();
1137 }
1138 
enableMiddleRectInteraction(const bool state)1139 void ImageView::enableMiddleRectInteraction(const bool state) {
1140   bool internal_state = m_middleCornerHandlers[0].is_linked();
1141   if (state == internal_state) {
1142     // Don't enable or disable the interaction if that's already done.
1143     return;
1144   }
1145 
1146   if (state) {
1147     for (int i = 0; i < 4; ++i) {
1148       makeLastFollower(m_middleCornerHandlers[i]);
1149       makeLastFollower(m_middleEdgeHandlers[i]);
1150     }
1151   } else {
1152     for (int i = 0; i < 4; ++i) {
1153       m_middleCornerHandlers[i].unlink();
1154       m_middleEdgeHandlers[i].unlink();
1155     }
1156   }
1157 
1158   update();
1159 }
1160 
isShowingMiddleRectEnabled() const1161 bool ImageView::isShowingMiddleRectEnabled() const {
1162   return (!m_nullContentRect && m_settings->isShowingMiddleRectEnabled()) || m_alignment.isNull();
1163 }
1164 }  // namespace page_layout
1165