Skip to content

Commit cc9b5a4

Browse files
committed
[FEATURE] Interactive curve editing for property overrides
This adds a new interactive "curve" to the assistant widgets. It allows you to fine tune exactly how input values get mapped to output sizes/colors/etc. Think GIMP or Photoshop curves, but for your data...
1 parent 45861d3 commit cc9b5a4

8 files changed

+566
-28
lines changed

python/gui/gui.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
%Include qgsconfigureshortcutsdialog.sip
5252
%Include qgscredentialdialog.sip
5353
%Include qgscustomdrophandler.sip
54+
%Include qgscurveeditorwidget.sip
5455
%Include qgsdetaileditemdata.sip
5556
%Include qgsdetaileditemdelegate.sip
5657
%Include qgsdetaileditemwidget.sip

python/gui/qgscurveeditorwidget.sip

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class QgsCurveEditorWidget : QWidget
2+
{
3+
%TypeHeaderCode
4+
#include <qgscurveeditorwidget.h>
5+
%End
6+
7+
public:
8+
9+
QgsCurveEditorWidget( QWidget* parent /TransferThis/ = 0, const QgsCurveTransform& curve = QgsCurveTransform() );
10+
11+
QgsCurveTransform curve() const;
12+
13+
void setCurve( const QgsCurveTransform& curve );
14+
15+
signals:
16+
17+
void changed();
18+
19+
protected:
20+
21+
virtual void keyPressEvent( QKeyEvent *event );
22+
23+
};

