QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
60  double guideMarkerSize = mRulerFontMetrics->width( QStringLiteral( "*" ) );
61 #else
62  double guideMarkerSize = mRulerFontMetrics->horizontalAdvance( '*' );
63 #endif
64  mDragGuideTolerance = guideMarkerSize;
65  switch ( mOrientation )
66  {
67  case Qt::Horizontal:
68  mGuideMarker << QPoint( -guideMarkerSize / 2, mRulerMinSize - guideMarkerSize ) << QPoint( 0, mRulerMinSize ) <<
69  QPoint( guideMarkerSize / 2, mRulerMinSize - guideMarkerSize );
70  break;
71 
72  case Qt::Vertical:
73  mGuideMarker << QPoint( mRulerMinSize - guideMarkerSize, -guideMarkerSize / 2 ) << QPoint( mRulerMinSize, 0 ) <<
74  QPoint( mRulerMinSize - guideMarkerSize, guideMarkerSize / 2 );
75  break;
76  }
77 }
78 
80 {
81  return QSize( mRulerMinSize, mRulerMinSize );
82 }
83 
84 void QgsLayoutRuler::paintEvent( QPaintEvent *event )
85 {
86  Q_UNUSED( event )
87  if ( !mView || !mView->currentLayout() )
88  {
89  return;
90  }
91 
92  QgsLayout *layout = mView->currentLayout();
93  QPainter p( this );
94 
95  drawGuideMarkers( &p, layout );
96 
97  QTransform t = mTransform.inverted();
98  p.setFont( mRulerFont );
99  // keep same default color, but lower opacity a tad
100  QBrush brush = p.brush();
101  QColor color = brush.color();
102  color.setAlphaF( 0.7 );
103  brush.setColor( color );
104  p.setBrush( brush );
105  QPen pen = p.pen();
106  color = pen.color();
107  color.setAlphaF( 0.7 );
108  pen.setColor( color );
109  p.setPen( pen );
110 
111  //find optimum scale for ruler (size of numbered divisions)
112  int magnitude = 1;
113  int multiple = 1;
114  int mmDisplay = optimumScale( mScaleMinPixelsWidth, magnitude, multiple );
115 
116  //find optimum number of small divisions
117  int numSmallDivisions = optimumNumberDivisions( mmDisplay, multiple );
118 
119  switch ( mOrientation )
120  {
121  case Qt::Horizontal:
122  {
123  if ( qgsDoubleNear( width(), 0 ) )
124  {
125  return;
126  }
127 
128  //start x-coordinate
129  double startX = t.map( QPointF( 0, 0 ) ).x();
130  double endX = t.map( QPointF( width(), 0 ) ).x();
131 
132  //start marker position in mm
133  double markerPos = ( std::floor( startX / mmDisplay ) + 1 ) * mmDisplay;
134 
135  //draw minor ticks marks which occur before first major tick
136  drawSmallDivisions( &p, markerPos, numSmallDivisions, -mmDisplay );
137 
138  while ( markerPos <= endX )
139  {
140  double pixelCoord = mTransform.map( QPointF( markerPos, 0 ) ).x();
141 
142  //draw large division and text
143  p.drawLine( pixelCoord, 0, pixelCoord, mRulerMinSize );
144  p.drawText( QPointF( pixelCoord + mPixelsBetweenLineAndText, mTextBaseline ), QString::number( markerPos ) );
145 
146  //draw small divisions
147  drawSmallDivisions( &p, markerPos, numSmallDivisions, mmDisplay, endX );
148 
149  markerPos += mmDisplay;
150  }
151  break;
152  }
153  case Qt::Vertical:
154  {
155  if ( qgsDoubleNear( height(), 0 ) )
156  {
157  return;
158  }
159 
160  double startY = t.map( QPointF( 0, 0 ) ).y(); //start position in mm (total including space between pages)
161  double endY = t.map( QPointF( 0, height() ) ).y(); //stop position in mm (total including space between pages)
162 
163  // work out start page
164  int startPage = 0;
165  int endPage = 0;
166  double currentY = 0;
167  double currentPageY = 0;
168  for ( int page = 0; page < layout->pageCollection()->pageCount(); ++page )
169  {
170  if ( currentY < startY )
171  {
172  startPage = page;
173  currentPageY = currentY;
174  }
175  endPage = page;
176 
177  currentY += layout->pageCollection()->page( startPage )->rect().height() + layout->pageCollection()->spaceBetweenPages();
178  if ( currentY > endY )
179  break;
180  }
181 
182  if ( startY < 0 )
183  {
184  double beforePageCoord = -mmDisplay;
185  double firstPageY = mTransform.map( QPointF( 0, 0 ) ).y();
186 
187  //draw negative rulers which fall before first page
188  while ( beforePageCoord > startY )
189  {
190  double pixelCoord = mTransform.map( QPointF( 0, beforePageCoord ) ).y();
191  p.drawLine( 0, pixelCoord, mRulerMinSize, pixelCoord );
192  //calc size of label
193  QString label = QString::number( beforePageCoord );
194  int labelSize = mRulerFontMetrics->boundingRect( label ).width();
195 
196  //draw label only if it fits in before start of next page
197  if ( pixelCoord + labelSize + 8 < firstPageY )
198  {
199  drawRotatedText( &p, QPointF( mTextBaseline, pixelCoord + mMinSpacingVerticalLabels + labelSize ), label );
200  }
201 
202  //draw small divisions
203  drawSmallDivisions( &p, beforePageCoord, numSmallDivisions, mmDisplay );
204 
205  beforePageCoord -= mmDisplay;
206  }
207 
208  //draw minor ticks marks which occur before first major tick
209  drawSmallDivisions( &p, beforePageCoord + mmDisplay, numSmallDivisions, -mmDisplay, startY );
210  }
211 
212  double nextPageStartPos = 0;
213  int nextPageStartPixel = 0;
214 
215  for ( int i = startPage; i <= endPage; ++i )
216  {
217  double pageCoord = 0; //page coordinate in mm
218  //total (composition) coordinate in mm, including space between pages
219 
220  double totalCoord = currentPageY;
221 
222  //position of next page
223  if ( i < endPage )
224  {
225  //not the last page
226  nextPageStartPos = currentPageY + layout->pageCollection()->page( i )->rect().height() + layout->pageCollection()->spaceBetweenPages();
227  nextPageStartPixel = mTransform.map( QPointF( 0, nextPageStartPos ) ).y();
228  }
229  else
230  {
231  //is the last page
232  nextPageStartPos = 0;
233  nextPageStartPixel = 0;
234  }
235 
236  while ( ( totalCoord < nextPageStartPos ) || ( ( nextPageStartPos == 0 ) && ( totalCoord <= endY ) ) )
237  {
238  double pixelCoord = mTransform.map( QPointF( 0, totalCoord ) ).y();
239  p.drawLine( 0, pixelCoord, mRulerMinSize, pixelCoord );
240  //calc size of label
241  QString label = QString::number( pageCoord );
242  int labelSize = mRulerFontMetrics->boundingRect( label ).width();
243 
244  //draw label only if it fits in before start of next page
245  if ( ( pixelCoord + labelSize + 8 < nextPageStartPixel )
246  || ( nextPageStartPixel == 0 ) )
247  {
248  drawRotatedText( &p, QPointF( mTextBaseline, pixelCoord + mMinSpacingVerticalLabels + labelSize ), label );
249  }
250 
251  //draw small divisions
252  drawSmallDivisions( &p, totalCoord, numSmallDivisions, mmDisplay, nextPageStartPos );
253 
254  pageCoord += mmDisplay;
255  totalCoord += mmDisplay;
256  }
257  if ( i < endPage )
258  currentPageY += layout->pageCollection()->page( i )->rect().height() + layout->pageCollection()->spaceBetweenPages();
259  }
260  break;
261  }
262  }
263 
264  //draw current marker pos
265  drawMarkerPos( &p );
266 }
267 
268 void QgsLayoutRuler::drawMarkerPos( QPainter *painter )
269 {
270  //draw current marker pos in red
271  painter->setPen( QColor( Qt::red ) );
272  switch ( mOrientation )
273  {
274  case Qt::Horizontal:
275  {
276  painter->drawLine( mMarkerPos.x(), 0, mMarkerPos.x(), mRulerMinSize );
277  break;
278  }
279  case Qt::Vertical:
280  {
281  painter->drawLine( 0, mMarkerPos.y(), mRulerMinSize, mMarkerPos.y() );
282  break;
283  }
284  }
285 }
286 
287 void QgsLayoutRuler::drawGuideMarkers( QPainter *p, QgsLayout *layout )
288 {
289  QList< QgsLayoutItemPage * > visiblePages = mView->visiblePages();
290  QList< QgsLayoutGuide * > guides = layout->guides().guides( mOrientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal );
291  QgsScopedQPainterState painterState( p );
292  p->setRenderHint( QPainter::Antialiasing, true );
293  p->setPen( Qt::NoPen );
294  const auto constGuides = guides;
295  for ( QgsLayoutGuide *guide : constGuides )
296  {
297  if ( visiblePages.contains( guide->page() ) )
298  {
299  if ( guide == mHoverGuide )
300  {
301  p->setBrush( QBrush( QColor( 255, 0, 0, 225 ) ) );
302  }
303  else
304  {
305  p->setBrush( QBrush( QColor( 255, 0, 0, 150 ) ) );
306  }
307  QPointF point;
308  switch ( mOrientation )
309  {
310  case Qt::Horizontal:
311  point = QPointF( guide->layoutPosition(), 0 );
312  break;
313 
314  case Qt::Vertical:
315  point = QPointF( 0, guide->layoutPosition() );
316  break;
317  }
318  drawGuideAtPos( p, convertLayoutPointToLocal( point ) );
319  }
320  }
321 }
322 
323 void QgsLayoutRuler::drawGuideAtPos( QPainter *painter, QPoint pos )
324 {
325  switch ( mOrientation )
326  {
327  case Qt::Horizontal:
328  {
329  painter->translate( pos.x(), 0 );
330  painter->drawPolygon( mGuideMarker );
331  painter->translate( -pos.x(), 0 );
332  break;
333  }
334  case Qt::Vertical:
335  {
336  painter->translate( 0, pos.y() );
337  painter->drawPolygon( mGuideMarker );
338  painter->translate( 0, -pos.y() );
339  break;
340  }
341  }
342 }
343 
344 void QgsLayoutRuler::createTemporaryGuideItem()
345 {
346  if ( !mView->currentLayout() )
347  return;
348 
349  delete mGuideItem;
350  mGuideItem = new QGraphicsLineItem();
351 
352  mGuideItem->setZValue( QgsLayout::ZGuide );
353  QPen linePen( Qt::DotLine );
354  linePen.setColor( QColor( 255, 0, 0, 150 ) );
355  linePen.setWidthF( 0 );
356  mGuideItem->setPen( linePen );
357 
358  mView->currentLayout()->addItem( mGuideItem );
359 }
360 
361 QPointF QgsLayoutRuler::convertLocalPointToLayout( QPoint localPoint ) const
362 {
363  QPoint viewPoint = mView->mapFromGlobal( mapToGlobal( localPoint ) );
364  return mView->mapToScene( viewPoint );
365 }
366 
367 QPoint QgsLayoutRuler::convertLayoutPointToLocal( QPointF layoutPoint ) const
368 {
369  QPoint viewPoint = mView->mapFromScene( layoutPoint );
370  return mapFromGlobal( mView->mapToGlobal( viewPoint ) );
371 }
372 
373 QgsLayoutGuide *QgsLayoutRuler::guideAtPoint( QPoint localPoint ) const
374 {
375  if ( !mView->currentLayout() )
376  return nullptr;
377 
378  QPointF layoutPoint = convertLocalPointToLayout( localPoint );
379  QList< QgsLayoutItemPage * > visiblePages = mView->visiblePages();
380  QList< QgsLayoutGuide * > guides = mView->currentLayout()->guides().guides( mOrientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal );
381  QgsLayoutGuide *closestGuide = nullptr;
382  double minDelta = std::numeric_limits<double>::max();
383  const auto constGuides = guides;
384  for ( QgsLayoutGuide *guide : constGuides )
385  {
386  if ( visiblePages.contains( guide->page() ) )
387  {
388  double currentDelta = 0;
389  switch ( mOrientation )
390  {
391  case Qt::Horizontal:
392  currentDelta = std::fabs( layoutPoint.x() - guide->layoutPosition() );
393  break;
394 
395  case Qt::Vertical:
396  currentDelta = std::fabs( layoutPoint.y() - guide->layoutPosition() );
397  break;
398  }
399  if ( currentDelta < minDelta )
400  {
401  minDelta = currentDelta;
402  closestGuide = guide;
403  }
404  }
405  }
406 
407  if ( minDelta * mView->transform().m11() <= mDragGuideTolerance )
408  {
409  return closestGuide;
410  }
411  else
412  {
413  return nullptr;
414  }
415 }
416 
417 void QgsLayoutRuler::drawRotatedText( QPainter *painter, QPointF pos, const QString &text )
418 {
419  QgsScopedQPainterState painterState( painter );
420  painter->translate( pos.x(), pos.y() );
421  painter->rotate( 270 );
422  painter->drawText( 0, 0, text );
423 }
424 
425 void QgsLayoutRuler::drawSmallDivisions( QPainter *painter, double startPos, int numDivisions, double rulerScale, double maxPos )
426 {
427  if ( numDivisions == 0 )
428  return;
429 
430  //draw small divisions starting at startPos (in mm)
431  double smallMarkerPos = startPos;
432  double smallDivisionSpacing = rulerScale / numDivisions;
433 
434  double pixelCoord = 0.0;
435 
436  //draw numDivisions small divisions
437  for ( int i = 0; i < numDivisions; ++i )
438  {
439  smallMarkerPos += smallDivisionSpacing;
440 
441  if ( maxPos > 0 && smallMarkerPos > maxPos )
442  {
443  //stop drawing current division position is past maxPos
444  return;
445  }
446 
447  //calculate pixelCoordinate of the current division
448  switch ( mOrientation )
449  {
450  case Qt::Horizontal:
451  {
452  pixelCoord = mTransform.map( QPointF( smallMarkerPos, 0 ) ).x();
453  break;
454  }
455  case Qt::Vertical:
456  {
457  pixelCoord = mTransform.map( QPointF( 0, smallMarkerPos ) ).y();
458  break;
459  }
460  }
461 
462  //calculate height of small division line
463  double lineSize;
464  if ( ( numDivisions == 10 && i == 4 ) || ( numDivisions == 4 && i == 1 ) )
465  {
466  //if drawing the 5th line of 10 or drawing the 2nd line of 4, then draw it slightly longer
467  lineSize = mRulerMinSize / 1.5;
468  }
469  else
470  {
471  lineSize = mRulerMinSize / 1.25;
472  }
473 
474  //draw either horizontal or vertical line depending on ruler direction
475  switch ( mOrientation )
476  {
477  case Qt::Horizontal:
478  {
479  painter->drawLine( pixelCoord, lineSize, pixelCoord, mRulerMinSize );
480  break;
481  }
482  case Qt::Vertical:
483  {
484  painter->drawLine( lineSize, pixelCoord, mRulerMinSize, pixelCoord );
485  break;
486  }
487  }
488  }
489 }
490 
491 int QgsLayoutRuler::optimumScale( double minPixelDiff, int &magnitude, int &multiple )
492 {
493  //find optimal ruler display scale
494 
495  //loop through magnitudes and multiples to find optimum scale
496  for ( unsigned int magnitudeCandidate = 0; magnitudeCandidate < COUNT_VALID_MAGNITUDES; ++magnitudeCandidate )
497  {
498  for ( unsigned int multipleCandidate = 0; multipleCandidate < COUNT_VALID_MULTIPLES; ++multipleCandidate )
499  {
500  int candidateScale = VALID_SCALE_MULTIPLES[multipleCandidate] * VALID_SCALE_MAGNITUDES[magnitudeCandidate];
501  //find pixel size for each step using this candidate scale
502  double pixelDiff = mTransform.map( QPointF( candidateScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
503  if ( pixelDiff > minPixelDiff )
504  {
505  //found the optimum major scale
506  magnitude = VALID_SCALE_MAGNITUDES[magnitudeCandidate];
507  multiple = VALID_SCALE_MULTIPLES[multipleCandidate];
508  return candidateScale;
509  }
510  }
511  }
512 
513  return 100000;
514 }
515 
516 int QgsLayoutRuler::optimumNumberDivisions( double rulerScale, int scaleMultiple )
517 {
518  //calculate size in pixels of each marked ruler unit
519  double largeDivisionSize = mTransform.map( QPointF( rulerScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
520 
521  //now calculate optimum small tick scale, depending on marked ruler units
522  QList<int> validSmallDivisions;
523  switch ( scaleMultiple )
524  {
525  case 1:
526  //numbers increase by 1 increment each time, e.g., 1, 2, 3 or 10, 20, 30
527  //so we can draw either 10, 5 or 2 small ticks and have each fall on a nice value
528  validSmallDivisions << 10 << 5 << 2;
529  break;
530  case 2:
531  //numbers increase by 2 increments each time, e.g., 2, 4, 6 or 20, 40, 60
532  //so we can draw either 10, 4 or 2 small ticks and have each fall on a nice value
533  validSmallDivisions << 10 << 4 << 2;
534  break;
535  case 5:
536  //numbers increase by 5 increments each time, e.g., 5, 10, 15 or 100, 500, 1000
537  //so we can draw either 10 or 5 small ticks and have each fall on a nice value
538  validSmallDivisions << 10 << 5;
539  break;
540  }
541 
542  //calculate the most number of small divisions we can draw without them being too close to each other
543  QList<int>::iterator divisions_it;
544  for ( divisions_it = validSmallDivisions.begin(); divisions_it != validSmallDivisions.end(); ++divisions_it )
545  {
546  //find pixel size for this small division
547  double candidateSize = largeDivisionSize / ( *divisions_it );
548  //check if this separation is more then allowed min separation
549  if ( candidateSize >= mMinPixelsPerDivision )
550  {
551  //found a good candidate, return it
552  return ( *divisions_it );
553  }
554  }
555 
556  //unable to find a good candidate
557  return 0;
558 }
559 
560 
561 void QgsLayoutRuler::setSceneTransform( const QTransform &transform )
562 {
563  mTransform = transform;
564  update();
565 }
566 
568 {
569  mView = view;
571 }
572 
574 {
575  mMenu = menu;
576 }
577 
578 void QgsLayoutRuler::setCursorPosition( QPointF position )
579 {
580  mMarkerPos = mView->mapFromScene( position );
581  update();
582 }
583 
584 void QgsLayoutRuler::mouseMoveEvent( QMouseEvent *event )
585 {
586  mMarkerPos = event->pos();
587  update();
588 
589  if ( !mView->currentLayout() )
590  return;
591 
592  QPointF displayPos;
593  if ( mCreatingGuide || mDraggingGuide )
594  {
595  // event -> layout coordinates
596  displayPos = convertLocalPointToLayout( event->pos() );
597 
598  if ( mCreatingGuide )
599  {
600  QgsLayout *layout = mView->currentLayout();
601  int pageNo = layout->pageCollection()->pageNumberForPoint( displayPos );
602  QgsLayoutItemPage *page = layout->pageCollection()->page( pageNo );
603  if ( !page )
604  return;
605 
606  QPen linePen = mGuideItem->pen();
607  // if guide preview is outside a page draw it a lot fainter, to indicate it's invalid
608  if ( !layout->pageCollection()->pageAtPoint( displayPos ) )
609  {
610  linePen.setColor( QColor( 255, 0, 0, 150 ) );
611  }
612  else
613  {
614  linePen.setColor( QColor( 255, 0, 0, 225 ) );
615  }
616  mGuideItem->setPen( linePen );
617  switch ( mOrientation )
618  {
619  case Qt::Horizontal:
620  {
621  //mouse is creating a horizontal ruler, so don't show x coordinate
622  mGuideItem->setLine( page->scenePos().x(), displayPos.y(), page->scenePos().x() + page->rect().width(), displayPos.y() );
623  displayPos.setX( 0 );
624  break;
625  }
626  case Qt::Vertical:
627  {
628  //mouse is creating a vertical ruler, so don't show a y coordinate
629  mGuideItem->setLine( displayPos.x(), page->scenePos().y(), displayPos.x(), page->scenePos().y() + page->rect().height() );
630  displayPos.setY( 0 );
631  break;
632  }
633  }
634  }
635  else
636  {
637  // dragging guide
638  switch ( mOrientation )
639  {
640  case Qt::Horizontal:
641  {
642  mView->currentLayout()->guides().setGuideLayoutPosition( mDraggingGuide, displayPos.x() );
643  displayPos.setY( 0 );
644  break;
645  }
646  case Qt::Vertical:
647  {
648  mView->currentLayout()->guides().setGuideLayoutPosition( mDraggingGuide, displayPos.y() );
649  displayPos.setX( 0 );
650  break;
651  }
652  }
653  }
654  }
655  else
656  {
657  // is cursor over a guide marker?
658  mHoverGuide = guideAtPoint( event->pos() );
659  if ( mHoverGuide )
660  {
661  setCursor( mOrientation == Qt::Vertical ? Qt::SplitVCursor : Qt::SplitHCursor );
662  }
663  else
664  {
665  setCursor( Qt::ArrowCursor );
666  }
667 
668  //update cursor position in status bar
669  displayPos = mTransform.inverted().map( event->pos() );
670  switch ( mOrientation )
671  {
672  case Qt::Horizontal:
673  {
674  //mouse is over a horizontal ruler, so don't show a y coordinate
675  displayPos.setY( 0 );
676  break;
677  }
678  case Qt::Vertical:
679  {
680  //mouse is over a vertical ruler, so don't show an x coordinate
681  displayPos.setX( 0 );
682  break;
683  }
684  }
685  }
686  emit cursorPosChanged( displayPos );
687 }
688 
689 void QgsLayoutRuler::mousePressEvent( QMouseEvent *event )
690 {
691  if ( !mView->currentLayout() )
692  return;
693 
694  if ( event->button() == Qt::LeftButton )
695  {
696  mDraggingGuide = guideAtPoint( event->pos() );
697  if ( !mDraggingGuide )
698  {
699  // if no guide at the point, then we're creating one
700  if ( mView->currentLayout()->pageCollection()->pageCount() > 0 )
701  {
702  mCreatingGuide = true;
703  createTemporaryGuideItem();
704  }
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 
720 void 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  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  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  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 ( mMenu )
810  mMenu->popup( event->globalPos() );
811  }
812 }
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:50
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:328
@ ZGuide
Z-value for page guides.
Definition: qgslayout.h:61
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:316
const unsigned int COUNT_VALID_MULTIPLES
const unsigned int COUNT_VALID_MAGNITUDES
const int RULER_FONT_SIZE