Skip to content

Commit

Permalink
[FEATURE] Improved map select tool behaviour
Browse files Browse the repository at this point in the history
Implements the improved mouse/key modifier behaviour discussed in:
http://osgeo-org.1560.x6.nabble.com/Key-modifiers-with-selection-tc5239653.html

Specifically,

For click-and-drag selections:
- holding shift = add to selection
- holding ctrl = substract from selection
- holding ctrl+shift = intersect with current selection
- holding alt (can be used with shift/ctrl too) = change from
"intersects" to "fully contains" selection mode

For single-click selections:
- holding shift or ctrl = toggle whether feature is selected
(ie either add to current selection or remove from current
selection)

This brings the canvas behaviour into line with other design apps
and also with the composer behaviour.

(fix #2666)
  • Loading branch information
nyalldawson committed May 19, 2016
1 parent c0799d4 commit cd9f47a
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 74 deletions.
3 changes: 1 addition & 2 deletions src/app/qgsmaptoolselect.cpp
Expand Up @@ -50,8 +50,7 @@ void QgsMapToolSelect::canvasReleaseEvent( QgsMapMouseEvent* e )
QgsMapToolSelectUtils::expandSelectRectangle( selectRect, vlayer, e->pos() );
QgsMapToolSelectUtils::setRubberBand( mCanvas, selectRect, &rubberBand );
QgsGeometry* selectGeom = rubberBand.asGeometry();
bool doDifference = e->modifiers() & Qt::ControlModifier;
QgsMapToolSelectUtils::setSelectFeatures( mCanvas, selectGeom, false, doDifference, true );
QgsMapToolSelectUtils::selectSingleFeature( mCanvas, selectGeom, e );
delete selectGeom;
rubberBand.reset( QGis::Polygon );
}
7 changes: 4 additions & 3 deletions src/app/qgsmaptoolselectfreehand.cpp
Expand Up @@ -85,9 +85,10 @@ void QgsMapToolSelectFreehand::canvasReleaseEvent( QgsMapMouseEvent* e )
if ( mRubberBand->numberOfVertices() > 2 )
{
QgsGeometry* shapeGeom = mRubberBand->asGeometry();
QgsMapToolSelectUtils::setSelectFeatures( mCanvas, shapeGeom,
e->modifiers() & Qt::ShiftModifier,
e->modifiers() & Qt::ControlModifier, singleSelect );
if ( singleSelect )
QgsMapToolSelectUtils::selectSingleFeature( mCanvas, shapeGeom, e );
else
QgsMapToolSelectUtils::selectMultipleFeatures( mCanvas, shapeGeom, e );
delete shapeGeom;
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/qgsmaptoolselectpolygon.cpp
Expand Up @@ -54,7 +54,7 @@ void QgsMapToolSelectPolygon::canvasPressEvent( QgsMapMouseEvent* e )
if ( mRubberBand->numberOfVertices() > 2 )
{
QgsGeometry* polygonGeom = mRubberBand->asGeometry();
QgsMapToolSelectUtils::setSelectFeatures( mCanvas, polygonGeom, e );
QgsMapToolSelectUtils::selectMultipleFeatures( mCanvas, polygonGeom, e );
delete polygonGeom;
}
mRubberBand->reset( QGis::Polygon );
Expand Down
2 changes: 1 addition & 1 deletion src/app/qgsmaptoolselectradius.cpp
Expand Up @@ -92,7 +92,7 @@ void QgsMapToolSelectRadius::canvasReleaseEvent( QgsMapMouseEvent* e )
setRadiusRubberBand( radiusEdge );
}
QgsGeometry* radiusGeometry = mRubberBand->asGeometry();
QgsMapToolSelectUtils::setSelectFeatures( mCanvas, radiusGeometry, e );
QgsMapToolSelectUtils::selectMultipleFeatures( mCanvas, radiusGeometry, e );
delete radiusGeometry;
mRubberBand->reset( QGis::Polygon );
delete mRubberBand;
Expand Down
5 changes: 2 additions & 3 deletions src/app/qgsmaptoolselectrectangle.cpp
Expand Up @@ -106,11 +106,10 @@ void QgsMapToolSelectFeatures::canvasReleaseEvent( QgsMapMouseEvent* e )
QgsGeometry* selectGeom = mRubberBand->asGeometry();
if ( !mDragging )
{
bool doDifference = e->modifiers() & Qt::ControlModifier;
QgsMapToolSelectUtils::setSelectFeatures( mCanvas, selectGeom, false, doDifference, true );
QgsMapToolSelectUtils::selectSingleFeature( mCanvas, selectGeom, e );
}
else
QgsMapToolSelectUtils::setSelectFeatures( mCanvas, selectGeom, e );
QgsMapToolSelectUtils::selectMultipleFeatures( mCanvas, selectGeom, e );

delete selectGeom;

Expand Down
122 changes: 73 additions & 49 deletions src/app/qgsmaptoolselectutils.cpp
Expand Up @@ -85,19 +85,80 @@ void QgsMapToolSelectUtils::expandSelectRectangle( QRect& selectRect,
selectRect.setBottom( point.y() + boxSize );
}

