mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-14 00:07:35 -04:00
[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 ...
This commit is contained in:
parent
c62a4ae012
commit
4f905cc2e8
@ -200,6 +200,21 @@ Optional filter may discard unwanted matches.
|
||||
Override of edgesInRect that construct rectangle from a center point and tolerance
|
||||
%End
|
||||
|
||||
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0 );
|
||||
%Docstring
|
||||
Find vertices within a specified recangle
|
||||
Optional filter may discard unwanted matches.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
%End
|
||||
|
||||
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
|
||||
%Docstring
|
||||
Override of verticesInRect that construct rectangle from a center point and tolerance
|
||||
|
||||
.. versionadded:: 3.6
|
||||
%End
|
||||
|
||||
|
||||
MatchList pointInPolygon( const QgsPointXY &point );
|
||||
%Docstring
|
||||
|
@ -306,7 +306,7 @@ QgsVertexEditor::QgsVertexEditor( QgsMapCanvas *canvas )
|
||||
layout->setContentsMargins( 0, 0, 0, 0 );
|
||||
|
||||
mHintLabel = new QLabel( this );
|
||||
mHintLabel->setText( QStringLiteral( "%1\n\n%2" ).arg( tr( "Right click on the edge of an editable feature to show its table of vertices." ),
|
||||
mHintLabel->setText( QStringLiteral( "%1\n\n%2" ).arg( tr( "Right click on an editable feature to show its table of vertices." ),
|
||||
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." ) ) );
|
||||
mHintLabel->setWordWrap( true );
|
||||
mHintLabel->setAlignment( Qt::AlignHCenter | Qt::AlignVCenter );
|
||||
|
@ -457,33 +457,6 @@ void QgsVertexTool::cadCanvasPressEvent( QgsMapMouseEvent *e )
|
||||
if ( !mDraggingVertex && !mDraggingEdge )
|
||||
mSelectionRectStartPos.reset( new QPoint( e->pos() ) );
|
||||
}
|
||||
|
||||
if ( e->button() == Qt::RightButton )
|
||||
{
|
||||
if ( !mSelectionRect && !mDraggingVertex && !mDraggingEdge )
|
||||
{
|
||||
QgsPointLocator::Match m = snapToEditableLayer( e );
|
||||
if ( !m.isValid() )
|
||||
{
|
||||
// as the last resort check if we are on top of a feature if there is no vertex or edge snap
|
||||
m = snapToPolygonInterior( e );
|
||||
}
|
||||
|
||||
if ( m.isValid() && m.layer() )
|
||||
{
|
||||
updateVertexEditor( m.layer(), m.featureId() );
|
||||
}
|
||||
else
|
||||
{
|
||||
// there's really nothing under the cursor - let's deselect any feature we may have
|
||||
mSelectedFeature.reset();
|
||||
if ( mVertexEditor )
|
||||
{
|
||||
mVertexEditor->updateEditor( nullptr );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
|
||||
@ -593,8 +566,17 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
|
||||
}
|
||||
else if ( e->button() == Qt::RightButton )
|
||||
{
|
||||
// cancel action
|
||||
stopDragging();
|
||||
if ( mDraggingVertex || mDraggingEdge )
|
||||
{
|
||||
// cancel action
|
||||
stopDragging();
|
||||
}
|
||||
else if ( !mSelectionRect )
|
||||
{
|
||||
// Right-click to select/delect a feature for editing (also gets selected in vertex editor).
|
||||
// If there are multiple features at one location, cycle through them with subsequent right clicks.
|
||||
tryToSelectFeature( e );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -603,6 +585,13 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
|
||||
|
||||
void QgsVertexTool::cadCanvasMoveEvent( QgsMapMouseEvent *e )
|
||||
{
|
||||
if ( mSelectedFeatureAlternatives && ( e->pos() - mSelectedFeatureAlternatives->screenPoint ).manhattanLength() >= QApplication::startDragDistance() )
|
||||
{
|
||||
// as soon as the mouse moves more than just a tiny bit, previously stored alternatives info
|
||||
// is probably not valid anymore and will need to be re-calculated
|
||||
mSelectedFeatureAlternatives.reset();
|
||||
}
|
||||
|
||||
if ( mSelectionMethod == SelectionRange )
|
||||
{
|
||||
rangeMethodMoveEvent( e );
|
||||
@ -684,6 +673,9 @@ void QgsVertexTool::mouseMoveDraggingEdge( QgsMapMouseEvent *e )
|
||||
|
||||
void QgsVertexTool::canvasDoubleClickEvent( QgsMapMouseEvent *e )
|
||||
{
|
||||
if ( e->button() != Qt::LeftButton )
|
||||
return;
|
||||
|
||||
QgsPointLocator::Match m = snapToEditableLayer( e );
|
||||
if ( !m.hasEdge() )
|
||||
return;
|
||||
@ -852,6 +844,126 @@ QgsPointLocator::Match QgsVertexTool::snapToPolygonInterior( QgsMapMouseEvent *e
|
||||
}
|
||||
|
||||
|
||||
QList<QgsPointLocator::Match> QgsVertexTool::findEditableLayerMatches( const QgsPointXY &mapPoint, QgsVectorLayer *layer )
|
||||
{
|
||||
QgsPointLocator::MatchList matchList;
|
||||
|
||||
if ( !layer->isEditable() )
|
||||
return matchList;
|
||||
|
||||
QgsSnappingUtils *snapUtils = canvas()->snappingUtils();
|
||||
QgsPointLocator *locator = snapUtils->locatorForLayer( layer );
|
||||
|
||||
if ( layer->geometryType() == QgsWkbTypes::PolygonGeometry )
|
||||
{
|
||||
matchList << locator->pointInPolygon( mapPoint );
|
||||
}
|
||||
|
||||
double tolerance = QgsTolerance::vertexSearchRadius( canvas()->mapSettings() );
|
||||
matchList << locator->edgesInRect( mapPoint, tolerance );
|
||||
matchList << locator->verticesInRect( mapPoint, tolerance );
|
||||
|
||||
return matchList;
|
||||
}
|
||||
|
||||
|
||||
QSet<QPair<QgsVectorLayer *, QgsFeatureId> > QgsVertexTool::findAllEditableFeatures( const QgsPointXY &mapPoint )
|
||||
{
|
||||
QSet< QPair<QgsVectorLayer *, QgsFeatureId> > alternatives;
|
||||
|
||||
// if there is a current layer, it should have priority over other layers
|
||||
// because sometimes there may be match from multiple layers at one location
|
||||
// and selecting current layer is an easy way for the user to prioritize a layer
|
||||
if ( QgsVectorLayer *currentVlayer = currentVectorLayer() )
|
||||
{
|
||||
for ( const QgsPointLocator::Match &m : findEditableLayerMatches( mapPoint, currentVlayer ) )
|
||||
{
|
||||
alternatives.insert( qMakePair( m.layer(), m.featureId() ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( mMode == AllLayers )
|
||||
{
|
||||
const auto layers = canvas()->layers();
|
||||
for ( QgsMapLayer *layer : layers )
|
||||
{
|
||||
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
|
||||
if ( !vlayer )
|
||||
continue;
|
||||
|
||||
for ( const QgsPointLocator::Match &m : findEditableLayerMatches( mapPoint, vlayer ) )
|
||||
{
|
||||
alternatives.insert( qMakePair( m.layer(), m.featureId() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
|
||||
void QgsVertexTool::tryToSelectFeature( QgsMapMouseEvent *e )
|
||||
{
|
||||
if ( !mSelectedFeatureAlternatives )
|
||||
{
|
||||
// this is the first right-click on this location so we currently do not have information
|
||||
// about editable features at this mouse location - let's build the alternatives info
|
||||
QSet< QPair<QgsVectorLayer *, QgsFeatureId> > alternatives = findAllEditableFeatures( toMapCoordinates( e->pos() ) );
|
||||
if ( !alternatives.isEmpty() )
|
||||
{
|
||||
QgsPointLocator::Match m = snapToEditableLayer( e );
|
||||
if ( !m.isValid() )
|
||||
{
|
||||
// as the last resort check if we are on top of a feature if there is no vertex or edge snap
|
||||
m = snapToPolygonInterior( e );
|
||||
}
|
||||
|
||||
mSelectedFeatureAlternatives.reset( new SelectedFeatureAlternatives );
|
||||
mSelectedFeatureAlternatives->screenPoint = e->pos();
|
||||
mSelectedFeatureAlternatives->index = 0;
|
||||
if ( m.isValid() )
|
||||
{
|
||||
// ideally the feature that would get normally highlighted should be also the first choice
|
||||
// because as user moves mouse, different features are highlighted, so the highlighted feature
|
||||
// should be first to get selected
|
||||
QPair<QgsVectorLayer *, QgsFeatureId> firstChoice( m.layer(), m.featureId() );
|
||||
mSelectedFeatureAlternatives->alternatives.append( firstChoice );
|
||||
alternatives.remove( firstChoice );
|
||||
}
|
||||
mSelectedFeatureAlternatives->alternatives.append( alternatives.toList() );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// we have had right-click before on this mouse location - so let's just cycle in our alternatives
|
||||
// move to the next alternative
|
||||
if ( mSelectedFeatureAlternatives->index < mSelectedFeatureAlternatives->alternatives.count() - 1 )
|
||||
++mSelectedFeatureAlternatives->index;
|
||||
else
|
||||
mSelectedFeatureAlternatives->index = -1;
|
||||
}
|
||||
|
||||
if ( mSelectedFeatureAlternatives && mSelectedFeatureAlternatives->index != -1 )
|
||||
{
|
||||
// we have a feature to select
|
||||
QPair<QgsVectorLayer *, QgsFeatureId> alternative = mSelectedFeatureAlternatives->alternatives.at( mSelectedFeatureAlternatives->index );
|
||||
updateVertexEditor( alternative.first, alternative.second );
|
||||
updateFeatureBand( QgsPointLocator::Match( QgsPointLocator::Area, alternative.first, alternative.second, 0, QgsPointXY() ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
// there's really nothing under the cursor or while cycling through the list of available features
|
||||
// we got to the end of the list - let's deselect any feature we may have had selected
|
||||
mSelectedFeature.reset();
|
||||
if ( mVertexEditor )
|
||||
{
|
||||
mVertexEditor->updateEditor( nullptr );
|
||||
}
|
||||
updateFeatureBand( QgsPointLocator::Match() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool QgsVertexTool::isNearEndpointMarker( const QgsPointXY &mapPoint )
|
||||
{
|
||||
if ( !mEndpointMarkerCenter )
|
||||
|
@ -146,8 +146,32 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
|
||||
*/
|
||||
QgsPointLocator::Match snapToEditableLayer( QgsMapMouseEvent *e );
|
||||
|
||||
/**
|
||||
* Tries to find a match in polygon interiors. This is useful for mouse move
|
||||
* events to keep features highlighted to see their area.
|
||||
*/
|
||||
QgsPointLocator::Match snapToPolygonInterior( QgsMapMouseEvent *e );
|
||||
|
||||
/**
|
||||
* Returns a list of all matches at the given map point. That is a concatenation
|
||||
* of all vertex, edge and area matches (vertex/edge matches using standard search tolerance).
|
||||
* Layer is only searched if it is editable.
|
||||
*/
|
||||
QList<QgsPointLocator::Match> findEditableLayerMatches( const QgsPointXY &mapPoint, QgsVectorLayer *layer );
|
||||
|
||||
/**
|
||||
* Returns a set of all matches at the given map point from all editable layers (respecting the mode).
|
||||
* The set does not contain only the closest match from each layer, but all matches in the standard
|
||||
* vertex search tolerance. It also includes area matches.
|
||||
*/
|
||||
QSet<QPair<QgsVectorLayer *, QgsFeatureId> > findAllEditableFeatures( const QgsPointXY &mapPoint );
|
||||
|
||||
/**
|
||||
* Implements behavior for mouse right-click to select a feature for editing (and in case of multiple
|
||||
* features in one place, repeated right-clicks will cycle through the features).
|
||||
*/
|
||||
void tryToSelectFeature( QgsMapMouseEvent *e );
|
||||
|
||||
//! check whether we are still close to the mEndpointMarker
|
||||
bool isNearEndpointMarker( const QgsPointXY &mapPoint );
|
||||
|
||||
@ -416,6 +440,21 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
|
||||
//! Dock widget which allows editing vertices
|
||||
std::unique_ptr<QgsVertexEditor> mVertexEditor;
|
||||
|
||||
/**
|
||||
* Data structure that stores alternative features to the currently selected (locked) feature.
|
||||
* This is used when user clicks with right mouse button multiple times in one location
|
||||
* to easily switch to the desired feature.
|
||||
*/
|
||||
struct SelectedFeatureAlternatives
|
||||
{
|
||||
QPoint screenPoint;
|
||||
QList< QPair<QgsVectorLayer *, QgsFeatureId> > alternatives;
|
||||
int index = -1;
|
||||
};
|
||||
|
||||
//! Keeps information about other possible features to select with right click. Null if no info is currently held.
|
||||
std::unique_ptr<SelectedFeatureAlternatives> mSelectedFeatureAlternatives;
|
||||
|
||||
// support for validation of geometries
|
||||
|
||||
//! data structure for validation of one geometry of a vector layer
|
||||
|
@ -564,6 +564,52 @@ class QgsPointLocator_VisitorEdgesInRect : public IVisitor
|
||||
QgsPointLocator::MatchFilter *mFilter = nullptr;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* \ingroup core
|
||||
* Helper class used when traversing the index looking for vertices - builds a list of matches.
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class QgsPointLocator_VisitorVerticesInRect : public IVisitor
|
||||
{
|
||||
public:
|
||||
QgsPointLocator_VisitorVerticesInRect( QgsPointLocator *pl, QgsPointLocator::MatchList &lst, const QgsRectangle &srcRect, QgsPointLocator::MatchFilter *filter = nullptr )
|
||||
: mLocator( pl )
|
||||
, mList( lst )
|
||||
, mSrcRect( srcRect )
|
||||
, mFilter( filter )
|
||||
{}
|
||||
|
||||
void visitNode( const INode &n ) override { Q_UNUSED( n ); }
|
||||
void visitData( std::vector<const IData *> &v ) override { Q_UNUSED( v ); }
|
||||
|
||||
void visitData( const IData &d ) override
|
||||
{
|
||||
QgsFeatureId id = d.getIdentifier();
|
||||
const QgsGeometry *geom = mLocator->mGeoms.value( id );
|
||||
|
||||
for ( QgsAbstractGeometry::vertex_iterator it = geom->vertices_begin(); it != geom->vertices_end(); ++it )
|
||||
{
|
||||
if ( mSrcRect.contains( *it ) )
|
||||
{
|
||||
QgsPointLocator::Match m( QgsPointLocator::Vertex, mLocator->mLayer, id, 0, *it, geom->vertexNrFromVertexId( it.vertexId() ) );
|
||||
|
||||
// in range queries the filter may reject some matches
|
||||
if ( mFilter && !mFilter->acceptMatch( m ) )
|
||||
continue;
|
||||
|
||||
mList << m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QgsPointLocator *mLocator = nullptr;
|
||||
QgsPointLocator::MatchList &mList;
|
||||
QgsRectangle mSrcRect;
|
||||
QgsPointLocator::MatchFilter *mFilter = nullptr;
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@ -1020,6 +1066,28 @@ QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsPointXY &point
|
||||
return edgesInRect( rect, filter );
|
||||
}
|
||||
|
||||
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter )
|
||||
{
|
||||
if ( !mRTree )
|
||||
{
|
||||
init();
|
||||
if ( !mRTree ) // still invalid?
|
||||
return MatchList();
|
||||
}
|
||||
|
||||
MatchList lst;
|
||||
QgsPointLocator_VisitorVerticesInRect visitor( this, lst, rect, filter );
|
||||
mRTree->intersectsWithQuery( rect2region( rect ), visitor );
|
||||
|
||||
return lst;
|
||||
}
|
||||
|
||||
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter )
|
||||
{
|
||||
QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance );
|
||||
return verticesInRect( rect, filter );
|
||||
}
|
||||
|
||||
|
||||
QgsPointLocator::MatchList QgsPointLocator::pointInPolygon( const QgsPointXY &point )
|
||||
{
|
||||
|
@ -256,6 +256,19 @@ class CORE_EXPORT QgsPointLocator : public QObject
|
||||
//! Override of edgesInRect that construct rectangle from a center point and tolerance
|
||||
MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
|
||||
|
||||
/**
|
||||
* Find vertices within a specified recangle
|
||||
* Optional filter may discard unwanted matches.
|
||||
* \since QGIS 3.6
|
||||
*/
|
||||
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr );
|
||||
|
||||
/**
|
||||
* Override of verticesInRect that construct rectangle from a center point and tolerance
|
||||
* \since QGIS 3.6
|
||||
*/
|
||||
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
|
||||
|
||||
// point-in-polygon query
|
||||
|
||||
// TODO: function to return just the first match?
|
||||
@ -300,6 +313,7 @@ class CORE_EXPORT QgsPointLocator : public QObject
|
||||
friend class QgsPointLocator_VisitorNearestEdge;
|
||||
friend class QgsPointLocator_VisitorArea;
|
||||
friend class QgsPointLocator_VisitorEdgesInRect;
|
||||
friend class QgsPointLocator_VisitorVerticesInRect;
|
||||
};
|
||||
|
||||
|
||||
|
@ -210,6 +210,24 @@ class TestQgsPointLocator : public QObject
|
||||
QCOMPARE( lst3.count(), 2 );
|
||||
}
|
||||
|
||||
void testVerticesInTolerance()
|
||||
{
|
||||
QgsPointLocator loc( mVL );
|
||||
QgsPointLocator::MatchList lst = loc.verticesInRect( QgsPointXY( 0, 2 ), 0.5 );
|
||||
QCOMPARE( lst.count(), 0 );
|
||||
|
||||
QgsPointLocator::MatchList lst2 = loc.verticesInRect( QgsPointXY( 0, 1.5 ), 0.5 );
|
||||
QCOMPARE( lst2.count(), 2 ); // one matching point, but it is the first point in ring, so it is duplicated
|
||||
QCOMPARE( lst2[0].vertexIndex(), 0 );
|
||||
QCOMPARE( lst2[1].vertexIndex(), 3 );
|
||||
|
||||
QgsPointLocator::MatchList lst3 = loc.verticesInRect( QgsPointXY( 0, 1.5 ), 1 );
|
||||
QCOMPARE( lst3.count(), 3 );
|
||||
QCOMPARE( lst3[0].vertexIndex(), 0 );
|
||||
QCOMPARE( lst3[1].vertexIndex(), 2 );
|
||||
QCOMPARE( lst3[2].vertexIndex(), 3 );
|
||||
}
|
||||
|
||||
|
||||
void testLayerUpdates()
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user