Skip to content

Commit 4f905cc

Browse files
committedFeb 5, 2019
[vertex tool] right-click to loop through editable features
Until now mouse right-click could only select and deselect the highlighted feature to "lock" vertex tool (and numerical editor) to it, so that it is easier to focus only on editing of the particular feature. It was however still difficult to pick the right feature in case there were multiple features in one location or very close to each other. This is now solved by the fact that repeated right button clicks will loop through the editable features. So if there are two features in one location (A, B) then repeated right-clicks will select: A - B - nothing - A - B - nothing ...
1 parent c62a4ae commit 4f905cc

File tree

7 files changed

+296
-30
lines changed

7 files changed

+296
-30
lines changed
 

‎python/core/auto_generated/qgspointlocator.sip.in

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ Optional filter may discard unwanted matches.
200200
Override of edgesInRect that construct rectangle from a center point and tolerance
201201
%End
202202

203+
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0 );
204+
%Docstring
205+
Find vertices within a specified recangle
206+
Optional filter may discard unwanted matches.
207+
208+
.. versionadded:: 3.6
209+
%End
210+
211+
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
212+
%Docstring
213+
Override of verticesInRect that construct rectangle from a center point and tolerance
214+
215+
.. versionadded:: 3.6
216+
%End
217+
203218

204219
MatchList pointInPolygon( const QgsPointXY &point );
205220
%Docstring

‎src/app/vertextool/qgsvertexeditor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ QgsVertexEditor::QgsVertexEditor( QgsMapCanvas *canvas )
306306
layout->setContentsMargins( 0, 0, 0, 0 );
307307

308308
mHintLabel = new QLabel( this );
309-
mHintLabel->setText( QStringLiteral( "%1\n\n%2" ).arg( tr( "Right click on the edge of an editable feature to show its table of vertices." ),
309+
mHintLabel->setText( QStringLiteral( "%1\n\n%2" ).arg( tr( "Right click on an editable feature to show its table of vertices." ),
310310
tr( "When a feature is bound to this panel, dragging a rectangle to select vertices on the canvas will only select those of the bound feature." ) ) );
311311
mHintLabel->setWordWrap( true );
312312
mHintLabel->setAlignment( Qt::AlignHCenter | Qt::AlignVCenter );

‎src/app/vertextool/qgsvertextool.cpp

Lines changed: 141 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -457,33 +457,6 @@ void QgsVertexTool::cadCanvasPressEvent( QgsMapMouseEvent *e )
457457
if ( !mDraggingVertex && !mDraggingEdge )
458458
mSelectionRectStartPos.reset( new QPoint( e->pos() ) );
459459
}
460-
461-
if ( e->button() == Qt::RightButton )
462-
{
463-
if ( !mSelectionRect && !mDraggingVertex && !mDraggingEdge )
464-
{
465-
QgsPointLocator::Match m = snapToEditableLayer( e );
466-
if ( !m.isValid() )
467-
{
468-
// as the last resort check if we are on top of a feature if there is no vertex or edge snap
469-
m = snapToPolygonInterior( e );
470-
}
471-
472-
if ( m.isValid() && m.layer() )
473-
{
474-
updateVertexEditor( m.layer(), m.featureId() );
475-
}
476-
else
477-
{
478-
// there's really nothing under the cursor - let's deselect any feature we may have
479-
mSelectedFeature.reset();
480-
if ( mVertexEditor )
481-
{
482-
mVertexEditor->updateEditor( nullptr );
483-
}
484-
}
485-
}
486-
}
487460
}
488461

489462
void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
@@ -593,8 +566,17 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
593566
}
594567
else if ( e->button() == Qt::RightButton )
595568
{
596-
// cancel action
597-
stopDragging();
569+
if ( mDraggingVertex || mDraggingEdge )
570+
{
571+
// cancel action
572+
stopDragging();
573+
}
574+
else if ( !mSelectionRect )
575+
{
576+
// Right-click to select/delect a feature for editing (also gets selected in vertex editor).
577+
// If there are multiple features at one location, cycle through them with subsequent right clicks.
578+
tryToSelectFeature( e );
579+
}
598580
}
599581
}
600582

@@ -603,6 +585,13 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
603585