void QgsMapToolSelectUtils::setSelectFeatures( QgsMapCanvas* canvas,
QgsGeometry* selectGeometry,
bool doContains,
bool doDifference,
bool singleSelect )
void QgsMapToolSelectUtils::selectMultipleFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, QMouseEvent* e )
{
if ( selectGeometry->type() != QGis::Polygon )
QgsVectorLayer::SelectBehaviour behaviour = QgsVectorLayer::SetSelection;
if ( e->modifiers() & Qt::ShiftModifier && e->modifiers() & Qt::ControlModifier )
behaviour = QgsVectorLayer::IntersectSelection;
else if ( e->modifiers() & Qt::ShiftModifier )
behaviour = QgsVectorLayer::AddToSelection;
else if ( e->modifiers() & Qt::ControlModifier )
behaviour = QgsVectorLayer::RemoveFromSelection;

bool doContains = e->modifiers() & Qt::AltModifier;
setSelectedFeatures( canvas, selectGeometry, behaviour, doContains );
}

void QgsMapToolSelectUtils::selectSingleFeature( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, QMouseEvent* e )
{
QgsVectorLayer* vlayer = QgsMapToolSelectUtils::getCurrentVectorLayer( canvas );
if ( !vlayer )
return;

QApplication::setOverrideCursor( Qt::WaitCursor );

QgsFeatureIds selectedFeatures = getMatchingFeatures( canvas, selectGeometry, false, true );
if ( selectedFeatures.isEmpty() )
{
QApplication::restoreOverrideCursor();
return;
}

QgsVectorLayer::SelectBehaviour behaviour = QgsVectorLayer::SetSelection;

//either shift or control modifier switches to "toggle" selection mode
if ( e->modifiers() & Qt::ShiftModifier || e->modifiers() & Qt::ControlModifier )
{
QgsFeatureId selectId = *selectedFeatures.constBegin();
QgsFeatureIds layerSelectedFeatures = vlayer->selectedFeaturesIds();
if ( layerSelectedFeatures.contains( selectId ) )
behaviour = QgsVectorLayer::RemoveFromSelection;
else
behaviour = QgsVectorLayer::AddToSelection;
}

vlayer->selectByIds( selectedFeatures, behaviour );

QApplication::restoreOverrideCursor();
}

void QgsMapToolSelectUtils::setSelectedFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry,
QgsVectorLayer::SelectBehaviour selectBehaviour, bool doContains, bool singleSelect )
{
QgsVectorLayer* vlayer = QgsMapToolSelectUtils::getCurrentVectorLayer( canvas );
if ( !vlayer )
return;

QApplication::setOverrideCursor( Qt::WaitCursor );

QgsFeatureIds selectedFeatures = getMatchingFeatures( canvas, selectGeometry, doContains, singleSelect );
vlayer->selectByIds( selectedFeatures, selectBehaviour );

QApplication::restoreOverrideCursor();
}


QgsFeatureIds QgsMapToolSelectUtils::getMatchingFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, bool doContains, bool singleSelect )
{
QgsFeatureIds newSelectedFeatures;

if ( selectGeometry->type() != QGis::Polygon )
return newSelectedFeatures;

QgsVectorLayer* vlayer = QgsMapToolSelectUtils::getCurrentVectorLayer( canvas );
if ( !vlayer )
return newSelectedFeatures;

// toLayerCoordinates will throw an exception for any 'invalid' points in
// the rubber band.
// For example, if you project a world map onto a globe using EPSG 2163
Expand All @@ -121,16 +182,13 @@ void QgsMapToolSelectUtils::setSelectFeatures( QgsMapCanvas* canvas,
QObject::tr( "Selection extends beyond layer's coordinate system" ),
QgsMessageBar::WARNING,
QgisApp::instance()->messageTimeout() );
return;
return newSelectedFeatures;
}
}

QApplication::setOverrideCursor( Qt::WaitCursor );

QgsDebugMsg( "Selection layer: " + vlayer->name() );
QgsDebugMsg( "Selection polygon: " + selectGeomTrans.exportToWkt() );
QgsDebugMsg( "doContains: " + QString( doContains ? "T" : "F" ) );
QgsDebugMsg( "doDifference: " + QString( doDifference ? "T" : "F" ) );
QgsDebugMsgLevel( "Selection layer: " + vlayer->name(), 3 );
QgsDebugMsgLevel( "Selection polygon: " + selectGeomTrans.exportToWkt(), 3 );
QgsDebugMsgLevel( "doContains: " + QString( doContains ? "T" : "F" ), 3 );

