QGIS API Documentation  3.8.0-Zanzibar (11aff65)
qgslayoutmousehandles.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslayoutmousehandles.cpp
3  ------------------------
4  begin : September 2017
5  copyright : (C) 2017 by Nyall Dawson
6  email : [email protected]
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #include "qgslayoutmousehandles.h"
19 #include "qgis.h"
20 #include "qgslogger.h"
21 #include "qgsproject.h"
22 #include "qgslayout.h"
23 #include "qgslayoutitem.h"
24 #include "qgslayoututils.h"
25 #include "qgslayoutview.h"
27 #include "qgslayoutsnapper.h"
28 #include "qgslayoutitemgroup.h"
29 #include "qgslayoutundostack.h"
30 #include <QGraphicsView>
31 #include <QGraphicsSceneHoverEvent>
32 #include <QPainter>
33 #include <QWidget>
34 #include <limits>
35 
37 
38 QgsLayoutMouseHandles::QgsLayoutMouseHandles( QgsLayout *layout, QgsLayoutView *view )
39  : QObject( nullptr )
40  , QGraphicsRectItem( nullptr )
41  , mLayout( layout )
42  , mView( view )
43 {
44  //listen for selection changes, and update handles accordingly
45  connect( mLayout, &QGraphicsScene::selectionChanged, this, &QgsLayoutMouseHandles::selectionChanged );
46 
47  //accept hover events, required for changing cursor to resize cursors
48  setAcceptHoverEvents( true );
49 
50  mHorizontalSnapLine = mView->createSnapLine();
51  mHorizontalSnapLine->hide();
52  layout->addItem( mHorizontalSnapLine );
53  mVerticalSnapLine = mView->createSnapLine();
54  mVerticalSnapLine->hide();
55  layout->addItem( mVerticalSnapLine );
56 }
57 
58 void QgsLayoutMouseHandles::paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget )
59 {
60  Q_UNUSED( option )
61  Q_UNUSED( widget )
62 
63  if ( !mLayout->renderContext().isPreviewRender() )
64  {
65  //don't draw selection handles in layout outputs
66  return;
67  }
68 
69  if ( mLayout->renderContext().boundingBoxesVisible() )
70  {
71  //draw resize handles around bounds of entire selection
72  double rectHandlerSize = rectHandlerBorderTolerance();
73  drawHandles( painter, rectHandlerSize );
74  }
75 
76  if ( mIsResizing || mIsDragging || mLayout->renderContext().boundingBoxesVisible() )
77  {
78  //draw dotted boxes around selected items
79  drawSelectedItemBounds( painter );
80  }
81 }
82 
83 void QgsLayoutMouseHandles::drawHandles( QPainter *painter, double rectHandlerSize )
84 {
85  //blue, zero width cosmetic pen for outline
86  QPen handlePen = QPen( QColor( 55, 140, 195, 255 ) );
87  handlePen.setWidth( 0 );
88  painter->setPen( handlePen );
89 
90  //draw box around entire selection bounds
91  painter->setBrush( Qt::NoBrush );
92  painter->drawRect( QRectF( 0, 0, rect().width(), rect().height() ) );
93 
94  //draw resize handles, using a filled white box
95  painter->setBrush( QColor( 255, 255, 255, 255 ) );
96  //top left
97  painter->drawRect( QRectF( 0, 0, rectHandlerSize, rectHandlerSize ) );
98  //mid top
99  painter->drawRect( QRectF( ( rect().width() - rectHandlerSize ) / 2, 0, rectHandlerSize, rectHandlerSize ) );
100  //top right
101  painter->drawRect( QRectF( rect().width() - rectHandlerSize, 0, rectHandlerSize, rectHandlerSize ) );
102  //mid left
103  painter->drawRect( QRectF( 0, ( rect().height() - rectHandlerSize ) / 2, rectHandlerSize, rectHandlerSize ) );
104  //mid right
105  painter->drawRect( QRectF( rect().width() - rectHandlerSize, ( rect().height() - rectHandlerSize ) / 2, rectHandlerSize, rectHandlerSize ) );
106  //bottom left
107  painter->drawRect( QRectF( 0, rect().height() - rectHandlerSize, rectHandlerSize, rectHandlerSize ) );
108  //mid bottom
109  painter->drawRect( QRectF( ( rect().width() - rectHandlerSize ) / 2, rect().height() - rectHandlerSize, rectHandlerSize, rectHandlerSize ) );
110  //bottom right
111  painter->drawRect( QRectF( rect().width() - rectHandlerSize, rect().height() - rectHandlerSize, rectHandlerSize, rectHandlerSize ) );
112 }
113 
114 void QgsLayoutMouseHandles::drawSelectedItemBounds( QPainter *painter )
115 {
116  //draw dotted border around selected items to give visual feedback which items are selected
117  const QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
118  if ( selectedItems.isEmpty() )
119  {
120  return;
121  }
122 
123  //use difference mode so that they are visible regardless of item colors
124  painter->save();
125  painter->setCompositionMode( QPainter::CompositionMode_Difference );
126 
127  // use a grey dashed pen - in difference mode this should always be visible
128  QPen selectedItemPen = QPen( QColor( 144, 144, 144, 255 ) );
129  selectedItemPen.setStyle( Qt::DashLine );
130  selectedItemPen.setWidth( 0 );
131  painter->setPen( selectedItemPen );
132  painter->setBrush( Qt::NoBrush );
133 
134  QList< QgsLayoutItem * > itemsToDraw;
135  collectItems( selectedItems, itemsToDraw );
136 
137  for ( QgsLayoutItem *item : qgis::as_const( itemsToDraw ) )
138  {
139  //get bounds of selected item
140  QPolygonF itemBounds;
141  if ( mIsDragging && !item->isLocked() )
142  {
143  //if currently dragging, draw selected item bounds relative to current mouse position
144  //first, get bounds of current item in scene coordinates
145  QPolygonF itemSceneBounds = item->mapToScene( item->rectWithFrame() );
146  //now, translate it by the current movement amount
147  //IMPORTANT - this is done in scene coordinates, since we don't want any rotation/non-translation transforms to affect the movement
148  itemSceneBounds.translate( transform().dx(), transform().dy() );
149  //finally, remap it to the mouse handle item's coordinate system so it's ready for drawing
150  itemBounds = mapFromScene( itemSceneBounds );
151  }
152  else if ( mIsResizing && !item->isLocked() )
153  {
154  //if currently resizing, calculate relative resize of this item
155  if ( selectedItems.size() > 1 )
156  {
157  //get item bounds in mouse handle item's coordinate system
158  QRectF itemRect = mapRectFromItem( item, item->rectWithFrame() );
159  //now, resize it relative to the current resized dimensions of the mouse handles
160  QgsLayoutUtils::relativeResizeRect( itemRect, QRectF( -mResizeMoveX, -mResizeMoveY, mBeginHandleWidth, mBeginHandleHeight ), mResizeRect );
161  itemBounds = QPolygonF( itemRect );
162  }
163  else
164  {
165  //single item selected
166  itemBounds = rect();
167  }
168  }
169  else
170  {
171  //not resizing or moving, so just map from scene bounds
172  itemBounds = mapRectFromItem( item, item->rectWithFrame() );
173  }
174 
175  // drawPolygon causes issues on windows - corners of path may be missing resulting in triangles being drawn
176  // instead of rectangles! (Same cause as #13343)
177  QPainterPath path;
178  path.addPolygon( itemBounds );
179  painter->drawPath( path );
180  }
181  painter->restore();
182 }
183 
184 void QgsLayoutMouseHandles::selectionChanged()
185 {
186  //listen out for selected items' size and rotation changed signals
187  const QList<QGraphicsItem *> itemList = layout()->items();
188  for ( QGraphicsItem *graphicsItem : itemList )
189  {
190  QgsLayoutItem *item = dynamic_cast<QgsLayoutItem *>( graphicsItem );
191  if ( !item )
192  continue;
193 
194  if ( item->isSelected() )
195  {
196  connect( item, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutMouseHandles::selectedItemSizeChanged );
197  connect( item, &QgsLayoutItem::rotationChanged, this, &QgsLayoutMouseHandles::selectedItemRotationChanged );
198  connect( item, &QgsLayoutItem::frameChanged, this, &QgsLayoutMouseHandles::selectedItemSizeChanged );
199  connect( item, &QgsLayoutItem::lockChanged, this, &QgsLayoutMouseHandles::selectedItemSizeChanged );
200  }
201  else
202  {
203  disconnect( item, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutMouseHandles::selectedItemSizeChanged );
204  disconnect( item, &QgsLayoutItem::rotationChanged, this, &QgsLayoutMouseHandles::selectedItemRotationChanged );
205  disconnect( item, &QgsLayoutItem::frameChanged, this, &QgsLayoutMouseHandles::selectedItemSizeChanged );
206  disconnect( item, &QgsLayoutItem::lockChanged, this, &QgsLayoutMouseHandles::selectedItemSizeChanged );
207  }
208  }
209 
210  resetStatusBar();
211  updateHandles();
212 }
213 
214 void QgsLayoutMouseHandles::selectedItemSizeChanged()
215 {
216  if ( !mIsDragging && !mIsResizing )
217  {
218  //only required for non-mouse initiated size changes
219  updateHandles();
220  }
221 }
222 
223 void QgsLayoutMouseHandles::selectedItemRotationChanged()
224 {
225  if ( !mIsDragging && !mIsResizing )
226  {
227  //only required for non-mouse initiated rotation changes
228  updateHandles();
229  }
230 }
231 
232 void QgsLayoutMouseHandles::updateHandles()
233 {
234  //recalculate size and position of handle item
235 
236  //first check to see if any items are selected
237  QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
238  if ( !selectedItems.isEmpty() )
239  {
240  //one or more items are selected, get bounds of all selected items
241 
242  //update rotation of handle object
243  double rotation;
244  if ( selectionRotation( rotation ) )
245  {
246  //all items share a common rotation value, so we rotate the mouse handles to match
247  setRotation( rotation );
248  }
249  else
250  {
251  //items have varying rotation values - we can't rotate the mouse handles to match
252  setRotation( 0 );
253  }
254 
255  //get bounds of all selected items
256  QRectF newHandleBounds = selectionBounds();
257 
258  //update size and position of handle object
259  setRect( 0, 0, newHandleBounds.width(), newHandleBounds.height() );
260  setPos( mapToScene( newHandleBounds.topLeft() ) );
261 
262  show();
263  }
264  else
265  {
266  //no items selected, hide handles
267  hide();
268  }
269  //force redraw
270  update();
271 }
272 
273 QRectF QgsLayoutMouseHandles::selectionBounds() const
274 {
275  //calculate bounds of all currently selected items in mouse handle coordinate system
276  const QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
277  auto itemIter = selectedItems.constBegin();
278 
279  //start with handle bounds of first selected item
280  QRectF bounds = mapFromItem( ( *itemIter ), ( *itemIter )->rectWithFrame() ).boundingRect();
281 
282  //iterate through remaining items, expanding the bounds as required
283  for ( ++itemIter; itemIter != selectedItems.constEnd(); ++itemIter )
284  {
285  bounds = bounds.united( mapFromItem( ( *itemIter ), ( *itemIter )->rectWithFrame() ).boundingRect() );
286  }
287 
288  return bounds;
289 }
290 
291 bool QgsLayoutMouseHandles::selectionRotation( double &rotation ) const
292 {
293  //check if all selected items have same rotation
294  QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
295  auto itemIter = selectedItems.constBegin();
296 
297  //start with rotation of first selected item
298  double firstItemRotation = ( *itemIter )->rotation();
299 
300  //iterate through remaining items, checking if they have same rotation
301  for ( ++itemIter; itemIter != selectedItems.constEnd(); ++itemIter )
302  {
303  if ( !qgsDoubleNear( ( *itemIter )->rotation(), firstItemRotation ) )
304  {
305  //item has a different rotation, so return false
306  return false;
307  }
308  }
309 
310  //all items have the same rotation, so set the rotation variable and return true
311  rotation = firstItemRotation;
312  return true;
313 }
314 
315 double QgsLayoutMouseHandles::rectHandlerBorderTolerance()
316 {
317  if ( !mView )
318  return 0;
319 
320  //calculate size for resize handles
321  //get view scale factor
322  double viewScaleFactor = mView->transform().m11();
323 
324  //size of handle boxes depends on zoom level in layout view
325  double rectHandlerSize = 10.0 / viewScaleFactor;
326 
327  //make sure the boxes don't get too large
328  if ( rectHandlerSize > ( rect().width() / 3 ) )
329  {
330  rectHandlerSize = rect().width() / 3;
331  }
332  if ( rectHandlerSize > ( rect().height() / 3 ) )
333  {
334  rectHandlerSize = rect().height() / 3;
335  }
336  return rectHandlerSize;
337 }
338 
339 Qt::CursorShape QgsLayoutMouseHandles::cursorForPosition( QPointF itemCoordPos )
340 {
341  QgsLayoutMouseHandles::MouseAction mouseAction = mouseActionForPosition( itemCoordPos );
342  switch ( mouseAction )
343  {
344  case NoAction:
345  return Qt::ForbiddenCursor;
346  case MoveItem:
347  return Qt::SizeAllCursor;
348  case ResizeUp:
349  case ResizeDown:
350  //account for rotation
351  if ( ( rotation() <= 22.5 || rotation() >= 337.5 ) || ( rotation() >= 157.5 && rotation() <= 202.5 ) )
352  {
353  return Qt::SizeVerCursor;
354  }
355  else if ( ( rotation() >= 22.5 && rotation() <= 67.5 ) || ( rotation() >= 202.5 && rotation() <= 247.5 ) )
356  {
357  return Qt::SizeBDiagCursor;
358  }
359  else if ( ( rotation() >= 67.5 && rotation() <= 112.5 ) || ( rotation() >= 247.5 && rotation() <= 292.5 ) )
360  {
361  return Qt::SizeHorCursor;
362  }
363  else
364  {
365  return Qt::SizeFDiagCursor;
366  }
367  case ResizeLeft:
368  case ResizeRight:
369  //account for rotation
370  if ( ( rotation() <= 22.5 || rotation() >= 337.5 ) || ( rotation() >= 157.5 && rotation() <= 202.5 ) )
371  {
372  return Qt::SizeHorCursor;
373  }
374  else if ( ( rotation() >= 22.5 && rotation() <= 67.5 ) || ( rotation() >= 202.5 && rotation() <= 247.5 ) )
375  {
376  return Qt::SizeFDiagCursor;
377  }
378  else if ( ( rotation() >= 67.5 && rotation() <= 112.5 ) || ( rotation() >= 247.5 && rotation() <= 292.5 ) )
379  {
380  return Qt::SizeVerCursor;
381  }
382  else
383  {
384  return Qt::SizeBDiagCursor;
385  }
386 
387  case ResizeLeftUp:
388  case ResizeRightDown:
389  //account for rotation
390  if ( ( rotation() <= 22.5 || rotation() >= 337.5 ) || ( rotation() >= 157.5 && rotation() <= 202.5 ) )
391  {
392  return Qt::SizeFDiagCursor;
393  }
394  else if ( ( rotation() >= 22.5 && rotation() <= 67.5 ) || ( rotation() >= 202.5 && rotation() <= 247.5 ) )
395  {
396  return Qt::SizeVerCursor;
397  }
398  else if ( ( rotation() >= 67.5 && rotation() <= 112.5 ) || ( rotation() >= 247.5 && rotation() <= 292.5 ) )
399  {
400  return Qt::SizeBDiagCursor;
401  }
402  else
403  {
404  return Qt::SizeHorCursor;
405  }
406  case ResizeRightUp:
407  case ResizeLeftDown:
408  //account for rotation
409  if ( ( rotation() <= 22.5 || rotation() >= 337.5 ) || ( rotation() >= 157.5 && rotation() <= 202.5 ) )
410  {
411  return Qt::SizeBDiagCursor;
412  }
413  else if ( ( rotation() >= 22.5 && rotation() <= 67.5 ) || ( rotation() >= 202.5 && rotation() <= 247.5 ) )
414  {
415  return Qt::SizeHorCursor;
416  }
417  else if ( ( rotation() >= 67.5 && rotation() <= 112.5 ) || ( rotation() >= 247.5 && rotation() <= 292.5 ) )
418  {
419  return Qt::SizeFDiagCursor;
420  }
421  else
422  {
423  return Qt::SizeVerCursor;
424  }
425  case SelectItem:
426  return Qt::ArrowCursor;
427  }
428 
429  return Qt::ArrowCursor;
430 }
431 
432 QgsLayoutMouseHandles::MouseAction QgsLayoutMouseHandles::mouseActionForPosition( QPointF itemCoordPos )
433 {
434  bool nearLeftBorder = false;
435  bool nearRightBorder = false;
436  bool nearLowerBorder = false;
437  bool nearUpperBorder = false;
438 
439  bool withinWidth = false;
440  bool withinHeight = false;
441  if ( itemCoordPos.x() >= 0 && itemCoordPos.x() <= rect().width() )
442  {
443  withinWidth = true;
444  }
445  if ( itemCoordPos.y() >= 0 && itemCoordPos.y() <= rect().height() )
446  {
447  withinHeight = true;
448  }
449 
450  double borderTolerance = rectHandlerBorderTolerance();
451 
452  if ( itemCoordPos.x() >= 0 && itemCoordPos.x() < borderTolerance )
453  {
454  nearLeftBorder = true;
455  }
456  if ( itemCoordPos.y() >= 0 && itemCoordPos.y() < borderTolerance )
457  {
458  nearUpperBorder = true;
459  }
460  if ( itemCoordPos.x() <= rect().width() && itemCoordPos.x() > ( rect().width() - borderTolerance ) )
461  {
462  nearRightBorder = true;
463  }
464  if ( itemCoordPos.y() <= rect().height() && itemCoordPos.y() > ( rect().height() - borderTolerance ) )
465  {
466  nearLowerBorder = true;
467  }
468 
469  if ( nearLeftBorder && nearUpperBorder )
470  {
471  return QgsLayoutMouseHandles::ResizeLeftUp;
472  }
473  else if ( nearLeftBorder && nearLowerBorder )
474  {
475  return QgsLayoutMouseHandles::ResizeLeftDown;
476  }
477  else if ( nearRightBorder && nearUpperBorder )
478  {
479  return QgsLayoutMouseHandles::ResizeRightUp;
480  }
481  else if ( nearRightBorder && nearLowerBorder )
482  {
483  return QgsLayoutMouseHandles::ResizeRightDown;
484  }
485  else if ( nearLeftBorder && withinHeight )
486  {
487  return QgsLayoutMouseHandles::ResizeLeft;
488  }
489  else if ( nearRightBorder && withinHeight )
490  {
491  return QgsLayoutMouseHandles::ResizeRight;
492  }
493  else if ( nearUpperBorder && withinWidth )
494  {
495  return QgsLayoutMouseHandles::ResizeUp;
496  }
497  else if ( nearLowerBorder && withinWidth )
498  {
499  return QgsLayoutMouseHandles::ResizeDown;
500  }
501 
502  //find out if cursor position is over a selected item
503  QPointF scenePoint = mapToScene( itemCoordPos );
504  const QList<QGraphicsItem *> itemsAtCursorPos = mLayout->items( scenePoint );
505  if ( itemsAtCursorPos.isEmpty() )
506  {
507  //no items at cursor position
508  return QgsLayoutMouseHandles::SelectItem;
509  }
510  for ( QGraphicsItem *graphicsItem : itemsAtCursorPos )
511  {
512  QgsLayoutItem *item = dynamic_cast<QgsLayoutItem *>( graphicsItem );
513  if ( item && item->isSelected() )
514  {
515  //cursor is over a selected layout item
516  return QgsLayoutMouseHandles::MoveItem;
517  }
518  }
519 
520  //default
521  return QgsLayoutMouseHandles::SelectItem;
522 }
523 
524 QgsLayoutMouseHandles::MouseAction QgsLayoutMouseHandles::mouseActionForScenePos( QPointF sceneCoordPos )
525 {
526  // convert sceneCoordPos to item coordinates
527  QPointF itemPos = mapFromScene( sceneCoordPos );
528  return mouseActionForPosition( itemPos );
529 }
530 
531 bool QgsLayoutMouseHandles::shouldBlockEvent( QInputEvent * ) const
532 {
533  return mIsDragging || mIsResizing;
534 }
535 
536 void QgsLayoutMouseHandles::hoverMoveEvent( QGraphicsSceneHoverEvent *event )
537 {
538  setViewportCursor( cursorForPosition( event->pos() ) );
539 }
540 
541 void QgsLayoutMouseHandles::hoverLeaveEvent( QGraphicsSceneHoverEvent *event )
542 {
543  Q_UNUSED( event )
544  setViewportCursor( Qt::ArrowCursor );
545 }
546 
547 void QgsLayoutMouseHandles::setViewportCursor( Qt::CursorShape cursor )
548 {
549  //workaround qt bug #3732 by setting cursor for QGraphicsView viewport,
550  //rather then setting it directly here
551 
552  if ( qobject_cast< QgsLayoutViewToolSelect *>( mView->tool() ) )
553  {
554  mView->viewport()->setCursor( cursor );
555  }
556 }
557 
558 void QgsLayoutMouseHandles::mouseMoveEvent( QGraphicsSceneMouseEvent *event )
559 {
560  if ( mIsDragging )
561  {
562  //currently dragging a selection
563  //if shift depressed, constrain movement to horizontal/vertical
564  //if control depressed, ignore snapping
565  dragMouseMove( event->lastScenePos(), event->modifiers() & Qt::ShiftModifier, event->modifiers() & Qt::ControlModifier );
566  }
567  else if ( mIsResizing )
568  {
569  //currently resizing a selection
570  //lock aspect ratio if shift depressed
571  //resize from center if alt depressed
572  resizeMouseMove( event->lastScenePos(), event->modifiers() & Qt::ShiftModifier, event->modifiers() & Qt::AltModifier );
573  }
574 
575  mLastMouseEventPos = event->lastScenePos();
576 }
577 
578 void QgsLayoutMouseHandles::mouseReleaseEvent( QGraphicsSceneMouseEvent *event )
579 {
580  QPointF mouseMoveStopPoint = event->lastScenePos();
581  double diffX = mouseMoveStopPoint.x() - mMouseMoveStartPos.x();
582  double diffY = mouseMoveStopPoint.y() - mMouseMoveStartPos.y();
583 
584  //it was only a click
585  if ( std::fabs( diffX ) < std::numeric_limits<double>::min() && std::fabs( diffY ) < std::numeric_limits<double>::min() )
586  {
587  mIsDragging = false;
588  mIsResizing = false;
589  update();
590  hideAlignItems();
591  return;
592  }
593 
594  if ( mCurrentMouseMoveAction == QgsLayoutMouseHandles::MoveItem )
595  {
596  //move selected items
597  mLayout->undoStack()->beginMacro( tr( "Move Items" ) );
598 
599  QPointF mEndHandleMovePos = scenePos();
600 
601  double deltaX = mEndHandleMovePos.x() - mBeginHandlePos.x();
602  double deltaY = mEndHandleMovePos.y() - mBeginHandlePos.y();
603 
604  //move all selected items
605  const QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
606  for ( QgsLayoutItem *item : selectedItems )
607  {
608  if ( item->isLocked() || ( item->flags() & QGraphicsItem::ItemIsSelectable ) == 0 || item->isGroupMember() )
609  {
610  //don't move locked items, or grouped items (group takes care of that)
611  continue;
612  }
613 
614  std::unique_ptr< QgsAbstractLayoutUndoCommand > command( item->createCommand( QString(), 0 ) );
615  command->saveBeforeState();
616 
617  item->attemptMoveBy( deltaX, deltaY );
618 
619  command->saveAfterState();
620  mLayout->undoStack()->push( command.release() );
621  }
622  mLayout->undoStack()->endMacro();
623  }
624  else if ( mCurrentMouseMoveAction != QgsLayoutMouseHandles::NoAction )
625  {
626  //resize selected items
627  mLayout->undoStack()->beginMacro( tr( "Resize Items" ) );
628 
629  //resize all selected items
630  const QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
631  for ( QgsLayoutItem *item : selectedItems )
632  {
633  if ( item->isLocked() || ( item->flags() & QGraphicsItem::ItemIsSelectable ) == 0 )
634  {
635  //don't resize locked items or deselectable items (e.g., items which make up an item group)
636  continue;
637  }
638 
639  std::unique_ptr< QgsAbstractLayoutUndoCommand > command( item->createCommand( QString(), 0 ) );
640  command->saveBeforeState();
641 
642  QRectF itemRect;
643  if ( selectedItems.size() == 1 )
644  {
645  //only a single item is selected, so set its size to the final resized mouse handle size
646  itemRect = mResizeRect;
647  }
648  else
649  {
650  //multiple items selected, so each needs to be scaled relatively to the final size of the mouse handles
651  itemRect = mapRectFromItem( item, item->rectWithFrame() );
652  QgsLayoutUtils::relativeResizeRect( itemRect, QRectF( -mResizeMoveX, -mResizeMoveY, mBeginHandleWidth, mBeginHandleHeight ), mResizeRect );
653  }
654 
655  itemRect = itemRect.normalized();
656  QPointF newPos = mapToScene( itemRect.topLeft() );
657 
658  QgsLayoutSize itemSize = mLayout->convertFromLayoutUnits( itemRect.size(), item->sizeWithUnits().units() );
659  item->attemptResize( itemSize, true );
660 
661  // translate new position to current item units
662  QgsLayoutPoint itemPos = mLayout->convertFromLayoutUnits( newPos, item->positionWithUnits().units() );
663  item->attemptMove( itemPos, false, true );
664 
665  command->saveAfterState();
666  mLayout->undoStack()->push( command.release() );
667  }
668  mLayout->undoStack()->endMacro();
669  }
670 
671  hideAlignItems();
672  if ( mIsDragging )
673  {
674  mIsDragging = false;
675  }
676  if ( mIsResizing )
677  {
678  mIsResizing = false;
679  }
680 
681  //reset default action
682  mCurrentMouseMoveAction = QgsLayoutMouseHandles::MoveItem;
683  setViewportCursor( Qt::ArrowCursor );
684  //redraw handles
685  resetTransform();
686  updateHandles();
687  //reset status bar message
688  resetStatusBar();
689 }
690 
691 void QgsLayoutMouseHandles::resetStatusBar()
692 {
693  if ( !mView )
694  return;
695 
696  const QList<QgsLayoutItem *> selectedItems = mLayout->selectedLayoutItems( false );
697  int selectedCount = selectedItems.size();
698  if ( selectedCount > 1 )
699  {
700  //set status bar message to count of selected items
701  mView->pushStatusMessage( tr( "%1 items selected" ).arg( selectedCount ) );
702  }
703  else if ( selectedCount == 1 )
704  {
705  //set status bar message to count of selected items
706  mView->pushStatusMessage( tr( "1 item selected" ) );
707  }
708  else
709  {
710  //clear status bar message
711  mView->pushStatusMessage( QString() );
712  }
713 }
714 
715 QPointF QgsLayoutMouseHandles::snapPoint( QPointF originalPoint, QgsLayoutMouseHandles::SnapGuideMode mode, bool snapHorizontal, bool snapVertical )
716 {
717  bool snapped = false;
718 
719  const QList< QgsLayoutItem * > selectedItems = mLayout->selectedLayoutItems();
720  QList< QgsLayoutItem * > itemsToExclude;
721  collectItems( selectedItems, itemsToExclude );
722 
723  //depending on the mode, we either snap just the single point, or all the bounds of the selection
724  QPointF snappedPoint;
725  switch ( mode )
726  {
727  case Item:
728  snappedPoint = mLayout->snapper().snapRect( rect().translated( originalPoint ), mView->transform().m11(), snapped, snapHorizontal ? mHorizontalSnapLine : nullptr,
729  snapVertical ? mVerticalSnapLine : nullptr, &itemsToExclude ).topLeft();
730  break;
731  case Point:
732  snappedPoint = mLayout->snapper().snapPoint( originalPoint, mView->transform().m11(), snapped, snapHorizontal ? mHorizontalSnapLine : nullptr,
733  snapVertical ? mVerticalSnapLine : nullptr, &itemsToExclude );
734  break;
735  }
736 
737  return snapped ? snappedPoint : originalPoint;
738 }
739 
740 void QgsLayoutMouseHandles::hideAlignItems()
741 {
742  mHorizontalSnapLine->hide();
743  mVerticalSnapLine->hide();
744 }
745 
746 void QgsLayoutMouseHandles::collectItems( const QList<QgsLayoutItem *> items, QList<QgsLayoutItem *> &collected )
747 {
748  for ( QgsLayoutItem *item : items )
749  {
750  if ( item->type() == QgsLayoutItemRegistry::LayoutGroup )
751  {
752  // if a group is selected, we don't draw the bounds of the group - instead we draw the bounds of the grouped items
753  collectItems( static_cast< QgsLayoutItemGroup * >( item )->items(), collected );
754  }
755  else
756  {
757  collected << item;
758  }
759  }
760 }
761 
762 void QgsLayoutMouseHandles::mousePressEvent( QGraphicsSceneMouseEvent *event )
763 {
764  //save current cursor position
765  mMouseMoveStartPos = event->lastScenePos();
766  mLastMouseEventPos = event->lastScenePos();
767  //save current item geometry
768  mBeginMouseEventPos = event->lastScenePos();
769  mBeginHandlePos = scenePos();
770  mBeginHandleWidth = rect().width();
771  mBeginHandleHeight = rect().height();
772  //type of mouse move action
773  mCurrentMouseMoveAction = mouseActionForPosition( event->pos() );
774 
775  hideAlignItems();
776 
777  if ( mCurrentMouseMoveAction == QgsLayoutMouseHandles::MoveItem )
778  {
779  //moving items
780  mIsDragging = true;
781  }
782  else if ( mCurrentMouseMoveAction != QgsLayoutMouseHandles::SelectItem &&
783  mCurrentMouseMoveAction != QgsLayoutMouseHandles::NoAction )
784  {
785  //resizing items
786  mIsResizing = true;
787  mResizeRect = QRectF( 0, 0, mBeginHandleWidth, mBeginHandleHeight );
788  mResizeMoveX = 0;
789  mResizeMoveY = 0;
790  mCursorOffset = calcCursorEdgeOffset( mMouseMoveStartPos );
791 
792  }
793 
794 }
795 
796 void QgsLayoutMouseHandles::mouseDoubleClickEvent( QGraphicsSceneMouseEvent *event )
797 {
798  Q_UNUSED( event )
799 }
800 
801 QSizeF QgsLayoutMouseHandles::calcCursorEdgeOffset( QPointF cursorPos )
802 {
803  //find offset between cursor position and actual edge of item
804  QPointF sceneMousePos = mapFromScene( cursorPos );
805 
806  switch ( mCurrentMouseMoveAction )
807  {
808  //vertical resize
809  case QgsLayoutMouseHandles::ResizeUp:
810  return QSizeF( 0, sceneMousePos.y() );
811 
812  case QgsLayoutMouseHandles::ResizeDown:
813  return QSizeF( 0, sceneMousePos.y() - rect().height() );
814 
815  //horizontal resize
816  case QgsLayoutMouseHandles::ResizeLeft:
817  return QSizeF( sceneMousePos.x(), 0 );
818 
819  case QgsLayoutMouseHandles::ResizeRight:
820  return QSizeF( sceneMousePos.x() - rect().width(), 0 );
821 
822  //diagonal resize
823  case QgsLayoutMouseHandles::ResizeLeftUp:
824  return QSizeF( sceneMousePos.x(), sceneMousePos.y() );
825 
826  case QgsLayoutMouseHandles::ResizeRightDown:
827  return QSizeF( sceneMousePos.x() - rect().width(), sceneMousePos.y() - rect().height() );
828 
829  case QgsLayoutMouseHandles::ResizeRightUp:
830  return QSizeF( sceneMousePos.x() - rect().width(), sceneMousePos.y() );
831 
832  case QgsLayoutMouseHandles::ResizeLeftDown:
833  return QSizeF( sceneMousePos.x(), sceneMousePos.y() - rect().height() );
834 
835  case MoveItem:
836  case SelectItem:
837  case NoAction:
838  return QSizeF();
839  }
840 
841  return QSizeF();
842 }
843 
844 void QgsLayoutMouseHandles::dragMouseMove( QPointF currentPosition, bool lockMovement, bool preventSnap )
845 {
846  if ( !mLayout )
847  {
848  return;
849  }
850 
851  //calculate total amount of mouse movement since drag began
852  double moveX = currentPosition.x() - mBeginMouseEventPos.x();
853  double moveY = currentPosition.y() - mBeginMouseEventPos.y();
854 
855  //find target position before snapping (in scene coordinates)
856  QPointF upperLeftPoint( mBeginHandlePos.x() + moveX, mBeginHandlePos.y() + moveY );
857 
858  QPointF snappedLeftPoint;
859 
860  //no snapping for rotated items for now
861  if ( !preventSnap && qgsDoubleNear( rotation(), 0.0 ) )
862  {
863  //snap to grid and guides
864  snappedLeftPoint = snapPoint( upperLeftPoint, QgsLayoutMouseHandles::Item );
865 
866  }
867  else
868  {
869  //no snapping
870  snappedLeftPoint = upperLeftPoint;
871  hideAlignItems();
872  }
873 
874  //calculate total shift for item from beginning of drag operation to current position
875  double moveRectX = snappedLeftPoint.x() - mBeginHandlePos.x();
876  double moveRectY = snappedLeftPoint.y() - mBeginHandlePos.y();
877 
878  if ( lockMovement )
879  {
880  //constrained (shift) moving should lock to horizontal/vertical movement
881  //reset the smaller of the x/y movements
882  if ( std::fabs( moveRectX ) <= std::fabs( moveRectY ) )
883  {
884  moveRectX = 0;
885  }
886  else
887  {
888  moveRectY = 0;
889  }
890  }
891 
892  //shift handle item to new position
893  QTransform moveTransform;
894  moveTransform.translate( moveRectX, moveRectY );
895  setTransform( moveTransform );
896 
897  //show current displacement of selection in status bar
898  mView->pushStatusMessage( tr( "dx: %1 mm dy: %2 mm" ).arg( moveRectX ).arg( moveRectY ) );
899 }
900 
901 void QgsLayoutMouseHandles::resizeMouseMove( QPointF currentPosition, bool lockRatio, bool fromCenter )
902 {
903 
904  if ( !mLayout )
905  {
906  return;
907  }
908 
909  double mx = 0.0, my = 0.0, rx = 0.0, ry = 0.0;
910 
911  QPointF beginMousePos;
912  QPointF finalPosition;
913  if ( qgsDoubleNear( rotation(), 0.0 ) )
914  {
915  //snapping only occurs if handles are not rotated for now
916 
917  bool snapVertical = mCurrentMouseMoveAction == ResizeLeft ||
918  mCurrentMouseMoveAction == ResizeRight ||
919  mCurrentMouseMoveAction == ResizeLeftUp ||
920  mCurrentMouseMoveAction == ResizeRightUp ||
921  mCurrentMouseMoveAction == ResizeLeftDown ||
922  mCurrentMouseMoveAction == ResizeRightDown;
923 
924  bool snapHorizontal = mCurrentMouseMoveAction == ResizeUp ||
925  mCurrentMouseMoveAction == ResizeDown ||
926  mCurrentMouseMoveAction == ResizeLeftUp ||
927  mCurrentMouseMoveAction == ResizeRightUp ||
928  mCurrentMouseMoveAction == ResizeLeftDown ||
929  mCurrentMouseMoveAction == ResizeRightDown;
930 
931  //subtract cursor edge offset from begin mouse event and current cursor position, so that snapping occurs to edge of mouse handles
932  //rather then cursor position
933  beginMousePos = mapFromScene( QPointF( mBeginMouseEventPos.x() - mCursorOffset.width(), mBeginMouseEventPos.y() - mCursorOffset.height() ) );
934  QPointF snappedPosition = snapPoint( QPointF( currentPosition.x() - mCursorOffset.width(), currentPosition.y() - mCursorOffset.height() ), QgsLayoutMouseHandles::Point, snapHorizontal, snapVertical );
935  finalPosition = mapFromScene( snappedPosition );
936  }
937  else
938  {
939  //no snapping for rotated items for now
940  beginMousePos = mapFromScene( mBeginMouseEventPos );
941  finalPosition = mapFromScene( currentPosition );
942  }
943 
944  double diffX = finalPosition.x() - beginMousePos.x();
945  double diffY = finalPosition.y() - beginMousePos.y();
946 
947  double ratio = 0;
948  if ( lockRatio && !qgsDoubleNear( mBeginHandleHeight, 0.0 ) )
949  {
950  ratio = mBeginHandleWidth / mBeginHandleHeight;
951  }
952 
953  switch ( mCurrentMouseMoveAction )
954  {
955  //vertical resize
956  case QgsLayoutMouseHandles::ResizeUp:
957  {
958  if ( ratio )
959  {
960  diffX = ( ( mBeginHandleHeight - diffY ) * ratio ) - mBeginHandleWidth;
961  mx = -diffX / 2;
962  my = diffY;
963  rx = diffX;
964  ry = -diffY;
965  }
966  else
967  {
968  mx = 0;
969  my = diffY;
970  rx = 0;
971  ry = -diffY;
972  }
973  break;
974  }
975 
976  case QgsLayoutMouseHandles::ResizeDown:
977  {
978  if ( ratio )
979  {
980  diffX = ( ( mBeginHandleHeight + diffY ) * ratio ) - mBeginHandleWidth;
981  mx = -diffX / 2;
982  my = 0;
983  rx = diffX;
984  ry = diffY;
985  }
986  else
987  {
988  mx = 0;
989  my = 0;
990  rx = 0;
991  ry = diffY;
992  }
993  break;
994  }
995 
996  //horizontal resize
997  case QgsLayoutMouseHandles::ResizeLeft:
998  {
999  if ( ratio )
1000  {
1001  diffY = ( ( mBeginHandleWidth - diffX ) / ratio ) - mBeginHandleHeight;
1002  mx = diffX;
1003  my = -diffY / 2;
1004  rx = -diffX;
1005  ry = diffY;
1006  }
1007  else
1008  {
1009  mx = diffX, my = 0;
1010  rx = -diffX;
1011  ry = 0;
1012  }
1013  break;
1014  }
1015 
1016  case QgsLayoutMouseHandles::ResizeRight:
1017  {
1018  if ( ratio )
1019  {
1020  diffY = ( ( mBeginHandleWidth + diffX ) / ratio ) - mBeginHandleHeight;
1021  mx = 0;
1022  my = -diffY / 2;
1023  rx = diffX;
1024  ry = diffY;
1025  }
1026  else
1027  {
1028  mx = 0;
1029  my = 0;
1030  rx = diffX, ry = 0;
1031  }
1032  break;
1033  }
1034 
1035  //diagonal resize
1036  case QgsLayoutMouseHandles::ResizeLeftUp:
1037  {
1038  if ( ratio )
1039  {
1040  //ratio locked resize
1041  if ( ( mBeginHandleWidth - diffX ) / ( mBeginHandleHeight - diffY ) > ratio )
1042  {
1043  diffX = mBeginHandleWidth - ( ( mBeginHandleHeight - diffY ) * ratio );
1044  }
1045  else
1046  {
1047  diffY = mBeginHandleHeight - ( ( mBeginHandleWidth - diffX ) / ratio );
1048  }
1049  }
1050  mx = diffX, my = diffY;
1051  rx = -diffX;
1052  ry = -diffY;
1053  break;
1054  }
1055 
1056  case QgsLayoutMouseHandles::ResizeRightDown:
1057  {
1058  if ( ratio )
1059  {
1060  //ratio locked resize
1061  if ( ( mBeginHandleWidth + diffX ) / ( mBeginHandleHeight + diffY ) > ratio )
1062  {
1063  diffX = ( ( mBeginHandleHeight + diffY ) * ratio ) - mBeginHandleWidth;
1064  }
1065  else
1066  {
1067  diffY = ( ( mBeginHandleWidth + diffX ) / ratio ) - mBeginHandleHeight;
1068  }
1069  }
1070  mx = 0;
1071  my = 0;
1072  rx = diffX, ry = diffY;
1073  break;
1074  }
1075 
1076  case QgsLayoutMouseHandles::ResizeRightUp:
1077  {
1078  if ( ratio )
1079  {
1080  //ratio locked resize
1081  if ( ( mBeginHandleWidth + diffX ) / ( mBeginHandleHeight - diffY ) > ratio )
1082  {
1083  diffX = ( ( mBeginHandleHeight - diffY ) * ratio ) - mBeginHandleWidth;
1084  }
1085  else
1086  {
1087  diffY = mBeginHandleHeight - ( ( mBeginHandleWidth + diffX ) / ratio );
1088  }
1089  }
1090  mx = 0;
1091  my = diffY, rx = diffX, ry = -diffY;
1092  break;
1093  }
1094 
1095  case QgsLayoutMouseHandles::ResizeLeftDown:
1096  {
1097  if ( ratio )
1098  {
1099  //ratio locked resize
1100  if ( ( mBeginHandleWidth - diffX ) / ( mBeginHandleHeight + diffY ) > ratio )
1101  {
1102  diffX = mBeginHandleWidth - ( ( mBeginHandleHeight + diffY ) * ratio );
1103  }
1104  else
1105  {
1106  diffY = ( ( mBeginHandleWidth - diffX ) / ratio ) - mBeginHandleHeight;
1107  }
1108  }
1109  mx = diffX, my = 0;
1110  rx = -diffX;
1111  ry = diffY;
1112  break;
1113  }
1114 
1115  case QgsLayoutMouseHandles::MoveItem:
1116  case QgsLayoutMouseHandles::SelectItem:
1117  case QgsLayoutMouseHandles::NoAction:
1118  break;
1119  }
1120 
1121  //resizing from center of objects?
1122  if ( fromCenter )
1123  {
1124  my = -ry;
1125  mx = -rx;
1126  ry = 2 * ry;
1127  rx = 2 * rx;
1128  }
1129 
1130  //update selection handle rectangle
1131 
1132  //make sure selection handle size rectangle is normalized (ie, left coord < right coord)
1133  mResizeMoveX = mBeginHandleWidth + rx > 0 ? mx : mx + mBeginHandleWidth + rx;
1134  mResizeMoveY = mBeginHandleHeight + ry > 0 ? my : my + mBeginHandleHeight + ry;
1135 
1136  //calculate movement in scene coordinates
1137  QLineF translateLine = QLineF( 0, 0, mResizeMoveX, mResizeMoveY );
1138  translateLine.setAngle( translateLine.angle() - rotation() );
1139  QPointF sceneTranslate = translateLine.p2();
1140 
1141  //move selection handles
1142  QTransform itemTransform;
1143  itemTransform.translate( sceneTranslate.x(), sceneTranslate.y() );
1144  setTransform( itemTransform );
1145 
1146  //handle non-normalised resizes - e.g., dragging the left handle so far to the right that it's past the right handle
1147  if ( mBeginHandleWidth + rx >= 0 && mBeginHandleHeight + ry >= 0 )
1148  {
1149  mResizeRect = QRectF( 0, 0, mBeginHandleWidth + rx, mBeginHandleHeight + ry );
1150  }
1151  else if ( mBeginHandleHeight + ry >= 0 )
1152  {
1153  mResizeRect = QRectF( QPointF( -( mBeginHandleWidth + rx ), 0 ), QPointF( 0, mBeginHandleHeight + ry ) );
1154  }
1155  else if ( mBeginHandleWidth + rx >= 0 )
1156  {
1157  mResizeRect = QRectF( QPointF( 0, -( mBeginHandleHeight + ry ) ), QPointF( mBeginHandleWidth + rx, 0 ) );
1158  }
1159  else
1160  {
1161  mResizeRect = QRectF( QPointF( -( mBeginHandleWidth + rx ), -( mBeginHandleHeight + ry ) ), QPointF( 0, 0 ) );
1162  }
1163 
1164  setRect( 0, 0, std::fabs( mBeginHandleWidth + rx ), std::fabs( mBeginHandleHeight + ry ) );
1165 
1166  //show current size of selection in status bar
1167  mView->pushStatusMessage( tr( "width: %1 mm height: %2 mm" ).arg( rect().width() ).arg( rect().height() ) );
1168 }
1169 
QgsLayoutPoint positionWithUnits() const
Returns the item&#39;s current position, including units.
Base class for graphical items within a QgsLayout.
int type() const override
Returns a unique graphics item type identifier.
A graphical widget to display and interact with QgsLayouts.
Definition: qgslayoutview.h:49
virtual QRectF rectWithFrame() const
Returns the item&#39;s rectangular bounds, including any bleed caused by the item&#39;s frame.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:265
bool isGroupMember() const
Returns true if the item is part of a QgsLayoutItemGroup group.
QgsLayoutSize sizeWithUnits() const
Returns the item&#39;s current size, including units.
void attemptMoveBy(double deltaX, double deltaY)
Attempts to shift the item&#39;s position by a specified deltaX and deltaY, in layout units...
This class provides a method of storing points, consisting of an x and y coordinate, for use in QGIS layouts.
void frameChanged()
Emitted if the item&#39;s frame style changes.
void sizePositionChanged()
Emitted when the item&#39;s size or position changes.
QgsUnitTypes::LayoutUnit units() const
Returns the units for the point.
virtual void attemptResize(const QgsLayoutSize &size, bool includesFrame=false)
Attempts to resize the item to a specified target size.
QgsUnitTypes::LayoutUnit units() const
Returns the units for the size.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:49
void lockChanged()
Emitted if the item&#39;s lock status changes.
virtual void attemptMove(const QgsLayoutPoint &point, bool useReferencePoint=true, bool includesFrame=false, int page=-1)
Attempts to move the item to a specified point.
static void relativeResizeRect(QRectF &rectToResize, const QRectF &boundsBefore, const QRectF &boundsAfter)
Resizes a QRectF relative to a resized bounding rectangle.
bool isLocked() const
Returns true if the item is locked, and cannot be interacted with using the mouse.
This class provides a method of storing sizes, consisting of a width and height, for use in QGIS layo...
Definition: qgslayoutsize.h:40
void rotationChanged(double newRotation)
Emitted on item rotation change.
QgsAbstractLayoutUndoCommand * createCommand(const QString &text, int id, QUndoCommand *parent=nullptr) override
Creates a new layout undo command with the specified text and parent.