src/gui/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ SET(QGIS_GUI_SRCS
193193
qgscredentialdialog.cpp
194194
qgscursors.cpp
195195
qgscustomdrophandler.cpp
196+
qgscurveeditorwidget.cpp
196197
qgsdatumtransformdialog.cpp
197198
qgsdetaileditemdata.cpp
198199
qgsdetaileditemdelegate.cpp
@@ -345,6 +346,7 @@ SET(QGIS_GUI_MOC_HDRS
345346
qgscompoundcolorwidget.h
346347
qgsconfigureshortcutsdialog.h
347348
qgscredentialdialog.h
349+
qgscurveeditorwidget.h
348350
qgsdatumtransformdialog.h
349351
qgsdetaileditemdelegate.h
350352
qgsdetaileditemwidget.h

src/gui/qgscurveeditorwidget.cpp

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/***************************************************************************
2+
qgscurveeditorwidget.cpp
3+
------------------------
4+
begin : February 2017
5+
copyright : (C) 2017 by 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+
17+
#include "qgscurveeditorwidget.h"
18+
19+
#include <qmath.h>
20+
#include <QPainter>
21+
#include <QVBoxLayout>
22+
#include <QMouseEvent>
23+
24+
// QWT Charting widget
25+
#include <qwt_global.h>
26+
#include <qwt_plot_canvas.h>
27+
#include <qwt_plot.h>
28+
#include <qwt_plot_curve.h>
29+
#include <qwt_plot_grid.h>
30+
#include <qwt_plot_marker.h>
31+
#include <qwt_plot_picker.h>
32+
#include <qwt_picker_machine.h>
33+
#include <qwt_plot_layout.h>
34+
#include <qwt_symbol.h>
35+
#include <qwt_legend.h>
36+
37+
QgsCurveEditorWidget::QgsCurveEditorWidget( QWidget* parent, const QgsCurveTransform& transform )
38+
: QWidget( parent )
39+
, mCurve( transform )
40+
, mCurrentPlotMarkerIndex( -1 )
41+
{
42+
mPlot = new QwtPlot();
43+
mPlot->setMinimumSize( QSize( 0, 100 ) );
44+
mPlot->setAxisScale( QwtPlot::yLeft, 0, 1 );
45+
mPlot->setAxisScale( QwtPlot::yRight, 0, 1 );
46+
mPlot->setAxisScale( QwtPlot::xBottom, 0, 1 );
47+
mPlot->setAxisScale( QwtPlot::xTop, 0, 1 );
48+
49+
QVBoxLayout* vlayout = new QVBoxLayout();
50+
vlayout->addWidget( mPlot );
51+
setLayout( vlayout );
52+
53+
// hide the ugly canvas frame
54+
mPlot->setFrameStyle( QFrame::NoFrame );
55+
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
56+
QFrame* plotCanvasFrame = dynamic_cast<QFrame*>( mPlot->canvas() );
57+
if ( plotCanvasFrame )
58+
plotCanvasFrame->setFrameStyle( QFrame::NoFrame );
59+
#else
60+
mPlot->canvas()->setFrameStyle( QFrame::NoFrame );
61+
#endif
62+
63+
mPlot->enableAxis( QwtPlot::yLeft, false );
64+
mPlot->enableAxis( QwtPlot::xBottom, false );
65+
66+
// add a grid
67+
QwtPlotGrid * grid = new QwtPlotGrid();
68+
QwtScaleDiv gridDiv( 0.0, 1.0, QList<double>(), QList<double>(), QList<double>() << 0.2 << 0.4 << 0.6 << 0.8 );
69+
grid->setXDiv( gridDiv );
70+
grid->setYDiv( gridDiv );
71+
grid->setPen( QPen( QColor( 0, 0, 0, 50 ) ) );
72+
grid->attach( mPlot );
73+
74+
mPlotCurve = new QwtPlotCurve();
75+
mPlotCurve->setTitle( QStringLiteral( "Curve" ) );
76+
mPlotCurve->setPen( QPen( QColor( 30, 30, 30 ), 0.0 ) ),
77+
mPlotCurve->setRenderHint( QwtPlotItem::RenderAntialiased, true );
78+
mPlotCurve->attach( mPlot );
79+
80+
mPlotFilter = new QgsCurveEditorPlotEventFilter( mPlot );
81+
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mousePress, this, &QgsCurveEditorWidget::plotMousePress );
82+
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseRelease, this, &QgsCurveEditorWidget::plotMouseRelease );
83+
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseMove, this, &QgsCurveEditorWidget::plotMouseMove );
84+
85+
mPlotCurve->setVisible( true );
86+
updatePlot();
87+
}
88+
89+
void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve )
90+
{
91+
mCurve = curve;
92+
updatePlot();
93+
emit changed();
94+
}
95+
96+
void QgsCurveEditorWidget::keyPressEvent( QKeyEvent* event )
97+
{
98+
if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
99+
{
100+
QList< QgsPoint > cp = mCurve.controlPoints();
101+
if ( mCurrentPlotMarkerIndex > 0 && mCurrentPlotMarkerIndex < cp.count() - 1 )
102+
{
103+
cp.removeAt( mCurrentPlotMarkerIndex );
104+
mCurve.setControlPoints( cp );
105+
updatePlot();
106+
emit changed();
107+
}
108+
}
109+
}
110+
111+
void QgsCurveEditorWidget::plotMousePress( QPointF point )
112+
{
113+
mCurrentPlotMarkerIndex = findNearestControlPoint( point );
114+
if ( mCurrentPlotMarkerIndex < 0 )
115+
{
116+
// add a new point
117+
mCurve.addControlPoint( point.x(), point.y() );
118+
mCurrentPlotMarkerIndex = findNearestControlPoint( point );
119+
emit changed();
120+
}
121+
updatePlot();
122+
}
123+
124+
125+
int QgsCurveEditorWidget::findNearestControlPoint( QPointF point ) const
126+
{
127+
double minDist = 3.0 / mPlot->width();
128+
int currentPlotMarkerIndex = -1;
129+
130+
QList< QgsPoint > controlPoints = mCurve.controlPoints();
131+
132+
for ( int i = 0; i < controlPoints.count(); ++i )
133+
{
134+
QgsPoint currentPoint = controlPoints.at( i );
135+
double currentDist;
136+
currentDist = qPow( point.x() - currentPoint.x(), 2.0 ) + qPow( point.y() - currentPoint.y(), 2.0 );
137+
if ( currentDist < minDist )
138+
{
139+
minDist = currentDist;
140+
currentPlotMarkerIndex = i;
141+
}
142+
}
143+
return currentPlotMarkerIndex;
144+
}
145+
146+
147+
void QgsCurveEditorWidget::plotMouseRelease( QPointF )
148+
{
149+
}
150+
151+
void QgsCurveEditorWidget::plotMouseMove( QPointF point )
152+
{
153+
if ( mCurrentPlotMarkerIndex < 0 )
154+
return;
155+
156+
QList< QgsPoint > cp = mCurve.controlPoints();
157+
bool removePoint = false;
158+
if ( mCurrentPlotMarkerIndex == 0 )
159+
{
160+
point.setX( qMin( point.x(), cp.at( 1 ).x() - 0.01 ) );
161+
}
162+
else
163+
{
164+
removePoint = point.x() <= cp.at( mCurrentPlotMarkerIndex - 1 ).x();
165+
}
166+
if ( mCurrentPlotMarkerIndex == cp.count() - 1 )
167+
{
168+
point.setX( qMax( point.x(), cp.at( mCurrentPlotMarkerIndex - 1 ).x() + 0.01 ) );
169+
removePoint = false;
170+
}
171+
else
172+
{
173+
removePoint = removePoint || point.x() >= cp.at( mCurrentPlotMarkerIndex + 1 ).x();
174+
}
175+
176+
if ( removePoint )
177+
{
178+
cp.removeAt( mCurrentPlotMarkerIndex );
179+
mCurrentPlotMarkerIndex = -1;
180+
}
181+
else
182+
{
183+
cp[ mCurrentPlotMarkerIndex ] = QgsPoint( point.x(), point.y() );
184+
}
185+
mCurve.setControlPoints( cp );
186+
updatePlot();
187+
emit changed();
188+
}
189+
190+
void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected )
191+
{
192+
QColor borderColor( 0, 0, 0 );
193+
194+
QColor brushColor = isSelected ? borderColor : QColor( 255, 255, 255, 0 );
195+
196+
QwtPlotMarker *marker = new QwtPlotMarker();
197+
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
198+
marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 6, 6 ) ) );
199+
#else
200+
marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 6, 6 ) ) );
201+
#endif
202+
marker->setValue( x, y );
203+
marker->attach( mPlot );
204+
marker->setRenderHint( QwtPlotItem::RenderAntialiased, true );
205+
mMarkers << marker;
206+
}
207+
208+
void QgsCurveEditorWidget::updatePlot()
209+
{
210+
// remove existing markers
211+
Q_FOREACH ( QwtPlotMarker* marker, mMarkers )
212+
{
213+
marker->detach();
214+
delete marker;
215+
}
216+
mMarkers.clear();
217+
218+
QPolygonF curvePoints;
219+
QVector< double > x;
220+
221+
int i = 0;
222+
Q_FOREACH ( const QgsPoint& point, mCurve.controlPoints() )
223+
{
224+
x << point.x();
225+
addPlotMarker( point.x(), point.y(), mCurrentPlotMarkerIndex == i );
226+
i++;
227+
}
228+
229+
//add extra intermediate points
230+
231+
for ( double p = 0; p <= 1.0; p += 0.01 )
232+
{
233+
x << p;
234+
}
235+
std::sort( x.begin(), x.end() );
236+
QVector< double > y = mCurve.y( x );
237+
238+
for ( int j = 0; j < x.count(); ++j )
239+
{
240+
curvePoints << QPointF( x.at( j ), y.at( j ) );
241+
}
242+
243+
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
244+
mPlotCurve->setSamples( curvePoints );
245+
#else
246+
mPlotCurve->setData( curvePoints );
247+
#endif
248+
mPlot->replot();
249+
}
250+
251+
252+
/// @cond PRIVATE
253+
254+
QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot )
255+
: QObject( plot )
256+
, mPlot( plot )
257+
{
258+
mPlot->canvas()->installEventFilter( this );
259+
}
260+
261+
bool QgsCurveEditorPlotEventFilter::eventFilter( QObject *object, QEvent *event )
262+
{
263+
if ( !mPlot->isEnabled() )
264+
return QObject::eventFilter( object, event );
265+
266+
switch ( event->type() )
267+
{
268+
case QEvent::MouseButtonPress:
269+
{
270+
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event );
271+
if ( mouseEvent->button() == Qt::LeftButton )
272+
{
273+
emit mousePress( mapPoint( mouseEvent->pos() ) );
274+
}
275+
break;
276+
}
277+
case QEvent::MouseMove:
278+
{
279+
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event );
280+
if ( mouseEvent->buttons() & Qt::LeftButton )
281+
{
282+
// only emit when button pressed
283+
emit mouseMove( mapPoint( mouseEvent->pos() ) );
284+
}
285+
break;
286+
}
287+
case QEvent::MouseButtonRelease:
288+
{
289+
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event );
290+
if ( mouseEvent->button() == Qt::LeftButton )
291+
{
292+
emit mouseRelease( mapPoint( mouseEvent->pos() ) );
293+
}
294+
break;
295+
}
296+
default:
297+
break;
298+
}
299+
300+
return QObject::eventFilter( object, event );
301+
}
302+
303+
QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const
304+
{
305+
if ( !mPlot )
306+
return QPointF();
307+
308+
return QPointF( mPlot->canvasMap( QwtPlot::xBottom ).invTransform( point.x() ),
309+
mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) );
310+
}
311+
312+
///@endcond

0 commit comments

Comments
 (0)