QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgsidentifymenu.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsidentifymenu.cpp - menu to be used in identify map tool
3  ---------------------
4  begin : August 2014
5  copyright : (C) 2014 by Denis Rouzaud
6  email : denis.rouzaud@gmail.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 <QMouseEvent>
17 
18 #include "qgsidentifymenu.h"
19 
20 #include "qgsapplication.h"
21 #include "qgsattributeaction.h"
22 #include "qgshighlight.h"
23 
24 QgsIdentifyMenu::CustomActionRegistry::CustomActionRegistry( QObject* parent )
25  : QgsMapLayerActionRegistry( parent )
26 {
27 }
28 
29 
31  : QMenu( canvas )
32  , mCanvas( canvas )
33  , mAllowMultipleReturn( true )
34  , mExecWithSingleResult( false )
35  , mShowFeatureActions( false )
36  , mResultsIfExternalAction( false )
37  , mMaxLayerDisplay( 10 )
38  , mMaxFeatureDisplay( 10 )
39  , mDefaultActionName( tr( "Identify" ) )
40  , mCustomActionRegistry( CustomActionRegistry::instance() )
41 {
42 }
43 
45 {
46  deleteRubberBands();
47 }
48 
49 
50 void QgsIdentifyMenu::setMaxLayerDisplay( int maxLayerDisplay )
51 {
52  if ( maxLayerDisplay < 0 )
53  {
54  QgsDebugMsg( "invalid value for number of layers displayed." );
55  }
56  mMaxLayerDisplay = maxLayerDisplay;
57 }
58 
59 
60 void QgsIdentifyMenu::setMaxFeatureDisplay( int maxFeatureDisplay )
61 {
62  if ( maxFeatureDisplay < 0 )
63  {
64  QgsDebugMsg( "invalid value for number of layers displayed." );
65  }
66  mMaxFeatureDisplay = maxFeatureDisplay;
67 }
68 
69 
70 QList<QgsMapToolIdentify::IdentifyResult> QgsIdentifyMenu::exec( const QList<QgsMapToolIdentify::IdentifyResult> idResults, QPoint pos )
71 {
72  clear();
73  mLayerIdResults.clear();
74 
75  QList<QgsMapToolIdentify::IdentifyResult> returnResults = QList<QgsMapToolIdentify::IdentifyResult>();
76 
77  if ( idResults.count() == 0 )
78  {
79  return returnResults;
80  }
81  if ( idResults.count() == 1 && !mExecWithSingleResult )
82  {
83  returnResults << idResults[0];
84  return returnResults;
85  }
86 
87  // sort results by layer
88  Q_FOREACH ( const QgsMapToolIdentify::IdentifyResult result, idResults )
89  {
90  QgsMapLayer *layer = result.mLayer;
91  if ( mLayerIdResults.contains( layer ) )
92  {
93  mLayerIdResults[layer].append( result );
94  }
95  else
96  {
97  mLayerIdResults.insert( layer, QList<QgsMapToolIdentify::IdentifyResult>() << result );
98  }
99  }
100 
101  // add results to the menu
102  bool singleLayer = mLayerIdResults.count() == 1;
103  int count = 0;
104  QMapIterator< QgsMapLayer*, QList<QgsMapToolIdentify::IdentifyResult> > it( mLayerIdResults );
105  while ( it.hasNext() )
106  {
107  if ( mMaxLayerDisplay != 0 && count > mMaxLayerDisplay )
108  break;
109  ++count;
110  it.next();
111  QgsMapLayer* layer = it.key();
112  if ( layer->type() == QgsMapLayer::RasterLayer )
113  {
114  addRasterLayer( layer );
115  }
116  else if ( layer->type() == QgsMapLayer::VectorLayer )
117  {
118  QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( layer );
119  if ( !vl )
120  continue;
121  addVectorLayer( vl, it.value(), singleLayer );
122  }
123  }
124 
125  // add an "identify all" action on the top level
126  if ( !singleLayer && mAllowMultipleReturn && idResults.count() > 1 )
127  {
128  addSeparator();
129  QAction* allAction = new QAction( QgsApplication::getThemeIcon( "/mActionIdentify.svg" ), tr( "%1 all (%2)" ).arg( mDefaultActionName ).arg( idResults.count() ), this );
130  allAction->setData( QVariant::fromValue<ActionData>( ActionData( 0 ) ) );
131  connect( allAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
132  addAction( allAction );
133  }
134 
135  // exec
136  QAction* selectedAction = QMenu::exec( pos );
137  bool externalAction;
138  returnResults = results( selectedAction, externalAction );
139 
140  // delete actions
141  clear();
142  // also remove the QgsActionMenu
143  qDeleteAll( findChildren<QgsActionMenu*>() );
144 
145  if ( externalAction && !mResultsIfExternalAction )
146  {
147  return QList<QgsMapToolIdentify::IdentifyResult>();
148  }
149  else
150  {
151  return returnResults;
152  }
153 }
154 
155 void QgsIdentifyMenu::closeEvent( QCloseEvent* e )
156 {
157  deleteRubberBands();
158  QMenu::closeEvent( e );
159 }
160 
161 void QgsIdentifyMenu::addRasterLayer( QgsMapLayer* layer )
162 {
163  QAction* layerAction;
164  QMenu* layerMenu = 0;
165 
166  QList<QgsMapLayerAction*> separators = QList<QgsMapLayerAction*>();
167  QList<QgsMapLayerAction*> layerActions = mCustomActionRegistry.mapLayerActions( layer, QgsMapLayerAction::Layer );
168  int nCustomActions = layerActions.count();
169  if ( nCustomActions )
170  {
171  separators.append( layerActions[0] );
172  }
173  if ( mShowFeatureActions )
174  {
175  layerActions.append( QgsMapLayerActionRegistry::instance()->mapLayerActions( layer, QgsMapLayerAction::Layer ) );
176  if ( layerActions.count() > nCustomActions )
177  {
178  separators.append( layerActions[nCustomActions] );
179  }
180  }
181 
182  // use a menu only if actions will be listed
183  if ( !layerActions.count() )
184  {
185  layerAction = new QAction( layer->name(), this );
186  }
187  else
188  {
189  layerMenu = new QMenu( layer->name(), this );
190  layerAction = layerMenu->menuAction();
191  }
192 
193  // add layer action to the top menu
194  layerAction->setIcon( QgsApplication::getThemeIcon( "/mIconRasterLayer.png" ) );
195  layerAction->setData( QVariant::fromValue<ActionData>( ActionData( layer ) ) );
196  connect( layerAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
197  addAction( layerAction );
198 
199  // no need to go further if there is no menu
200  if ( !layerMenu )
201  return;
202 
203  // add default identify action
204  QAction* identifyFeatureAction = new QAction( mDefaultActionName, layerMenu );
205  connect( identifyFeatureAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
206  identifyFeatureAction->setData( QVariant::fromValue<ActionData>( ActionData( layer ) ) );
207  layerMenu->addAction( identifyFeatureAction );
208 
209  // add custom/layer actions
210  Q_FOREACH ( QgsMapLayerAction* mapLayerAction, layerActions )
211  {
212  QAction* action = new QAction( mapLayerAction->icon(), mapLayerAction->text(), layerMenu );
213  action->setData( QVariant::fromValue<ActionData>( ActionData( layer, true ) ) );
214  connect( action, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
215  connect( action, SIGNAL( triggered() ), this, SLOT( triggerMapLayerAction() ) );
216  layerMenu->addAction( action );
217  if ( separators.contains( mapLayerAction ) )
218  {
219  layerMenu->insertSeparator( action );
220  }
221  }
222 }
223 
224 void QgsIdentifyMenu::addVectorLayer( QgsVectorLayer* layer, const QList<QgsMapToolIdentify::IdentifyResult> results, bool singleLayer )
225 {
226  QAction* layerAction = 0;
227  QMenu* layerMenu = 0;
228 
229  // do not add actions with MultipleFeatures as target if only 1 feature is found for this layer
230  // targets defines which actions will be shown
231  QgsMapLayerAction::Targets targets = results.count() > 1 ? QgsMapLayerAction::Layer | QgsMapLayerAction::MultipleFeatures : QgsMapLayerAction::Layer;
232 
233  QList<QgsMapLayerAction*> separators = QList<QgsMapLayerAction*>();
234  QList<QgsMapLayerAction*> layerActions = mCustomActionRegistry.mapLayerActions( layer, targets );
235  int nCustomActions = layerActions.count();
236  if ( nCustomActions )
237  {
238  separators << layerActions[0];
239  }
240  if ( mShowFeatureActions )
241  {
242  layerActions << QgsMapLayerActionRegistry::instance()->mapLayerActions( layer, targets );
243 
244  if ( layerActions.count() > nCustomActions )
245  {
246  separators << layerActions[nCustomActions];
247  }
248  }
249 
250  // determines if a menu should be created or not. Following cases:
251  // 1. only one result and no feature action to be shown => just create an action
252  // 2. several features (2a) or display feature actions (2b) => create a menu
253  // 3. case 2 but only one layer (singeLayer) => do not create a menu, but give the top menu instead
254 
255  bool createMenu = results.count() > 1 || layerActions.count() > 0;
256 
257  // case 2b: still create a menu for layer, if there is a sub-level for features
258  // i.e custom actions or map layer actions at feature level
259  if ( !createMenu )
260  {
261  createMenu = mCustomActionRegistry.mapLayerActions( layer, QgsMapLayerAction::SingleFeature ).count() > 0;
262  if ( !createMenu && mShowFeatureActions )
263  {
264  QgsActionMenu* featureActionMenu = new QgsActionMenu( layer, &( results[0].mFeature ), this );
265  createMenu = featureActionMenu->actions().count() > 0;
266  delete featureActionMenu;
267  }
268  }
269 
270  // use a menu only if actions will be listed
271  if ( !createMenu )
272  {
273  // case 1
274  layerAction = new QAction( layer->name(), this );
275  }
276  else
277  {
278  if ( singleLayer )
279  {
280  // case 3
281  layerMenu = this;
282  }
283  else
284  {
285  // case 2
286  layerMenu = new QMenu( layer->name(), this );
287  layerAction = layerMenu->menuAction();
288  }
289  }
290 
291  // case 1 or 2
292  if ( layerAction )
293  {
294  // icons
295  switch ( layer->geometryType() )
296  {
297  case QGis::Point:
298  layerAction->setIcon( QgsApplication::getThemeIcon( "/mIconPointLayer.png" ) );
299  break;
300  case QGis::Line:
301  layerAction->setIcon( QgsApplication::getThemeIcon( "/mIconLineLayer.png" ) );
302  break;
303  case QGis::Polygon:
304  layerAction->setIcon( QgsApplication::getThemeIcon( "/mIconPolygonLayer.png" ) );
305  break;
306  default:
307  break;
308  }
309 
310  // add layer action to the top menu
311  layerAction->setData( QVariant::fromValue<ActionData>( ActionData( layer ) ) );
312  connect( layerAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
313  addAction( layerAction );
314  }
315 
316  // case 1. no need to go further
317  if ( !layerMenu )
318  return;
319 
320  // add results to the menu
321  int count = 0;
322  Q_FOREACH ( const QgsMapToolIdentify::IdentifyResult result, results )
323  {
324  if ( mMaxFeatureDisplay != 0 && count > mMaxFeatureDisplay )
325  break;
326  ++count;
327 
328  QAction* featureAction = 0;
329  QMenu* featureMenu = 0;
330  QgsActionMenu* featureActionMenu = 0;
331 
332  QList<QgsMapLayerAction*> customFeatureActions = mCustomActionRegistry.mapLayerActions( layer, QgsMapLayerAction::SingleFeature );
333  if ( mShowFeatureActions )
334  {
335  featureActionMenu = new QgsActionMenu( layer, result.mFeature.id(), layerMenu );
336  }
337 
338  // feature title
339  QString featureTitle = result.mFeature.attribute( layer->displayField() ).toString();
340  if ( featureTitle.isEmpty() )
341  featureTitle = QString( "%1" ).arg( result.mFeature.id() );
342 
343  if ( !customFeatureActions.count() && ( !featureActionMenu || !featureActionMenu->actions().count() ) )
344  {
345  featureAction = new QAction( featureTitle, layerMenu );
346  // add the feature action (or menu) to the layer menu
347  featureAction->setData( QVariant::fromValue<ActionData>( ActionData( layer, result.mFeature.id() ) ) );
348  connect( featureAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
349  layerMenu->addAction( featureAction );
350  }
351  else if ( results.count() == 1 )
352  {
353  // if we are here with only one results, this means there is a sub-feature level (for actions)
354  // => skip the feature level since there would be only a single entry
355  // => give the layer menu as pointer instead of a new feature menu
356  featureMenu = layerMenu;
357  }
358  else
359  {
360  featureMenu = new QMenu( featureTitle, layerMenu );
361 
362  // get the action from the menu
363  featureAction = featureMenu->menuAction();
364  // add the feature action (or menu) to the layer menu
365  featureAction->setData( QVariant::fromValue<ActionData>( ActionData( layer, result.mFeature.id() ) ) );
366  connect( featureAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
367  layerMenu->addAction( featureAction );
368  }
369 
370  // if no feature menu, no need to go further
371  if ( !featureMenu )
372  continue;
373 
374  // add default identify action
375  QAction* identifyFeatureAction = new QAction( QgsApplication::getThemeIcon( "/mActionIdentify.svg" ), mDefaultActionName, featureMenu );
376  connect( identifyFeatureAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
377  identifyFeatureAction->setData( QVariant::fromValue<ActionData>( ActionData( layer, result.mFeature.id() ) ) );
378  featureMenu->addAction( identifyFeatureAction );
379  featureMenu->addSeparator();
380 
381  // custom action at feature level
382  Q_FOREACH ( QgsMapLayerAction* mapLayerAction, customFeatureActions )
383  {
384  QAction* action = new QAction( mapLayerAction->icon(), mapLayerAction->text(), featureMenu );
385  action->setData( QVariant::fromValue<ActionData>( ActionData( layer, result.mFeature.id(), mapLayerAction ) ) );
386  connect( action, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
387  connect( action, SIGNAL( triggered() ), this, SLOT( triggerMapLayerAction() ) );
388  featureMenu->addAction( action );
389  }
390  // use QgsActionMenu for feature actions
391  if ( featureActionMenu )
392  {
393  Q_FOREACH ( QAction* action, featureActionMenu->actions() )
394  {
395  connect( action, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
396  featureMenu->addAction( action );
397  }
398  }
399  }
400 
401  // back to layer level
402 
403  // identify all action
404  if ( mAllowMultipleReturn && results.count() > 1 )
405  {
406  layerMenu->addSeparator();
407  QAction* allAction = new QAction( QgsApplication::getThemeIcon( "/mActionIdentify.svg" ), tr( "%1 all (%2)" ).arg( mDefaultActionName ).arg( results.count() ), layerMenu );
408  allAction->setData( QVariant::fromValue<ActionData>( ActionData( layer ) ) );
409  connect( allAction, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
410  layerMenu->addAction( allAction );
411  }
412 
413  // add custom/layer actions
414  Q_FOREACH ( QgsMapLayerAction* mapLayerAction, layerActions )
415  {
416  QString title = mapLayerAction->text();
417  if ( mapLayerAction->targets().testFlag( QgsMapLayerAction::MultipleFeatures ) )
418  title.append( QString( " (%1)" ).arg( results.count() ) );
419  QAction* action = new QAction( mapLayerAction->icon(), title, layerMenu );
420  action->setData( QVariant::fromValue<ActionData>( ActionData( layer, mapLayerAction ) ) );
421  connect( action, SIGNAL( hovered() ), this, SLOT( handleMenuHover() ) );
422  connect( action, SIGNAL( triggered() ), this, SLOT( triggerMapLayerAction() ) );
423  layerMenu->addAction( action );
424  if ( separators.contains( mapLayerAction ) )
425  {
426  layerMenu->insertSeparator( action );
427  }
428  }
429 }
430 
431 void QgsIdentifyMenu::triggerMapLayerAction()
432 {
433  QAction* action = qobject_cast<QAction*>( sender() );
434  if ( !action )
435  return;
436  QVariant varData = action->data();
437  if ( !varData.isValid() || !varData.canConvert<ActionData>() )
438  return;
439 
440  ActionData actData = action->data().value<ActionData>();
441 
442  if ( actData.mIsValid && actData.mMapLayerAction )
443  {
444  // layer
445  if ( actData.mMapLayerAction->targets().testFlag( QgsMapLayerAction::Layer ) )
446  {
447  actData.mMapLayerAction->triggerForLayer( actData.mLayer );
448  }
449 
450  // multiples features
451  if ( actData.mMapLayerAction->targets().testFlag( QgsMapLayerAction::MultipleFeatures ) )
452  {
453  QList<QgsFeature> featureList;
454  Q_FOREACH ( QgsMapToolIdentify::IdentifyResult result, mLayerIdResults[actData.mLayer] )
455  {
456  featureList << result.mFeature;
457  }
458  actData.mMapLayerAction->triggerForFeatures( actData.mLayer, featureList );
459  }
460 
461  // single feature
462  if ( actData.mMapLayerAction->targets().testFlag( QgsMapLayerAction::SingleFeature ) )
463  {
464  Q_FOREACH ( QgsMapToolIdentify::IdentifyResult result, mLayerIdResults[actData.mLayer] )
465  {
466  if ( result.mFeature.id() == actData.mFeatureId )
467  {
468  actData.mMapLayerAction->triggerForFeature( actData.mLayer, new QgsFeature( result.mFeature ) );
469  return;
470  }
471  }
472  QgsDebugMsg( QString( "Identify menu: could not retrieve feature for action %1" ).arg( action->text() ) );
473  }
474  }
475 }
476 
477 
478 QList<QgsMapToolIdentify::IdentifyResult> QgsIdentifyMenu::results( QAction* action, bool &externalAction )
479 {
480  QList<QgsMapToolIdentify::IdentifyResult> idResults = QList<QgsMapToolIdentify::IdentifyResult>();
481 
482  externalAction = false;
483 
484  ActionData actData;
485  bool hasData = false;
486 
487  if ( !action )
488  return idResults;
489 
490  QVariant varData = action->data();
491  if ( !varData.isValid() )
492  {
493  QgsDebugMsg( "Identify menu: could not retrieve results from menu entry (invalid data)" );
494  return idResults;
495  }
496 
497  if ( varData.canConvert<ActionData>() )
498  {
499  actData = action->data().value<ActionData>();
500  if ( actData.mIsValid )
501  {
502  externalAction = actData.mIsExternalAction;
503  hasData = true;
504  }
505  }
506 
507  if ( !hasData && varData.canConvert<QgsActionMenu::ActionData>() )
508  {
509  QgsActionMenu::ActionData dataSrc = action->data().value<QgsActionMenu::ActionData>();
510  if ( dataSrc.actionType != QgsActionMenu::Invalid )
511  {
512  externalAction = true;
513  actData = ActionData( dataSrc.mapLayer, dataSrc.featureId );
514  hasData = true;
515  }
516  }
517 
518  if ( !hasData )
519  {
520  QgsDebugMsg( "Identify menu: could not retrieve results from menu entry (no data found)" );
521  return idResults;
522  }
523 
524  // return all results
525  if ( actData.mAllResults )
526  {
527  // this means "All" action was triggered
528  QMapIterator< QgsMapLayer*, QList<QgsMapToolIdentify::IdentifyResult> > it( mLayerIdResults );
529  while ( it.hasNext() )
530  {
531  it.next();
532  idResults << it.value();
533  }
534  return idResults;
535  }
536 
537  if ( !mLayerIdResults.contains( actData.mLayer ) )
538  {
539  QgsDebugMsg( "Identify menu: could not retrieve results from menu entry (layer not found)" );
540  return idResults;
541  }
542 
543  if ( actData.mLevel == LayerLevel )
544  {
545  return mLayerIdResults[actData.mLayer];
546  }
547 
548  if ( actData.mLevel == FeatureLevel )
549  {
550  Q_FOREACH ( QgsMapToolIdentify::IdentifyResult res, mLayerIdResults[actData.mLayer] )
551  {
552  if ( res.mFeature.id() == actData.mFeatureId )
553  {
554  idResults << res;
555  return idResults;
556  }
557  }
558  }
559 
560  QgsDebugMsg( "Identify menu: could not retrieve results from menu entry (don't know what happened')" );
561  return idResults;
562 }
563 
564 void QgsIdentifyMenu::handleMenuHover()
565 {
566  if ( !mCanvas )
567  return;
568 
569  deleteRubberBands();
570 
571  QAction* senderAction = qobject_cast<QAction*>( sender() );
572  if ( !senderAction )
573  return;
574 
575  bool externalAction;
576  QList<QgsMapToolIdentify::IdentifyResult> idResults = results( senderAction, externalAction );
577 
578  Q_FOREACH ( const QgsMapToolIdentify::IdentifyResult result, idResults )
579  {
580  QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( result.mLayer );
581  if ( !vl )
582  continue;
583 
584  QgsHighlight *hl = new QgsHighlight( mCanvas, result.mFeature.geometry(), vl );
585  QSettings settings;
586  QColor color = QColor( settings.value( "/Map/highlight/color", QGis::DEFAULT_HIGHLIGHT_COLOR.name() ).toString() );
587  int alpha = settings.value( "/Map/highlight/colorAlpha", QGis::DEFAULT_HIGHLIGHT_COLOR.alpha() ).toInt();
588  double buffer = settings.value( "/Map/highlight/buffer", QGis::DEFAULT_HIGHLIGHT_BUFFER_MM ).toDouble();
589  double minWidth = settings.value( "/Map/highlight/minWidth", QGis::DEFAULT_HIGHLIGHT_MIN_WIDTH_MM ).toDouble();
590  hl->setColor( color ); // sets also fill with default alpha
591  color.setAlpha( alpha );
592  hl->setFillColor( color ); // sets fill with alpha
593  hl->setBuffer( buffer );
594  hl->setMinWidth( minWidth );
595  mRubberBands.append( hl );
596  connect( vl, SIGNAL( destroyed() ), this, SLOT( layerDestroyed() ) );
597  }
598 }
599 
600 void QgsIdentifyMenu::deleteRubberBands()
601 {
602  QList<QgsHighlight*>::const_iterator it = mRubberBands.constBegin();
603  for ( ; it != mRubberBands.constEnd(); ++it )
604  delete *it;
605  mRubberBands.clear();
606 }
607 
608 void QgsIdentifyMenu::layerDestroyed()
609 {
610  QList<QgsHighlight*>::iterator it = mRubberBands.begin();
611  while ( it != mRubberBands.end() )
612  {
613  if (( *it )->layer() == sender() )
614  {
615  delete *it;
616  it = mRubberBands.erase( it );
617  }
618  else
619  {
620  ++it;
621  }
622  }
623 }
624 
626 {
627  mCustomActionRegistry.clear();
628 
629 }