QGIS API Documentation  3.23.0-Master (7c4a6de034)
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 
27 const int RULER_FONT_SIZE = 8;
28 const unsigned int COUNT_VALID_MULTIPLES = 3;
29 const unsigned int COUNT_VALID_MAGNITUDES = 5;
30 const int QgsLayoutRuler::VALID_SCALE_MULTIPLES[] = {1, 2, 5};
31 const int QgsLayoutRuler::VALID_SCALE_MAGNITUDES[] = {1, 10, 100, 1000, 10000};
32 
33 QgsLayoutRuler::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 
80 void 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 
264 void 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 
283 void 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 
319 void 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 
340 void 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 
357 QPointF QgsLayoutRuler::convertLocalPointToLayout( QPoint localPoint ) const
358 {
359  const QPoint viewPoint = mView->mapFromGlobal( mapToGlobal( localPoint ) );
360  return mView->mapToScene( viewPoint );
361 }
362 
363 QPoint QgsLayoutRuler::convertLayoutPointToLocal( QPointF layoutPoint ) const
364 {
365  const QPoint viewPoint = mView->mapFromScene( layoutPoint );
366  return mapFromGlobal( mView->mapToGlobal( viewPoint ) );
367 }
368 
369 QgsLayoutGuide *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 
413 void 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 
421 void 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 
487 int 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 
512 int 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 
557 void 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 
574 void QgsLayoutRuler::setCursorPosition( QPointF position )
575 {
576  mMarkerPos = mView->mapFromScene( position );
577  update();
578 }
579 
580 void 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 
685 void 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  switch ( mOrientation )
703  {
704  case Qt::Horizontal:
705  {
706  QApplication::setOverrideCursor( mDraggingGuide ? Qt::SplitHCursor : Qt::SplitVCursor );
707  break;
708  }
709  case Qt::Vertical:
710  QApplication::setOverrideCursor( mDraggingGuide ? Qt::SplitVCursor : Qt::SplitHCursor );
711  break;
712  }
713  }
714 }
715 
716 void QgsLayoutRuler::mouseReleaseEvent( QMouseEvent *event )
717 {
718  if ( !mView->currentLayout() )
719  return;
720 
721  if ( event->button() == Qt::LeftButton )
722  {
723  if ( mDraggingGuide )
724  {
725  QApplication::restoreOverrideCursor();
726 
727  const QPointF layoutPoint = convertLocalPointToLayout( event->pos() );
728 
729  // delete guide if it ends outside of page
730  QgsLayoutItemPage *page = mDraggingGuide->page();
731  bool deleteGuide = false;
732  switch ( mDraggingGuide->orientation() )
733  {
734  case Qt::Horizontal:
735  if ( layoutPoint.y() < page->scenePos().y() || layoutPoint.y() > page->scenePos().y() + page->rect().height() )
736  deleteGuide = true;
737  break;
738 
739  case Qt::Vertical:
740  if ( layoutPoint.x() < page->scenePos().x() || layoutPoint.x() > page->scenePos().x() + page->rect().width() )
741  deleteGuide = true;
742  break;
743  }
744 
745  if ( deleteGuide )
746  {
747  mView->currentLayout()->guides().removeGuide( mDraggingGuide );
748  }
749  mDraggingGuide = nullptr;
750  }
751  else
752  {
753  mCreatingGuide = false;
754  QApplication::restoreOverrideCursor();
755  delete mGuideItem;
756  mGuideItem = nullptr;
757 
758  // check that cursor left the ruler
759  switch ( mOrientation )
760  {
761  case Qt::Horizontal:
762  {
763  if ( event->pos().y() <= height() )
764  return;
765  break;
766  }
767  case Qt::Vertical:
768  {
769  if ( event->pos().x() <= width() )
770  return;
771  break;
772  }
773  }
774 
775  QgsLayout *layout = mView->currentLayout();
776 
777  // create guide
778  const QPointF scenePos = convertLocalPointToLayout( event->pos() );
779  QgsLayoutItemPage *page = layout->pageCollection()->pageAtPoint( scenePos );
780  if ( !page )
781  return; // dragged outside of a page
782 
783  std::unique_ptr< QgsLayoutGuide > guide;
784  switch ( mOrientation )
785  {
786  case Qt::Horizontal:
787  {
788  //mouse is creating a horizontal guide
789  const double posOnPage = layout->pageCollection()->positionOnPage( scenePos ).y();
790  guide.reset( new QgsLayoutGuide( Qt::Horizontal, QgsLayoutMeasurement( posOnPage, layout->units() ), page ) );
791  break;
792  }
793  case Qt::Vertical:
794  {
795  //mouse is creating a vertical guide
796  guide.reset( new QgsLayoutGuide( Qt::Vertical, QgsLayoutMeasurement( scenePos.x(), layout->units() ), page ) );
797  break;
798  }
799  }
800  mView->currentLayout()->guides().addGuide( guide.release() );
801  }
802  }
803  else if ( event->button() == Qt::RightButton )
804  {
805  if ( mMenu )
806  mMenu->popup( event->globalPos() );
807  }
808 }
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.
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:1546
const unsigned int COUNT_VALID_MULTIPLES
const unsigned int COUNT_VALID_MAGNITUDES
const int RULER_FONT_SIZE