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