604586
void QgsVertexTool::cadCanvasMoveEvent( QgsMapMouseEvent *e )
605587
{
588+
if ( mSelectedFeatureAlternatives && ( e->pos() - mSelectedFeatureAlternatives->screenPoint ).manhattanLength() >= QApplication::startDragDistance() )
589+
{
590+
// as soon as the mouse moves more than just a tiny bit, previously stored alternatives info
591+
// is probably not valid anymore and will need to be re-calculated
592+
mSelectedFeatureAlternatives.reset();
593+
}
594+
606595
if ( mSelectionMethod == SelectionRange )
607596
{
608597
rangeMethodMoveEvent( e );
@@ -684,6 +673,9 @@ void QgsVertexTool::mouseMoveDraggingEdge( QgsMapMouseEvent *e )
684673

685674
void QgsVertexTool::canvasDoubleClickEvent( QgsMapMouseEvent *e )
686675
{
676+
if ( e->button() != Qt::LeftButton )
677+
return;
678+
687679
QgsPointLocator::Match m = snapToEditableLayer( e );
688680
if ( !m.hasEdge() )
689681
return;
@@ -852,6 +844,126 @@ QgsPointLocator::Match QgsVertexTool::snapToPolygonInterior( QgsMapMouseEvent *e
852844
}
853845

854846

847+
QList<QgsPointLocator::Match> QgsVertexTool::findEditableLayerMatches( const QgsPointXY &mapPoint, QgsVectorLayer *layer )
848+
{
849+
QgsPointLocator::MatchList matchList;
850+
851+
if ( !layer->isEditable() )
852+
return matchList;
853+
854+
QgsSnappingUtils *snapUtils = canvas()->snappingUtils();
855+
QgsPointLocator *locator = snapUtils->locatorForLayer( layer );
856+
857+
if ( layer->geometryType() == QgsWkbTypes::PolygonGeometry )
858+
{
859+
matchList << locator->pointInPolygon( mapPoint );
860+
}
861+
862+
double tolerance = QgsTolerance::vertexSearchRadius( canvas()->mapSettings() );
863+
matchList << locator->edgesInRect( mapPoint, tolerance );
864+
matchList << locator->verticesInRect( mapPoint, tolerance );
865+
866+
return matchList;
867+
}
868+
869+
870+
QSet<QPair<QgsVectorLayer *, QgsFeatureId> > QgsVertexTool::findAllEditableFeatures( const QgsPointXY &mapPoint )
871+
{
872+
QSet< QPair<QgsVectorLayer *, QgsFeatureId> > alternatives;
873+
874+
// if there is a current layer, it should have priority over other layers
875+
// because sometimes there may be match from multiple layers at one location
876+
// and selecting current layer is an easy way for the user to prioritize a layer
877+
if ( QgsVectorLayer *currentVlayer = currentVectorLayer() )
878+
{
879+
for ( const QgsPointLocator::Match &m : findEditableLayerMatches( mapPoint, currentVlayer ) )
880+
{
881+
alternatives.insert( qMakePair( m.layer(), m.featureId() ) );
882+
}
883+
}
884+
885+
if ( mMode == AllLayers )
886+
{
887+
const auto layers = canvas()->layers();
888+
for ( QgsMapLayer *layer : layers )
889+
{
890+
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
891+
if ( !vlayer )
892+
continue;
893+
894+
for ( const QgsPointLocator::Match &m : findEditableLayerMatches( mapPoint, vlayer ) )
895+
{
896+
alternatives.insert( qMakePair( m.layer(), m.featureId() ) );
897+
}
898+
}
899+
}
900+
901+
return alternatives;
902+
}
903+
904+
905+
void QgsVertexTool::tryToSelectFeature( QgsMapMouseEvent *e )
906+
{
907+
if ( !mSelectedFeatureAlternatives )
908+
{
909+
// this is the first right-click on this location so we currently do not have information
910+
// about editable features at this mouse location - let's build the alternatives info
911+
QSet< QPair<QgsVectorLayer *, QgsFeatureId> > alternatives = findAllEditableFeatures( toMapCoordinates( e->pos() ) );
912+
if ( !alternatives.isEmpty() )
913+
{
914+
QgsPointLocator::Match m = snapToEditableLayer( e );
915+
if ( !m.isValid() )
916+
{
917+
// as the last resort check if we are on top of a feature if there is no vertex or edge snap
918+
m = snapToPolygonInterior( e );
919+
}
920+
921+
mSelectedFeatureAlternatives.reset( new SelectedFeatureAlternatives );
922+
mSelectedFeatureAlternatives->screenPoint = e->pos();
923+
mSelectedFeatureAlternatives->index = 0;
924+
if ( m.isValid() )
925+
{
926+
// ideally the feature that would get normally highlighted should be also the first choice
927+
// because as user moves mouse, different features are highlighted, so the highlighted feature
928+
// should be first to get selected
929+
QPair<QgsVectorLayer *, QgsFeatureId> firstChoice( m.layer(), m.featureId() );
930+
mSelectedFeatureAlternatives->alternatives.append( firstChoice );
931+
alternatives.remove( firstChoice );
932+
}
933+
mSelectedFeatureAlternatives->alternatives.append( alternatives.toList() );
934+
}
935+
}
936+
else
937+
{
938+
// we have had right-click before on this mouse location - so let's just cycle in our alternatives
939+
// move to the next alternative
940+
if ( mSelectedFeatureAlternatives->index < mSelectedFeatureAlternatives->alternatives.count() - 1 )
941+
++mSelectedFeatureAlternatives->index;
942+
else
943+
mSelectedFeatureAlternatives->index = -1;
944+
}
945+
946+
if ( mSelectedFeatureAlternatives && mSelectedFeatureAlternatives->index != -1 )
947+
{
948+
// we have a feature to select
949+
QPair<QgsVectorLayer *, QgsFeatureId> alternative = mSelectedFeatureAlternatives->alternatives.at( mSelectedFeatureAlternatives->index );
950+
updateVertexEditor( alternative.first, alternative.second );
951+
updateFeatureBand( QgsPointLocator::Match( QgsPointLocator::Area, alternative.first, alternative.second, 0, QgsPointXY() ) );
952+
}
953+
else
954+
{
955+
// there's really nothing under the cursor or while cycling through the list of available features
956+
// we got to the end of the list - let's deselect any feature we may have had selected
957+
mSelectedFeature.reset();
958+
if ( mVertexEditor )
959+
{
960+
mVertexEditor->updateEditor( nullptr );
961+
}
962+
updateFeatureBand( QgsPointLocator::Match() );
963+
}
964+
}
965+
966+
855967
bool QgsVertexTool::isNearEndpointMarker( const QgsPointXY &mapPoint )
856968
{
857969
if ( !mEndpointMarkerCenter )

‎src/app/vertextool/qgsvertextool.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,32 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
146146
*/
147147
QgsPointLocator::Match snapToEditableLayer( QgsMapMouseEvent *e );
148148

149+
/**
150+
* Tries to find a match in polygon interiors. This is useful for mouse move
151+
* events to keep features highlighted to see their area.
152+
*/
149153
QgsPointLocator::Match snapToPolygonInterior( QgsMapMouseEvent *e );
150154

155+
/**
156+
* Returns a list of all matches at the given map point. That is a concatenation
157+
* of all vertex, edge and area matches (vertex/edge matches using standard search tolerance).
158+
* Layer is only searched if it is editable.
159+
*/
160+
QList<QgsPointLocator::Match> findEditableLayerMatches( const QgsPointXY &mapPoint, QgsVectorLayer *layer );
161+
162+
/**
163+
* Returns a set of all matches at the given map point from all editable layers (respecting the mode).
164+
* The set does not contain only the closest match from each layer, but all matches in the standard
165+
* vertex search tolerance. It also includes area matches.
166+
*/
167+
QSet<QPair<QgsVectorLayer *, QgsFeatureId> > findAllEditableFeatures( const QgsPointXY &mapPoint );
168+
169+
/**
170+
* Implements behavior for mouse right-click to select a feature for editing (and in case of multiple
171+
* features in one place, repeated right-clicks will cycle through the features).
172+
*/
173+
void tryToSelectFeature( QgsMapMouseEvent *e );
174+
151175
//! check whether we are still close to the mEndpointMarker
152176
bool isNearEndpointMarker( const QgsPointXY &mapPoint );
153177

@@ -416,6 +440,21 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
416440
//! Dock widget which allows editing vertices
417441
std::unique_ptr<QgsVertexEditor> mVertexEditor;
418442

443+
/**
444+
* Data structure that stores alternative features to the currently selected (locked) feature.
445+
* This is used when user clicks with right mouse button multiple times in one location
446+
* to easily switch to the desired feature.
447+
*/
448+
struct SelectedFeatureAlternatives
449+
{
450+
QPoint screenPoint;
451+
QList< QPair<QgsVectorLayer *, QgsFeatureId> > alternatives;
452+
int index = -1;
453+
};
454+
455+
//! Keeps information about other possible features to select with right click. Null if no info is currently held.
456+
std::unique_ptr<SelectedFeatureAlternatives> mSelectedFeatureAlternatives;
457+
419458
// support for validation of geometries
420459

421460
//! data structure for validation of one geometry of a vector layer

‎src/core/qgspointlocator.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,52 @@ class QgsPointLocator_VisitorEdgesInRect : public IVisitor
564564
QgsPointLocator::MatchFilter *mFilter = nullptr;
565565
};
566566

567+
////////////////////////////////////////////////////////////////////////////
568+
569+
/**
570+
* \ingroup core
571+
* Helper class used when traversing the index looking for vertices - builds a list of matches.
572+
* \note not available in Python bindings
573+
*/
574+
class QgsPointLocator_VisitorVerticesInRect : public IVisitor
575+
{
576+
public:
577+
QgsPointLocator_VisitorVerticesInRect( QgsPointLocator *pl, QgsPointLocator::MatchList &lst, const QgsRectangle &srcRect, QgsPointLocator::MatchFilter *filter = nullptr )
578+
: mLocator( pl )
579+
, mList( lst )
580+
, mSrcRect( srcRect )
581+
, mFilter( filter )
582+
{}
583+
584+
void visitNode( const INode &n ) override { Q_UNUSED( n ); }
585+
void visitData( std::vector<const IData *> &v ) override { Q_UNUSED( v ); }
586+
587+
void visitData( const IData &d ) override
588+
{
589+
QgsFeatureId id = d.getIdentifier();
590+
const QgsGeometry *geom = mLocator->mGeoms.value( id );
591+
592+
for ( QgsAbstractGeometry::vertex_iterator it = geom->vertices_begin(); it != geom->vertices_end(); ++it )
593+
{
594+
if ( mSrcRect.contains( *it ) )
595+
{
596+
QgsPointLocator::Match m( QgsPointLocator::Vertex, mLocator->mLayer, id, 0, *it, geom->vertexNrFromVertexId( it.vertexId() ) );
597+
598+
// in range queries the filter may reject some matches
599+
if ( mFilter && !mFilter->acceptMatch( m ) )
600+
continue;
601+
602+
mList << m;
603+
}
604+
}
605+
}
606+
607+
private:
608+
QgsPointLocator *mLocator = nullptr;
609+
QgsPointLocator::MatchList &mList;
610+
QgsRectangle mSrcRect;
611+
QgsPointLocator::MatchFilter *mFilter = nullptr;
612+
};
567613

568614

569615
////////////////////////////////////////////////////////////////////////////
@@ -1020,6 +1066,28 @@ QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsPointXY &point
10201066
return edgesInRect( rect, filter );
10211067
}
10221068

1069+
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter )
1070+
{
1071+
if ( !mRTree )
1072+
{
1073+
init();
1074+
if ( !mRTree ) // still invalid?
1075+
return MatchList();
1076+
}
1077+
1078+
MatchList lst;
1079+
QgsPointLocator_VisitorVerticesInRect visitor( this, lst, rect, filter );
1080+
mRTree->intersectsWithQuery( rect2region( rect ), visitor );
1081+
1082+
return lst;
1083+
}
1084+
1085+
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter )
1086+
{
1087+
QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance );
1088+
return verticesInRect( rect, filter );
1089+
}
1090+
10231091