QgsRenderContext context = QgsRenderContext::fromMapSettings( canvas->mapSettings() );
context.expressionContext() << QgsExpressionContextUtils::layerScope( vlayer );
Expand All @@ -148,7 +206,6 @@ void QgsMapToolSelectUtils::setSelectFeatures( QgsMapCanvas* canvas,

QgsFeatureIterator fit = vlayer->getFeatures( request );

QgsFeatureIds newSelectedFeatures;
QgsFeature f;
QgsFeatureId closestFeatureId = 0;
bool foundSingleFeature = false;
Expand Down Expand Up @@ -196,40 +253,7 @@ void QgsMapToolSelectUtils::setSelectFeatures( QgsMapCanvas* canvas,

QgsDebugMsg( "Number of new selected features: " + QString::number( newSelectedFeatures.size() ) );

if ( doDifference )
{
QgsFeatureIds layerSelectedFeatures = vlayer->selectedFeaturesIds();

QgsFeatureIds selectedFeatures;
QgsFeatureIds deselectedFeatures;

QgsFeatureIds::const_iterator i = newSelectedFeatures.constEnd();
while ( i != newSelectedFeatures.constBegin() )
{
--i;
if ( layerSelectedFeatures.contains( *i ) )
{
deselectedFeatures.insert( *i );
}
else
{
selectedFeatures.insert( *i );
}
}

vlayer->modifySelection( selectedFeatures, deselectedFeatures );
}
else
{
vlayer->selectByIds( newSelectedFeatures );
}

QApplication::restoreOverrideCursor();
return newSelectedFeatures;
}

void QgsMapToolSelectUtils::setSelectFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, QMouseEvent * e )
{
bool doContains = e->modifiers() & Qt::ShiftModifier;
bool doDifference = e->modifiers() & Qt::ControlModifier;
setSelectFeatures( canvas, selectGeometry, doContains, doDifference );
}

58 changes: 43 additions & 15 deletions src/app/qgsmaptoolselectutils.h
Expand Up @@ -16,6 +16,7 @@ email : jpalmer at linz dot govt dot nz
#ifndef QGSMAPTOOLSELECTUTILS_H
#define QGSMAPTOOLSELECTUTILS_H

#include "qgsvectorlayer.h"
#include <Qt>
#include <QRect>
#include <QPoint>
Expand All @@ -31,34 +32,61 @@ class QgsRubberBand;
*/
namespace QgsMapToolSelectUtils
{
/** Calculates a list of features matching a selection geometry and flags.
* @param canvas the map canvas used to get the current selected vector layer and
for any required geometry transformations
* @param selectGeometry the geometry to select the layers features. This geometry
must be in terms of the canvas coordinate system.
* @param doContains features will only be selected if fully contained within
the selection rubber band (otherwise intersection is enough).
* @param singleSelect only selects the closest feature to the selectGeometry.
* @returns list of features which match search geometry and parameters
* @note added in QGIS 2.16
*/
QgsFeatureIds getMatchingFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, bool doContains, bool singleSelect );

/**
Selects the features within currently selected layer.
@param canvas The map canvas used to get the current selected vector layer and
@param canvas the map canvas used to get the current selected vector layer and
for any required geometry transformations
@param selectGeometry The geometry to select the layers features. This geometry
@param selectGeometry the geometry to select the layers features. This geometry
must be in terms of the canvas coordinate system.
@param doContains Features will only be selected if fully contained within
@param selectBehaviour behaviour of select (ie replace selection, add to selection)
@param doContains features will only be selected if fully contained within
the selection rubber band (otherwise intersection is enough).
@param doDifference Take the symmetric difference of the current selected
features and the new features found within the provided selectGeometry.
@param singleSelect Only selects the closest feature to the selectGeometry.
@param singleSelect only selects the closest feature to the selectGeometry.
@note added in QGIS 2.16
*/
void setSelectedFeatures( QgsMapCanvas* canvas,
QgsGeometry* selectGeometry,
QgsVectorLayer::SelectBehaviour selectBehaviour = QgsVectorLayer::SetSelection,
bool doContains = true,
bool singleSelect = false );

/**
Selects multiple matching features from within currently selected layer.
@param canvas the map canvas used to get the current selected vector layer and
for any required geometry transformations
@param selectGeometry the geometry to select the layers features. This geometry
must be in terms of the canvas coordinate system.
@param e MouseEvents are used to determine the current selection
operations (add, subtract, contains)
@note added in QGIS 2.16
@see selectSingleFeature()
*/
void setSelectFeatures( QgsMapCanvas* canvas,
QgsGeometry* selectGeometry,
bool doContains = true,
bool doDifference = false,
bool singleSelect = false );
void selectMultipleFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, QMouseEvent * e );

/**
Select the features within currently selected layer.
@param canvas The map canvas used to get the current selected vector layer and
Selects a single feature from within currently selected layer.
@param canvas the map canvas used to get the current selected vector layer and
for any required geometry transformations
@param selectGeometry The geometry to select the layers features. This geometry
@param selectGeometry the geometry to select the layers features. This geometry
must be in terms of the canvas coordinate system.
@param e MouseEvents are used to determine the current selection
operations (add, subtract, contains)
@see selectMultipleFeatures()
*/
void setSelectFeatures( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, QMouseEvent * e );
void selectSingleFeature( QgsMapCanvas* canvas, QgsGeometry* selectGeometry, QMouseEvent * e );

/**
Get the current selected canvas map layer. Returns nullptr if it is not a vector layer
Expand Down

0 comments on commit cd9f47a

Please sign in to comment.