1 /***************************************************************************
2                              qgslayoutviewtoolselect.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 
16 #include "qgslayoutviewtoolselect.h"
17 #include "qgslayoutviewmouseevent.h"
18 #include "qgslayoutview.h"
19 #include "qgslayout.h"
20 #include "qgslayoutitempage.h"
21 #include "qgslayoutmousehandles.h"
22 #include "qgslayoutitemgroup.h"
23 
24 
QgsLayoutViewToolSelect(QgsLayoutView * view)25 QgsLayoutViewToolSelect::QgsLayoutViewToolSelect( QgsLayoutView *view )
26   : QgsLayoutViewTool( view, tr( "Select" ) )
27 {
28   setCursor( Qt::ArrowCursor );
29 
30   mRubberBand.reset( new QgsLayoutViewRectangularRubberBand( view ) );
31   mRubberBand->setBrush( QBrush( QColor( 224, 178, 76, 63 ) ) );
32   mRubberBand->setPen( QPen( QBrush( QColor( 254, 58, 29, 100 ) ), 0, Qt::DotLine ) );
33 }
34 
~QgsLayoutViewToolSelect()35 QgsLayoutViewToolSelect::~QgsLayoutViewToolSelect()
36 {
37   if ( mMouseHandles )
38   {
39     // want to force them to be removed from the scene
40     if ( mMouseHandles->scene() )
41       mMouseHandles->scene()->removeItem( mMouseHandles );
42     mMouseHandles->deleteLater();
43   }
44 }
45 
layoutPressEvent(QgsLayoutViewMouseEvent * event)46 void QgsLayoutViewToolSelect::layoutPressEvent( QgsLayoutViewMouseEvent *event )
47 {
48   if ( mMouseHandles->shouldBlockEvent( event ) )
49   {
50     //swallow clicks while dragging/resizing items
51     return;
52   }
53 
54   if ( mMouseHandles->isVisible() )
55   {
56     //selection handles are being shown, get mouse action for current cursor position
57     QgsLayoutMouseHandles::MouseAction mouseAction = mMouseHandles->mouseActionForScenePos( event->layoutPoint() );
58 
59     if ( mouseAction != QgsLayoutMouseHandles::MoveItem
60          && mouseAction != QgsLayoutMouseHandles::NoAction
61          && mouseAction != QgsLayoutMouseHandles::SelectItem )
62     {
63       //mouse is over a resize handle, so propagate event onward
64       event->ignore();
65       return;
66     }
67   }
68 
69   if ( event->button() != Qt::LeftButton )
70   {
71     event->ignore();
72     return;
73   }
74 
75   QgsLayoutItem *selectedItem = nullptr;
76   QgsLayoutItem *previousSelectedItem = nullptr;
77 
78   QList<QgsLayoutItem *> selectedItems = layout()->selectedLayoutItems();
79 
80   if ( event->modifiers() & Qt::ControlModifier )
81   {
82     //CTRL modifier, so we are trying to select the next item below the current one
83     //first, find currently selected item
84     if ( !selectedItems.isEmpty() )
85     {
86       previousSelectedItem = selectedItems.at( 0 );
87     }
88   }
89 
90   if ( previousSelectedItem )
91   {
92     //select highest item just below previously selected item at position of event
93     selectedItem = layout()->layoutItemAt( event->layoutPoint(), previousSelectedItem, true );
94 
95     //if we didn't find a lower item we'll use the top-most as fall-back
96     //this duplicates mapinfo/illustrator/etc behavior where ctrl-clicks are "cyclic"
97     if ( !selectedItem )
98     {
99       selectedItem = layout()->layoutItemAt( event->layoutPoint(), true );
100     }
101   }
102   else
103   {
104     //select topmost item at position of event
105     selectedItem = layout()->layoutItemAt( event->layoutPoint(), true );
106   }
107 
108   // if selected item is in a group, we actually get the top-level group it's part of
109   QgsLayoutItemGroup *group = selectedItem ? selectedItem->parentGroup() : nullptr;
110   while ( group && group->parentGroup() )
111   {
112     group = group->parentGroup();
113   }
114   if ( group )
115     selectedItem = group;
116 
117   if ( !selectedItem )
118   {
119     //not clicking over an item, so start marquee selection
120     mIsSelecting = true;
121     mMousePressStartPos = event->pos();
122     mRubberBand->start( event->layoutPoint(), Qt::KeyboardModifiers() );
123     return;
124   }
125 
126   if ( ( event->modifiers() & Qt::ShiftModifier ) && ( selectedItem->isSelected() ) )
127   {
128     //SHIFT-clicking a selected item deselects it
129     selectedItem->setSelected( false );
130 
131     //Check if we have any remaining selected items, and if so, update the item panel
132     const QList<QgsLayoutItem *> selectedItems = layout()->selectedLayoutItems();
133     if ( !selectedItems.isEmpty() )
134     {
135       emit itemFocused( selectedItems.at( 0 ) );
136     }
137     else
138     {
139       emit itemFocused( nullptr );
140     }
141   }
142   else
143   {
144     if ( ( !selectedItem->isSelected() ) &&       //keep selection if an already selected item pressed
145          !( event->modifiers() & Qt::ShiftModifier ) ) //keep selection if shift key pressed
146     {
147       layout()->setSelectedItem( selectedItem ); // clears existing selection
148     }
149     else
150     {
151       selectedItem->setSelected( true );
152     }
153     event->ignore();
154     emit itemFocused( selectedItem );
155   }
156 }
157 
layoutMoveEvent(QgsLayoutViewMouseEvent * event)158 void QgsLayoutViewToolSelect::layoutMoveEvent( QgsLayoutViewMouseEvent *event )
159 {
160   if ( mIsSelecting )
161   {
162     mRubberBand->update( event->layoutPoint(), Qt::KeyboardModifiers() );
163   }
164   else
165   {
166     event->ignore();
167   }
168 }
169 
layoutReleaseEvent(QgsLayoutViewMouseEvent * event)170 void QgsLayoutViewToolSelect::layoutReleaseEvent( QgsLayoutViewMouseEvent *event )
171 {
172   if ( event->button() != Qt::LeftButton && mMouseHandles->shouldBlockEvent( event ) )
173   {
174     //swallow clicks while dragging/resizing items
175     return;
176   }
177 
178   if ( !mIsSelecting || event->button() != Qt::LeftButton )
179   {
180     event->ignore();
181     return;
182   }
183 
184   mIsSelecting = false;
185   bool wasClick = !isClickAndDrag( mMousePressStartPos, event->pos() );
186 
187   // important -- we don't pass the event modifiers here, because we use them for a different meaning!
188   // (modifying how the selection interacts with the items, rather than modifying the selection shape)
189   QRectF rect = mRubberBand->finish( event->layoutPoint() );
190 
191   bool subtractingSelection = false;
192   if ( event->modifiers() & Qt::ShiftModifier )
193   {
194     //shift modifier means adding to selection, nothing required here
195   }
196   else if ( event->modifiers() & Qt::ControlModifier )
197   {
198     //control modifier means subtract from current selection
199     subtractingSelection = true;
200   }
201   else
202   {
203     //not adding to or removing from selection, so clear current selection
204     whileBlocking( layout() )->deselectAll();
205   }
206 
207   //determine item selection mode, default to intersection
208   Qt::ItemSelectionMode selectionMode = Qt::IntersectsItemShape;
209   if ( event->modifiers() & Qt::AltModifier )
210   {
211     //alt modifier switches to contains selection mode
212     selectionMode = Qt::ContainsItemShape;
213   }
214 
215   //find all items in rect
216   QList<QGraphicsItem *> itemList;
217   if ( wasClick )
218     itemList = layout()->items( rect.center(), selectionMode );
219   else
220     itemList = layout()->items( rect, selectionMode );
221   for ( QGraphicsItem *item : qgis::as_const( itemList ) )
222   {
223     QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( item );
224     QgsLayoutItemPage *paperItem = dynamic_cast<QgsLayoutItemPage *>( item );
225     if ( layoutItem && !paperItem )
226     {
227       if ( !layoutItem->isLocked() )
228       {
229         if ( subtractingSelection )
230         {
231           layoutItem->setSelected( false );
232         }
233         else
234         {
235           layoutItem->setSelected( true );
236         }
237         if ( wasClick )
238         {
239           // found an item, and only a click - nothing more to do
240           break;
241         }
242       }
243     }
244   }
245 
246   //update item panel
247   const QList<QgsLayoutItem *> selectedItemList = layout()->selectedLayoutItems();
248   if ( !selectedItemList.isEmpty() )
249   {
250     emit itemFocused( selectedItemList.at( 0 ) );
251   }
252   else
253   {
254     emit itemFocused( nullptr );
255   }
256   mMouseHandles->selectionChanged();
257 }
258 
wheelEvent(QWheelEvent * event)259 void QgsLayoutViewToolSelect::wheelEvent( QWheelEvent *event )
260 {
261   if ( mMouseHandles->shouldBlockEvent( event ) )
262   {
263     //ignore wheel events while dragging/resizing items
264     return;
265   }
266   else
267   {
268     event->ignore();
269   }
270 }
271 
keyPressEvent(QKeyEvent * event)272 void QgsLayoutViewToolSelect::keyPressEvent( QKeyEvent *event )
273 {
274   if ( mMouseHandles->isDragging() || mMouseHandles->isResizing() )
275   {
276     return;
277   }
278   else
279   {
280     event->ignore();
281   }
282 }
283 
deactivate()284 void QgsLayoutViewToolSelect::deactivate()
285 {
286   if ( mIsSelecting )
287   {
288     mRubberBand->finish();
289     mIsSelecting = false;
290   }
291   QgsLayoutViewTool::deactivate();
292 }
293 
294 ///@cond PRIVATE
mouseHandles()295 QgsLayoutMouseHandles *QgsLayoutViewToolSelect::mouseHandles()
296 {
297   return mMouseHandles;
298 }
299 
setLayout(QgsLayout * layout)300 void QgsLayoutViewToolSelect::setLayout( QgsLayout *layout )
301 {
302   // existing handles are owned by previous layout
303   if ( mMouseHandles )
304     mMouseHandles->deleteLater();
305 
306   //add mouse selection handles to layout, and initially hide
307   mMouseHandles = new QgsLayoutMouseHandles( layout, view() );
308   mMouseHandles->hide();
309   mMouseHandles->setZValue( QgsLayout::ZMouseHandles );
310   layout->addItem( mMouseHandles );
311 }
312 ///@endcond
313