10241092
QgsPointLocator::MatchList QgsPointLocator::pointInPolygon( const QgsPointXY &point )
10251093
{

‎src/core/qgspointlocator.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,19 @@ class CORE_EXPORT QgsPointLocator : public QObject
256256
//! Override of edgesInRect that construct rectangle from a center point and tolerance
257257
MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
258258

259+
/**
260+
* Find vertices within a specified recangle
261+
* Optional filter may discard unwanted matches.
262+
* \since QGIS 3.6
263+
*/
264+
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr );
265+
266+
/**
267+
* Override of verticesInRect that construct rectangle from a center point and tolerance
268+
* \since QGIS 3.6
269+
*/
270+
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
271+
259272
// point-in-polygon query
260273

261274
// TODO: function to return just the first match?
@@ -300,6 +313,7 @@ class CORE_EXPORT QgsPointLocator : public QObject
300313
friend class QgsPointLocator_VisitorNearestEdge;
301314
friend class QgsPointLocator_VisitorArea;
302315
friend class QgsPointLocator_VisitorEdgesInRect;
316+
friend class QgsPointLocator_VisitorVerticesInRect;
303317
};
304318

305319

‎tests/src/core/testqgspointlocator.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,24 @@ class TestQgsPointLocator : public QObject
210210
QCOMPARE( lst3.count(), 2 );
211211
}
212212

213+
void testVerticesInTolerance()
214+
{
215+
QgsPointLocator loc( mVL );
216+
QgsPointLocator::MatchList lst = loc.verticesInRect( QgsPointXY( 0, 2 ), 0.5 );
217+
QCOMPARE( lst.count(), 0 );
218+
219+
QgsPointLocator::MatchList lst2 = loc.verticesInRect( QgsPointXY( 0, 1.5 ), 0.5 );
220+
QCOMPARE( lst2.count(), 2 ); // one matching point, but it is the first point in ring, so it is duplicated
221+
QCOMPARE( lst2[0].vertexIndex(), 0 );
222+
QCOMPARE( lst2[1].vertexIndex(), 3 );
223+
224+
QgsPointLocator::MatchList lst3 = loc.verticesInRect( QgsPointXY( 0, 1.5 ), 1 );
225+
QCOMPARE( lst3.count(), 3 );
226+
QCOMPARE( lst3[0].vertexIndex(), 0 );
227+
QCOMPARE( lst3[1].vertexIndex(), 2 );
228+
QCOMPARE( lst3[2].vertexIndex(), 3 );
229+
}
230+
213231

214232
void testLayerUpdates()
215233
{

0 commit comments

Comments
 (0)
Please sign in to comment.