QGIS API Documentation 3.27.0-Master (75dc696944)
qgslayoutruler.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslayoutruler.cpp
3 ------------------
4 Date : July 2017
5 Copyright : (C) 2017 Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15#include "qgslayoutruler.h"
16#include "qgslayout.h"
17#include "qgis.h"
18#include "qgslayoutview.h"
19#include "qgslogger.h"
21#include <QDragEnterEvent>
22#include <QGraphicsLineItem>
23#include <QPainter>
24#include <QMenu>
25#include <cmath>
26
27const int RULER_FONT_SIZE = 8;
28const unsigned int COUNT_VALID_MULTIPLES = 3;
29const unsigned int COUNT_VALID_MAGNITUDES = 5;
30const int QgsLayoutRuler::VALID_SCALE_MULTIPLES[] = {1, 2, 5};
31const int QgsLayoutRuler::VALID_SCALE_MAGNITUDES[] = {1, 10, 100, 1000, 10000};
32
33QgsLayoutRuler::QgsLayoutRuler( QWidget *parent, Qt::Orientation orientation )
34 : QWidget( parent )
35 , mOrientation( orientation )
36{
37 setMouseTracking( true );
38
39 //calculate minimum size required for ruler text
40 mRulerFont.setPointSize( RULER_FONT_SIZE );
41 mRulerFontMetrics.reset( new QFontMetrics( mRulerFont ) );
42
43 //calculate ruler sizes and marker separations
44
45 //minimum gap required between major ticks is 3 digits * 250%, based on appearance
46 mScaleMinPixelsWidth = mRulerFontMetrics->boundingRect( QStringLiteral( "000" ) ).width() * 2.5;
47 //minimum ruler height is twice the font height in pixels
48 mRulerMinSize = mRulerFontMetrics->height() * 1.5;
49
50 mMinPixelsPerDivision = mRulerMinSize / 4;
51 //each small division must be at least 2 pixels apart
52 if ( mMinPixelsPerDivision < 2 )
53 mMinPixelsPerDivision = 2;
54
55 mPixelsBetweenLineAndText = mRulerMinSize / 10;
56 mTextBaseline = mRulerMinSize / 1.667;
57 mMinSpacingVerticalLabels = mRulerMinSize / 5;
58
59 const double guideMarkerSize = mRulerFontMetrics->horizontalAdvance( '*' );
60 mDragGuideTolerance = guideMarkerSize;
61 switch ( mOrientation )
62 {
63 case Qt::Horizontal:
64 mGuideMarker << QPoint( -guideMarkerSize / 2, mRulerMinSize - guideMarkerSize ) << QPoint( 0, mRulerMinSize ) <<
65 QPoint( guideMarkerSize / 2, mRulerMinSize - guideMarkerSize );
66 break;
67
68 case Qt::Vertical:
69 mGuideMarker << QPoint( mRulerMinSize - guideMarkerSize, -guideMarkerSize / 2 ) << QPoint( mRulerMinSize, 0 ) <<
70 QPoint( mRulerMinSize - guideMarkerSize, guideMarkerSize / 2 );
71 break;
72 }
73}
74
76{
77 return QSize( mRulerMinSize, mRulerMinSize );
78}
79
80void QgsLayoutRuler::paintEvent( QPaintEvent *event )
81{
82 Q_UNUSED( event )
83 if ( !mView || !mView->currentLayout() )
84 {
85 return;
86 }
87
88 QgsLayout *layout = mView->currentLayout();
89 QPainter p( this );
90
91 drawGuideMarkers( &p, layout );
92
93 const QTransform t = mTransform.inverted();
94 p.setFont( mRulerFont );
95 // keep same default color, but lower opacity a tad
96 QBrush brush = p.brush();
97 QColor color = brush.color();
98 color.setAlphaF( 0.7 );
99 brush.setColor( color );
100 p.setBrush( brush );
101 QPen pen = p.pen();
102 color = pen.color();
103 color.setAlphaF( 0.7 );
104 pen.setColor( color );
105 p.setPen( pen );
106
107 //find optimum scale for ruler (size of numbered divisions)
108 int magnitude = 1;
109 int multiple = 1;
110 const int mmDisplay = optimumScale( mScaleMinPixelsWidth, magnitude, multiple );
111
112 //find optimum number of small divisions
113 const int numSmallDivisions = optimumNumberDivisions( mmDisplay, multiple );
114
115 switch ( mOrientation )
116 {
117 case Qt::Horizontal:
118 {
119 if ( qgsDoubleNear( width(), 0 ) )
120 {
121 return;
122 }
123
124 //start x-coordinate
125 const double startX = t.map( QPointF( 0, 0 ) ).x();
126 const double endX = t.map( QPointF( width(), 0 ) ).x();
127
128 //start marker position in mm
129 double markerPos = ( std::floor( startX / mmDisplay ) + 1 ) * mmDisplay;
130
131 //draw minor ticks marks which occur before first major tick
132 drawSmallDivisions( &p, markerPos, numSmallDivisions, -mmDisplay );
133
134 while ( markerPos <= endX )
135 {
136 const double pixelCoord = mTransform.map( QPointF( markerPos, 0 ) ).x();
137
138 //draw large division and text
139 p.drawLine( pixelCoord, 0, pixelCoord, mRulerMinSize );
140 p.drawText( QPointF( pixelCoord + mPixelsBetweenLineAndText, mTextBaseline ), QLocale().toString( markerPos ) );
141
142 //draw small divisions
143 drawSmallDivisions( &p, markerPos, numSmallDivisions, mmDisplay, endX );
144
145 markerPos += mmDisplay;
146 }
147 break;
148 }
149 case Qt::Vertical:
150 {
151 if ( qgsDoubleNear( height(), 0 ) )
152 {
153 return;
154 }
155
156 const double startY = t.map( QPointF( 0, 0 ) ).y(); //start position in mm (total including space between pages)
157 const double endY = t.map( QPointF( 0, height() ) ).y(); //stop position in mm (total including space between pages)
158
159 // work out start page
160 int startPage = 0;
161 int endPage = 0;
162 double currentY = 0;
163 double currentPageY = 0;
164 for ( int page = 0; page < layout->pageCollection()->pageCount(); ++page )
165 {
166 if ( currentY < startY )
167 {
168 startPage = page;
169 currentPageY = currentY;
170 }
171 endPage = page;
172
173 currentY += layout->pageCollection()->page( startPage )->rect().height() + layout->pageCollection()->spaceBetweenPages();
174 if ( currentY > endY )
175 break;
176 }
177
178 if ( startY < 0 )
179 {
180 double beforePageCoord = -mmDisplay;
181 const double firstPageY = mTransform.map( QPointF( 0, 0 ) ).y();
182
183 //draw negative rulers which fall before first page
184 while ( beforePageCoord > startY )
185 {
186 const double pixelCoord = mTransform.map( QPointF( 0, beforePageCoord ) ).y();
187 p.drawLine( 0, pixelCoord, mRulerMinSize, pixelCoord );
188 //calc size of label
189 const QString label = QLocale().toString( beforePageCoord );
190 const int labelSize = mRulerFontMetrics->boundingRect( label ).width();
191
192 //draw label only if it fits in before start of next page
193 if ( pixelCoord + labelSize + 8 < firstPageY )
194 {
195 drawRotatedText( &p, QPointF( mTextBaseline, pixelCoord + mMinSpacingVerticalLabels + labelSize ), label );
196 }
197
198 //draw small divisions
199 drawSmallDivisions( &p, beforePageCoord, numSmallDivisions, mmDisplay );
200
201 beforePageCoord -= mmDisplay;
202 }
203
204 //draw minor ticks marks which occur before first major tick
205 drawSmallDivisions( &p, beforePageCoord + mmDisplay, numSmallDivisions, -mmDisplay, startY );
206 }
207
208 double nextPageStartPos = 0;
209 int nextPageStartPixel = 0;
210
211 for ( int i = startPage; i <= endPage; ++i )
212 {
213 double pageCoord = 0; //page coordinate in mm
214 //total (composition) coordinate in mm, including space between pages
215
216 double totalCoord = currentPageY;
217
218 //position of next page
219 if ( i < endPage )
220 {
221 //not the last page
222 nextPageStartPos = currentPageY + layout->pageCollection()->page( i )->rect().height() + layout->pageCollection()->spaceBetweenPages();
223 nextPageStartPixel = mTransform.map( QPointF( 0, nextPageStartPos ) ).y();
224 }
225 else
226 {
227 //is the last page
228 nextPageStartPos = 0;
229 nextPageStartPixel = 0;
230 }
231
232 while ( ( totalCoord < nextPageStartPos ) || ( ( nextPageStartPos == 0 ) && ( totalCoord <= endY ) ) )
233 {
234 const double pixelCoord = mTransform.map( QPointF( 0, totalCoord ) ).y();
235 p.drawLine( 0, pixelCoord, mRulerMinSize, pixelCoord );
236 //calc size of label
237 const QString label = QLocale().toString( pageCoord );
238 const int labelSize = mRulerFontMetrics->boundingRect( label ).width();
239
240 //draw label only if it fits in before start of next page
241 if ( ( pixelCoord + labelSize + 8 < nextPageStartPixel )
242 || ( nextPageStartPixel == 0 ) )
243 {
244 drawRotatedText( &p, QPointF( mTextBaseline, pixelCoord + mMinSpacingVerticalLabels + labelSize ), label );
245 }
246
247 //draw small divisions
248 drawSmallDivisions( &p, totalCoord, numSmallDivisions, mmDisplay, nextPageStartPos );
249
250 pageCoord += mmDisplay;
251 totalCoord += mmDisplay;
252 }
253 if ( i < endPage )
254 currentPageY += layout->pageCollection()->page( i )->rect().height() + layout->pageCollection()->spaceBetweenPages();
255 }
256 break;
257 }
258 }
259
260 //draw current marker pos
261 drawMarkerPos( &p );
262}
263
264void QgsLayoutRuler::drawMarkerPos( QPainter *painter )
265{
266 //draw current marker pos in red
267 painter->setPen( QColor( Qt::red ) );
268 switch ( mOrientation )
269 {
270 case Qt::Horizontal:
271 {
272 painter->drawLine( mMarkerPos.x(), 0, mMarkerPos.x(), mRulerMinSize );
273 break;
274 }
275 case Qt::Vertical:
276 {
277 painter->drawLine( 0, mMarkerPos.y(), mRulerMinSize, mMarkerPos.y() );
278 break;
279 }
280 }
281}
282
283void QgsLayoutRuler::drawGuideMarkers( QPainter *p, QgsLayout *layout )
284{
285 const QList< QgsLayoutItemPage * > visiblePages = mView->visiblePages();
286 const QList< QgsLayoutGuide * > guides = layout->guides().guides( mOrientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal );
287 const QgsScopedQPainterState painterState( p );
288 p->setRenderHint( QPainter::Antialiasing, true );
289 p->setPen( Qt::NoPen );
290 const auto constGuides = guides;
291 for ( QgsLayoutGuide *guide : constGuides )
292 {
293 if ( visiblePages.contains( guide->page() ) )
294 {
295 if ( guide == mHoverGuide )
296 {
297 p->setBrush( QBrush( QColor( 255, 0, 0, 225 ) ) );
298 }
299 else
300 {
301 p->setBrush( QBrush( QColor( 255, 0, 0, 150 ) ) );
302 }
303 QPointF point;
304 switch ( mOrientation )
305 {
306 case Qt::Horizontal:
307 point = QPointF( guide->layoutPosition(), 0 );
308 break;
309
310 case Qt::Vertical:
311 point = QPointF( 0, guide->layoutPosition() );
312 break;
313 }
314 drawGuideAtPos( p, convertLayoutPointToLocal( point ) );
315 }
316 }
317}
318
319void QgsLayoutRuler::drawGuideAtPos( QPainter *painter, QPoint pos )
320{
321 switch ( mOrientation )
322 {
323 case Qt::Horizontal:
324 {
325 painter->translate( pos.x(), 0 );
326 painter->drawPolygon( mGuideMarker );
327 painter->translate( -pos.x(), 0 );
328 break;
329 }
330 case Qt::Vertical:
331 {
332 painter->translate( 0, pos.y() );
333 painter->drawPolygon( mGuideMarker );
334 painter->translate( 0, -pos.y() );
335 break;
336 }
337 }
338}
339
340void QgsLayoutRuler::createTemporaryGuideItem()
341{
342 if ( !mView->currentLayout() )
343 return;
344
345 delete mGuideItem;
346 mGuideItem = new QGraphicsLineItem();
347
348 mGuideItem->setZValue( QgsLayout::ZGuide );
349 QPen linePen( Qt::DotLine );
350 linePen.setColor( QColor( 255, 0, 0, 150 ) );
351 linePen.setWidthF( 0 );
352 mGuideItem->setPen( linePen );
353
354 mView->currentLayout()->addItem( mGuideItem );
355}
356
357QPointF QgsLayoutRuler::convertLocalPointToLayout( QPoint localPoint ) const
358{
359 const QPoint viewPoint = mView->mapFromGlobal( mapToGlobal( localPoint ) );
360 return mView->mapToScene( viewPoint );
361}
362
363QPoint QgsLayoutRuler::convertLayoutPointToLocal( QPointF layoutPoint ) const
364{
365 const QPoint viewPoint = mView->mapFromScene( layoutPoint );
366 return mapFromGlobal( mView->mapToGlobal( viewPoint ) );
367}
368
369QgsLayoutGuide *QgsLayoutRuler::guideAtPoint( QPoint localPoint ) const
370{
371 if ( !mView->currentLayout() )
372 return nullptr;
373
374 const QPointF layoutPoint = convertLocalPointToLayout( localPoint );
375 const QList< QgsLayoutItemPage * > visiblePages = mView->visiblePages();
376 const QList< QgsLayoutGuide * > guides = mView->currentLayout()->guides().guides( mOrientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal );
377 QgsLayoutGuide *closestGuide = nullptr;
378 double minDelta = std::numeric_limits<double>::max();
379 const auto constGuides = guides;
380 for ( QgsLayoutGuide *guide : constGuides )
381 {
382 if ( visiblePages.contains( guide->page() ) )
383 {
384 double currentDelta = 0;
385 switch ( mOrientation )
386 {
387 case Qt::Horizontal:
388 currentDelta = std::fabs( layoutPoint.x() - guide->layoutPosition() );
389 break;
390
391 case Qt::Vertical:
392 currentDelta = std::fabs( layoutPoint.y() - guide->layoutPosition() );
393 break;
394 }
395 if ( currentDelta < minDelta )
396 {
397 minDelta = currentDelta;
398 closestGuide = guide;
399 }
400 }
401 }
402
403 if ( minDelta * mView->transform().m11() <= mDragGuideTolerance )
404 {
405 return closestGuide;
406 }
407 else
408 {
409 return nullptr;
410 }
411}
412
413void QgsLayoutRuler::drawRotatedText( QPainter *painter, QPointF pos, const QString &text )
414{
415 const QgsScopedQPainterState painterState( painter );
416 painter->translate( pos.x(), pos.y() );
417 painter->rotate( 270 );
418 painter->drawText( 0, 0, text );
419}
420
421void QgsLayoutRuler::drawSmallDivisions( QPainter *painter, double startPos, int numDivisions, double rulerScale, double maxPos )
422{
423 if ( numDivisions == 0 )
424 return;
425
426 //draw small divisions starting at startPos (in mm)
427 double smallMarkerPos = startPos;
428 const double smallDivisionSpacing = rulerScale / numDivisions;
429
430 double pixelCoord = 0.0;
431
432 //draw numDivisions small divisions
433 for ( int i = 0; i < numDivisions; ++i )
434 {
435 smallMarkerPos += smallDivisionSpacing;
436
437 if ( maxPos > 0 && smallMarkerPos > maxPos )
438 {
439 //stop drawing current division position is past maxPos
440 return;
441 }
442
443 //calculate pixelCoordinate of the current division
444 switch ( mOrientation )
445 {
446 case Qt::Horizontal:
447 {
448 pixelCoord = mTransform.map( QPointF( smallMarkerPos, 0 ) ).x();
449 break;
450 }
451 case Qt::Vertical:
452 {
453 pixelCoord = mTransform.map( QPointF( 0, smallMarkerPos ) ).y();
454 break;
455 }
456 }
457
458 //calculate height of small division line
459 double lineSize;
460 if ( ( numDivisions == 10 && i == 4 ) || ( numDivisions == 4 && i == 1 ) )
461 {
462 //if drawing the 5th line of 10 or drawing the 2nd line of 4, then draw it slightly longer
463 lineSize = mRulerMinSize / 1.5;
464 }
465 else
466 {
467 lineSize = mRulerMinSize / 1.25;
468 }
469
470 //draw either horizontal or vertical line depending on ruler direction
471 switch ( mOrientation )
472 {
473 case Qt::Horizontal:
474 {
475 painter->drawLine( pixelCoord, lineSize, pixelCoord, mRulerMinSize );
476 break;
477 }
478 case Qt::Vertical:
479 {
480 painter->drawLine( lineSize, pixelCoord, mRulerMinSize, pixelCoord );
481 break;
482 }
483 }
484 }
485}
486
487int QgsLayoutRuler::optimumScale( double minPixelDiff, int &magnitude, int &multiple )
488{
489 //find optimal ruler display scale
490
491 //loop through magnitudes and multiples to find optimum scale
492 for ( unsigned int magnitudeCandidate = 0; magnitudeCandidate < COUNT_VALID_MAGNITUDES; ++magnitudeCandidate )
493 {
494 for ( unsigned int multipleCandidate = 0; multipleCandidate < COUNT_VALID_MULTIPLES; ++multipleCandidate )
495 {
496 const int candidateScale = VALID_SCALE_MULTIPLES[multipleCandidate] * VALID_SCALE_MAGNITUDES[magnitudeCandidate];
497 //find pixel size for each step using this candidate scale
498 const double pixelDiff = mTransform.map( QPointF( candidateScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
499 if ( pixelDiff > minPixelDiff )
500 {
501 //found the optimum major scale
502 magnitude = VALID_SCALE_MAGNITUDES[magnitudeCandidate];
503 multiple = VALID_SCALE_MULTIPLES[multipleCandidate];
504 return candidateScale;
505 }
506 }
507 }
508
509 return 100000;
510}
511
512int QgsLayoutRuler::optimumNumberDivisions( double rulerScale, int scaleMultiple )
513{
514 //calculate size in pixels of each marked ruler unit
515 const double largeDivisionSize = mTransform.map( QPointF( rulerScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
516
517 //now calculate optimum small tick scale, depending on marked ruler units
518 QList<int> validSmallDivisions;
519 switch ( scaleMultiple )
520 {
521 case 1:
522 //numbers increase by 1 increment each time, e.g., 1, 2, 3 or 10, 20, 30
523 //so we can draw either 10, 5 or 2 small ticks and have each fall on a nice value
524 validSmallDivisions << 10 << 5 << 2;
525 break;
526 case 2:
527 //numbers increase by 2 increments each time, e.g., 2, 4, 6 or 20, 40, 60
528 //so we can draw either 10, 4 or 2 small ticks and have each fall on a nice value
529 validSmallDivisions << 10 << 4 << 2;
530 break;
531 case 5:
532 //numbers increase by 5 increments each time, e.g., 5, 10, 15 or 100, 500, 1000
533 //so we can draw either 10 or 5 small ticks and have each fall on a nice value
534 validSmallDivisions << 10 << 5;
535 break;
536 }
537
538 //calculate the most number of small divisions we can draw without them being too close to each other
539 QList<int>::iterator divisions_it;
540 for ( divisions_it = validSmallDivisions.begin(); divisions_it != validSmallDivisions.end(); ++divisions_it )
541 {
542 //find pixel size for this small division
543 const double candidateSize = largeDivisionSize / ( *divisions_it );
544 //check if this separation is more then allowed min separation
545 if ( candidateSize >= mMinPixelsPerDivision )
546 {
547 //found a good candidate, return it
548 return ( *divisions_it );
549 }
550 }
551
552 //unable to find a good candidate
553 return 0;
554}
555
556
557void QgsLayoutRuler::setSceneTransform( const QTransform &transform )
558{
559 mTransform = transform;
560 update();
561}
562
564{
565 mView = view;
567}
568
570{
571 mMenu = menu;
572}
573
574void QgsLayoutRuler::setCursorPosition( QPointF position )
575{
576 mMarkerPos = mView->mapFromScene( position );
577 update();
578}
579
580void QgsLayoutRuler::mouseMoveEvent( QMouseEvent *event )
581{
582 mMarkerPos = event->pos();
583 update();
584
585 if ( !mView->currentLayout() )
586 return;
587
588 QPointF displayPos;
589 if ( mCreatingGuide || mDraggingGuide )
590 {
591 // event -> layout coordinates
592 displayPos = convertLocalPointToLayout( event->pos() );
593
594 if ( mCreatingGuide )
595 {
596 QgsLayout *layout = mView->currentLayout();
597 const int pageNo = layout->pageCollection()->pageNumberForPoint( displayPos );
598 QgsLayoutItemPage *page = layout->pageCollection()->page( pageNo );
599 if ( !page )
600 return;
601
602 QPen linePen = mGuideItem->pen();
603 // if guide preview is outside a page draw it a lot fainter, to indicate it's invalid
604 if ( !layout->pageCollection()->pageAtPoint( displayPos ) )
605 {
606 linePen.setColor( QColor( 255, 0, 0, 150 ) );
607 }
608 else
609 {
610 linePen.setColor( QColor( 255, 0, 0, 225 ) );
611 }
612 mGuideItem->setPen( linePen );
613 switch ( mOrientation )
614 {
615 case Qt::Horizontal:
616 {
617 //mouse is creating a horizontal ruler, so don't show x coordinate
618 mGuideItem->setLine( page->scenePos().x(), displayPos.y(), page->scenePos().x() + page->rect().width(), displayPos.y() );
619 displayPos.setX( 0 );
620 break;
621 }
622 case Qt::Vertical:
623 {
624 //mouse is creating a vertical ruler, so don't show a y coordinate
625 mGuideItem->setLine( displayPos.x(), page->scenePos().y(), displayPos.x(), page->scenePos().y() + page->rect().height() );
626 displayPos.setY( 0 );
627 break;
628 }
629 }
630 }
631 else
632 {
633 // dragging guide
634 switch ( mOrientation )
635 {
636 case Qt::Horizontal:
637 {
638 mView->currentLayout()->guides().setGuideLayoutPosition( mDraggingGuide, displayPos.x() );
639 displayPos.setY( 0 );
640 break;
641 }
642 case Qt::Vertical:
643 {
644 mView->currentLayout()->guides().setGuideLayoutPosition( mDraggingGuide, displayPos.y() );
645 displayPos.setX( 0 );
646 break;
647 }
648 }
649 }
650 }
651 else
652 {
653 // is cursor over a guide marker?
654 mHoverGuide = guideAtPoint( event->pos() );
655 if ( mHoverGuide )
656 {
657 setCursor( mOrientation == Qt::Vertical ? Qt::SplitVCursor : Qt::SplitHCursor );
658 }
659 else
660 {
661 setCursor( Qt::ArrowCursor );
662 }
663
664 //update cursor position in status bar
665 displayPos = mTransform.inverted().map( event->pos() );
666 switch ( mOrientation )
667 {
668 case Qt::Horizontal:
669 {
670 //mouse is over a horizontal ruler, so don't show a y coordinate
671 displayPos.setY( 0 );
672 break;
673 }
674 case Qt::Vertical:
675 {
676 //mouse is over a vertical ruler, so don't show an x coordinate
677 displayPos.setX( 0 );
678 break;
679 }
680 }
681 }
682 emit cursorPosChanged( displayPos );
683}
684
685void QgsLayoutRuler::mousePressEvent( QMouseEvent *event )
686{
687 if ( !mView->currentLayout() )
688 return;
689
690 if ( event->button() == Qt::LeftButton )
691 {
692 mDraggingGuide = guideAtPoint( event->pos() );
693 if ( !mDraggingGuide )
694 {
695 // if no guide at the point, then we're creating one
696 if ( mView->currentLayout()->pageCollection()->pageCount() > 0 )
697 {
698 mCreatingGuide = true;
699 createTemporaryGuideItem();
700 }
701 }
702 else
703 {
704 mDraggingGuideOldPosition = mDraggingGuide->layoutPosition();
705 }
706 switch ( mOrientation )
707 {
708 case Qt::Horizontal:
709 {
710 QApplication::setOverrideCursor( mDraggingGuide ? Qt::SplitHCursor : Qt::SplitVCursor );
711 break;
712 }
713 case Qt::Vertical:
714 QApplication::setOverrideCursor( mDraggingGuide ? Qt::SplitVCursor : Qt::SplitHCursor );
715 break;
716 }
717 }
718}
719
720void QgsLayoutRuler::mouseReleaseEvent( QMouseEvent *event )
721{
722 if ( !mView->currentLayout() )
723 return;
724
725 if ( event->button() == Qt::LeftButton )
726 {
727 if ( mDraggingGuide )
728 {
729 QApplication::restoreOverrideCursor();
730
731 const QPointF layoutPoint = convertLocalPointToLayout( event->pos() );
732
733 // delete guide if it ends outside of page
734 QgsLayoutItemPage *page = mDraggingGuide->page();
735 bool deleteGuide = false;
736 switch ( mDraggingGuide->orientation() )
737 {
738 case Qt::Horizontal:
739 if ( layoutPoint.y() < page->scenePos().y() || layoutPoint.y() > page->scenePos().y() + page->rect().height() )
740 deleteGuide = true;
741 break;
742
743 case Qt::Vertical:
744 if ( layoutPoint.x() < page->scenePos().x() || layoutPoint.x() > page->scenePos().x() + page->rect().width() )
745 deleteGuide = true;
746 break;
747 }
748
749 if ( deleteGuide )
750 {
751 mView->currentLayout()->guides().removeGuide( mDraggingGuide );
752 }
753 mDraggingGuide = nullptr;
754 }
755 else
756 {
757 mCreatingGuide = false;
758 QApplication::restoreOverrideCursor();
759 delete mGuideItem;
760 mGuideItem = nullptr;
761
762 // check that cursor left the ruler
763 switch ( mOrientation )
764 {
765 case Qt::Horizontal:
766 {
767 if ( event->pos().y() <= height() )
768 return;
769 break;
770 }
771 case Qt::Vertical:
772 {
773 if ( event->pos().x() <= width() )
774 return;
775 break;
776 }
777 }
778
779 QgsLayout *layout = mView->currentLayout();
780
781 // create guide
782 const QPointF scenePos = convertLocalPointToLayout( event->pos() );
783 QgsLayoutItemPage *page = layout->pageCollection()->pageAtPoint( scenePos );
784 if ( !page )
785 return; // dragged outside of a page
786
787 std::unique_ptr< QgsLayoutGuide > guide;
788 switch ( mOrientation )
789 {
790 case Qt::Horizontal:
791 {
792 //mouse is creating a horizontal guide
793 const double posOnPage = layout->pageCollection()->positionOnPage( scenePos ).y();
794 guide.reset( new QgsLayoutGuide( Qt::Horizontal, QgsLayoutMeasurement( posOnPage, layout->units() ), page ) );
795 break;
796 }
797 case Qt::Vertical:
798 {
799 //mouse is creating a vertical guide
800 guide.reset( new QgsLayoutGuide( Qt::Vertical, QgsLayoutMeasurement( scenePos.x(), layout->units() ), page ) );
801 break;
802 }
803 }
804 mView->currentLayout()->guides().addGuide( guide.release() );
805 }
806 }
807 else if ( event->button() == Qt::RightButton )
808 {
809 if ( mCreatingGuide || mDraggingGuide )
810 {
811 QApplication::restoreOverrideCursor();
812 delete mGuideItem;
813 mGuideItem = nullptr;
814 mCreatingGuide = false;
815 if ( mDraggingGuide )
816 {
817 mDraggingGuide->setLayoutPosition( mDraggingGuideOldPosition );
818 }
819 mDraggingGuide = nullptr;
820 }
821 if ( mMenu )
822 mMenu->popup( event->globalPos() );
823 }
824}
void addGuide(QgsLayoutGuide *guide)
Adds a guide to the collection.
void removeGuide(QgsLayoutGuide *guide)
Removes the specified guide, and deletes it.
void setGuideLayoutPosition(QgsLayoutGuide *guide, double position)
Sets the absolute position (in layout coordinates) for guide within the layout.
QList< QgsLayoutGuide * > guides()
Returns a list of all guides contained in the collection.
Contains the configuration for a single snap guide used by a layout.
QgsLayoutItemPage * page()
Returns the page the guide is contained within.
Qt::Orientation orientation() const
Returns the guide's orientation.
void setLayoutPosition(double position)
Sets the guide's position in absolute layout units.
double layoutPosition() const
Returns the guide's position in absolute layout units.
Item representing the paper in a layout.
This class provides a method of storing measurements for use in QGIS layouts using a variety of diffe...
int pageNumberForPoint(QPointF point) const
Returns the page number corresponding to a point in the layout (in layout units).
double spaceBetweenPages() const
Returns the space between pages, in layout units.
int pageCount() const
Returns the number of pages in the collection.
QPointF positionOnPage(QPointF point) const
Returns the position within a page of a point in the layout (in layout units).
QgsLayoutItemPage * page(int pageNumber)
Returns a specific page (by pageNumber) from the collection.
QgsLayoutItemPage * pageAtPoint(QPointF point) const
Returns the page at a specified point (in layout coordinates).
void cursorPosChanged(QPointF)
Emitted when mouse cursor coordinates change.
void paintEvent(QPaintEvent *event) override
void setContextMenu(QMenu *menu)
Sets a context menu to show when right clicking occurs on the ruler.
void setCursorPosition(QPointF position)
Updates the position of the marker showing the current mouse position within the view.
void setSceneTransform(const QTransform &transform)
Sets the current scene transform.
void mouseReleaseEvent(QMouseEvent *event) override
void mouseMoveEvent(QMouseEvent *event) override
QgsLayoutRuler(QWidget *parent=nullptr, Qt::Orientation orientation=Qt::Horizontal)
Constructor for QgsLayoutRuler, with the specified parent widget and orientation.
void setLayoutView(QgsLayoutView *view)
Sets the current layout view to synchronize the ruler with.
void mousePressEvent(QMouseEvent *event) override
QSize minimumSizeHint() const override
A graphical widget to display and interact with QgsLayouts.
Definition: qgslayoutview.h:50
void cursorPosChanged(QPointF layoutPoint)
Emitted when the mouse cursor coordinates change within the view.
QgsLayout * currentLayout
Definition: qgslayoutview.h:63
QList< QgsLayoutItemPage * > visiblePages() const
Returns a list of page items which are currently visible in the view.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:51
QgsLayoutPageCollection * pageCollection()
Returns a pointer to the layout's page collection, which stores and manages page items in the layout.
Definition: qgslayout.cpp:459
QgsLayoutGuideCollection & guides()
Returns a reference to the layout's guide collection, which manages page snap guides.
Definition: qgslayout.cpp:385
QgsUnitTypes::LayoutUnit units() const
Returns the native units for the layout.
Definition: qgslayout.h:329
@ ZGuide
Z-value for page guides.
Definition: qgslayout.h:62
Scoped object for saving and restoring a QPainter object's state.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2404
const unsigned int COUNT_VALID_MULTIPLES
const unsigned int COUNT_VALID_MAGNITUDES
const int RULER_FONT_SIZE