[FEATURE] New reusable interactive widget for editing gradient ramps

Supports:

- drag to move color stops
- double clicking to add a new stop
- pressing delete will remove the selected stop
- pressing arrow keys will move the selected stop, shift+arrow= larger
  move
- drag and drop a color onto the widget to add a new stop

Sponsored by North Road
This commit is contained in:
Nyall Dawson 2016-05-12 20:56:40 +10:00
parent 79f3d4221b
commit 881be0f365
5 changed files with 774 additions and 0 deletions

View File

@ -82,6 +82,7 @@
%Include qgsformannotationitem.sip
%Include qgsgenericprojectionselector.sip
%Include qgsgeometryrubberband.sip
%Include qgsgradientstopeditor.sip
%Include qgsgroupwmsdatadialog.sip
%Include qgshighlight.sip
%Include qgshistogramwidget.sip

View File

@ -0,0 +1,115 @@
/** \ingroup gui
* \class QgsGradientStopEditor
* An interactive editor for previewing a gradient color ramp and modifying the position of color
* stops along the gradient.
* \note Added in version 2.16
*/
class QgsGradientStopEditor : QWidget
{
%TypeHeaderCode
#include <qgsgradientstopeditor.h>
%End
public:
/** Constructor for QgsGradientStopEditor.
* @param parent parent widget
* @param ramp optional initial gradient ramp
*/
QgsGradientStopEditor( QWidget* parent /TransferThis/ = nullptr, QgsVectorGradientColorRampV2* ramp = nullptr );
/** Sets the current ramp shown in the editor.
* @param ramp color ramp
* @see gradientRamp()
*/
void setGradientRamp( const QgsVectorGradientColorRampV2& ramp );
/** Returns the current ramp created by the editor.
* @see setGradientRamp()
*/
QgsVectorGradientColorRampV2 gradientRamp() const;
/** Sets the currently selected stop.
* @param index index of stop, where 0 corresponds to the first stop
* @see selectedStop()
*/
void selectStop( int index );
/** Returns details about the currently selected stop.
* @see selectStop()
*/
QgsGradientStop selectedStop() const;
virtual QSize sizeHint() const;
void paintEvent( QPaintEvent* event );
public slots:
/** Sets the color for the current selected stop.
* @param color new stop color
* @see setSelectedStopOffset()
* @see setSelectedStopDetails()
* @see setColor1()
* @see setColor2()
*/
void setSelectedStopColor( const QColor& color );
/** Sets the offset for the current selected stop. This slot has no effect if either the
* first or last stop is selected, as they cannot be repositioned.
* @param offset new stop offset
* @see setSelectedStopColor()
* @see setSelectedStopDetails()
*/
void setSelectedStopOffset( double offset );
/** Sets the color and offset for the current selected stop.
* @param color new stop color
* @param offset new stop offset
* @see setSelectedStopColor()
* @see setSelectedStopOffset()
*/
void setSelectedStopDetails( const QColor& color, double offset );
/** Deletes the current selected stop. This slot has no effect if either the
* first or last stop is selected, as they cannot be deleted.
*/
void deleteSelectedStop();
/** Sets the color for the first stop.
* @param color new stop color
* @see setColor2()
* @see setSelectedStopColor()
*/
void setColor1( const QColor& color );
/** Sets the color for the last stop.
* @param color new stop color
* @see setColor1()
* @see setSelectedStopColor()
*/
void setColor2( const QColor& color );
signals:
//! Emmitted when the gradient ramp is changed by a user
void changed();
/** Emmitted when the current selected stop changes.
* @param stop details about newly selected stop
*/
void selectedStopChanged( const QgsGradientStop& stop );
protected:
virtual void mouseMoveEvent( QMouseEvent *event );
virtual void mousePressEvent( QMouseEvent *event );
virtual void mouseDoubleClickEvent( QMouseEvent * event );
virtual void keyPressEvent( QKeyEvent * event );
//Reimplemented to accept dragged colors
void dragEnterEvent( QDragEnterEvent * e );
//Reimplemented to accept dropped colors
void dropEvent( QDropEvent *e );
};

View File

@ -212,6 +212,7 @@ SET(QGIS_GUI_SRCS
qgsformannotationitem.cpp
qgsgenericprojectionselector.cpp
qgsgeometryrubberband.cpp
qgsgradientstopeditor.cpp
qgsgroupwmsdatadialog.cpp
qgshighlight.cpp
qgshistogramwidget.cpp
@ -357,6 +358,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsfocuswatcher.h
qgsformannotationitem.h
qgsgenericprojectionselector.h
qgsgradientstopeditor.h
qgsgroupwmsdatadialog.h
qgshistogramwidget.h
qgshtmlannotationitem.h

View File

@ -0,0 +1,477 @@
/***************************************************************************
qgsgradientstopeditor.cpp
-------------------------
begin : April 2016
copyright : (C) 2016 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsgradientstopeditor.h"
#include "qgsapplication.h"
#include <QPainter>
#include <QStyleOptionFrameV3>
#include <QMouseEvent>
#define MARKER_WIDTH 11
#define MARKER_HEIGHT 14
#define MARKER_GAP 1.5
#define MARGIN_BOTTOM ( MARKER_HEIGHT + 2 )
#define MARGIN_X ( MARKER_WIDTH / 2 )
#define FRAME_MARGIN 2
#define CLICK_THRESHOLD ( MARKER_WIDTH / 2 + 3 )
QgsGradientStopEditor::QgsGradientStopEditor( QWidget *parent, QgsVectorGradientColorRampV2 *ramp )
: QWidget( parent )
, mSelectedStop( 0 )
{
if ( ramp )
mGradient = *ramp;
mStops = mGradient.stops();
if ( sOuterTriangle.isEmpty() )
{
sOuterTriangle << QPointF( 0, MARKER_HEIGHT ) << QPointF( MARKER_WIDTH, MARKER_HEIGHT )
<< QPointF( MARKER_WIDTH, MARKER_WIDTH / 2.0 )
<< QPointF( MARKER_WIDTH / 2.0, 0 )
<< QPointF( 0, MARKER_WIDTH / 2.0 )
<< QPointF( 0, MARKER_HEIGHT );
}
if ( sInnerTriangle.isEmpty() )
{
sInnerTriangle << QPointF( MARKER_GAP, MARKER_HEIGHT - MARKER_GAP ) << QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_HEIGHT - MARKER_GAP )
<< QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
<< QPointF( MARKER_WIDTH / 2.0, MARKER_GAP )
<< QPointF( MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
<< QPointF( MARKER_GAP, MARKER_HEIGHT - MARKER_GAP );
}
setFocusPolicy( Qt::StrongFocus );
setAcceptDrops( true );
}
void QgsGradientStopEditor::setGradientRamp( const QgsVectorGradientColorRampV2 &ramp )
{
mGradient = ramp;
mStops = mGradient.stops();
mSelectedStop = 0;
update();
emit changed();
}
QSize QgsGradientStopEditor::sizeHint() const
{
//horizontal
return QSize( 200, 80 );
}
void QgsGradientStopEditor::paintEvent( QPaintEvent *event )
{
Q_UNUSED( event );
QPainter painter( this );
QRect frameRect( rect().x() + MARGIN_X, rect().y(),
rect().width() - 2 * MARGIN_X,
rect().height() - MARGIN_BOTTOM );
//draw frame
QStyleOptionFrameV3 option;
option.initFrom( this );
option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
option.rect = frameRect;
style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
if ( hasFocus() )
{
//draw focus rect
QStyleOptionFocusRect option;
option.initFrom( this );
option.state = QStyle::State_KeyboardFocusChange;
option.rect = frameRect;
style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
}
//start with the checkboard pattern
QBrush checkBrush = QBrush( transparentBackground() );
painter.setBrush( checkBrush );
painter.setPen( Qt::NoPen );
QRect box( frameRect.x() + FRAME_MARGIN, frameRect.y() + FRAME_MARGIN,
frameRect.width() - 2 * FRAME_MARGIN,
frameRect.height() - 2 * FRAME_MARGIN );
painter.drawRect( box );
// draw gradient preview on top of checkerboard
for ( int i = 0; i < box.width() + 1; ++i )
{
QPen pen( mGradient.color( static_cast< double >( i ) / box.width() ) );
painter.setPen( pen );
painter.drawLine( box.left() + i, box.top(), box.left() + i, box.height() + 1 );
}
// draw stop markers
int markerTop = frameRect.bottom() + 1;
drawStopMarker( painter, QPoint( box.left(), markerTop ), mGradient.color1(), mSelectedStop == 0 );
drawStopMarker( painter, QPoint( box.right(), markerTop ), mGradient.color2(), mSelectedStop == mGradient.count() - 1 );
int i = 1;
Q_FOREACH ( const QgsGradientStop& stop, mStops )
{
int x = stop.offset * box.width() + box.left();
drawStopMarker( painter, QPoint( x, markerTop ), stop.color, mSelectedStop == i );
++i;
}
painter.end();
}
void QgsGradientStopEditor::selectStop( int index )
{
if ( index > 0 && index < mGradient.count() - 1 )
{
// need to map original stop index across to cached, possibly out of order stop index
QgsGradientStop selectedStop = mGradient.stops().at( index - 1 );
index = 1;
Q_FOREACH ( const QgsGradientStop& stop, mStops )
{
if ( stop == selectedStop )
{
break;
}
index++;
}
}
mSelectedStop = index;
emit selectedStopChanged( selectedStop() );
update();
}
QgsGradientStop QgsGradientStopEditor::selectedStop() const
{
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
{
return mStops.at( mSelectedStop - 1 );
}
else if ( mSelectedStop == 0 )
{
return QgsGradientStop( 0.0, mGradient.color1() );
}
else
{
return QgsGradientStop( 1.0, mGradient.color2() );
}
}
void QgsGradientStopEditor::setSelectedStopColor( const QColor &color )
{
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
{
mStops[ mSelectedStop - 1 ].color = color;
mGradient.setStops( mStops );
}
else if ( mSelectedStop == 0 )
{
mGradient.setColor1( color );
}
else
{
mGradient.setColor2( color );
}
update();
emit changed();
}
void QgsGradientStopEditor::setSelectedStopOffset( double offset )
{
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
{
mStops[ mSelectedStop - 1 ].offset = offset;
mGradient.setStops( mStops );
update();
emit changed();
}
}
void QgsGradientStopEditor::setSelectedStopDetails( const QColor &color, double offset )
{
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
{
mStops[ mSelectedStop - 1 ].color = color;
mStops[ mSelectedStop - 1 ].offset = offset;
mGradient.setStops( mStops );
}
else if ( mSelectedStop == 0 )
{
mGradient.setColor1( color );
}
else
{
mGradient.setColor2( color );
}
update();
emit changed();
}
void QgsGradientStopEditor::deleteSelectedStop()
{
if ( selectedStopIsMovable() )
{
//delete stop
double stopOffset = mStops.at( mSelectedStop - 1 ).offset;
mStops.removeAt( mSelectedStop - 1 );
mGradient.setStops( mStops );
int closest = findClosestStop( relativePositionToPoint( stopOffset ) );
if ( closest >= 0 )
selectStop( closest );
update();
emit changed();
}
}
void QgsGradientStopEditor::setColor1( const QColor &color )
{
mGradient.setColor1( color );
update();
emit changed();
}
void QgsGradientStopEditor::setColor2( const QColor &color )
{
mGradient.setColor2( color );
update();
emit changed();
}
void QgsGradientStopEditor::mouseMoveEvent( QMouseEvent *e )
{
if ( e->buttons() & Qt::LeftButton )
{
if ( selectedStopIsMovable() )
{
double offset = pointToRelativePosition( e->pos().x() );
// have to edit the temporary stop list, as setting stops on the gradient will reorder them
// and change which stop corresponds to the selected one;
mStops[ mSelectedStop - 1 ].offset = offset;
mGradient.setStops( mStops );
update();
emit changed();
}
}
e->accept();
}
int QgsGradientStopEditor::findClosestStop( int x, int threshold ) const
{
int closestStop = -1;
int closestDiff = INT_MAX;
int currentDiff = INT_MAX;
// check for matching stops first, so that they take precedence
// otherwise it's impossible to select a stop which sits above the first/last stop, making
// it impossible to move or delete these
int i = 1;
Q_FOREACH ( const QgsGradientStop& stop, mGradient.stops() )
{
currentDiff = qAbs( relativePositionToPoint( stop.offset ) + 1 - x );
if (( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
{
closestStop = i;
closestDiff = currentDiff;
}
i++;
}
//first stop
currentDiff = qAbs( relativePositionToPoint( 0.0 ) + 1 - x );
if (( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
{
closestStop = 0;
closestDiff = currentDiff;
}
//last stop
currentDiff = qAbs( relativePositionToPoint( 1.0 ) + 1 - x );
if (( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
{
closestStop = mGradient.count() - 1;
closestDiff = currentDiff;
}
return closestStop;
}
void QgsGradientStopEditor::mousePressEvent( QMouseEvent *e )
{
if ( e->pos().y() >= rect().height() - MARGIN_BOTTOM - 1 )
{
// find closest point
int closestStop = findClosestStop( e->pos().x(), CLICK_THRESHOLD );
if ( closestStop >= 0 )
{
selectStop( closestStop );
}
update();
}
e->accept();
}
void QgsGradientStopEditor::mouseDoubleClickEvent( QMouseEvent *e )
{
if ( e->buttons() & Qt::LeftButton )
{
// add a new stop
double offset = pointToRelativePosition( e->pos().x() );
mStops << QgsGradientStop( offset, mGradient.color( offset ) );
mSelectedStop = mStops.length();
mGradient.setStops( mStops );
update();
emit changed();
}
e->accept();
}
void QgsGradientStopEditor::keyPressEvent( QKeyEvent *e )
{
if (( e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete ) )
{
deleteSelectedStop();
e->accept();
return;
}
else if ( e->key() == Qt::Key_Left || e->key() == Qt::Key_Right )
{
if ( selectedStopIsMovable() )
{
// calculate offset corresponding to 1 px
double offsetDiff = pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN + 2 ) - pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN + 1 );
if ( e->modifiers() & Qt::ShiftModifier )
offsetDiff *= 10.0;
if ( e->key() == Qt::Key_Left )
offsetDiff *= -1;
mStops[ mSelectedStop - 1 ].offset = qBound( 0.0, mStops[ mSelectedStop - 1 ].offset + offsetDiff, 1.0 );
mGradient.setStops( mStops );
update();
e->accept();
emit changed();
return;
}
}
QWidget::keyPressEvent( e );
}
const QPixmap& QgsGradientStopEditor::transparentBackground()
{
static QPixmap transpBkgrd;
if ( transpBkgrd.isNull() )
transpBkgrd = QgsApplication::getThemePixmap( "/transp-background_8x8.png" );
return transpBkgrd;
}
void QgsGradientStopEditor::drawStopMarker( QPainter& painter, QPoint topMiddle, const QColor &color, bool selected )
{
painter.save();
painter.setRenderHint( QPainter::Antialiasing );
painter.setBrush( selected ? QColor( 150, 150, 150 ) : Qt::white );
painter.setPen( selected ? Qt::black : QColor( 150, 150, 150 ) );
// 0.5 offsets to make edges pixel grid aligned
painter.translate( qRound( topMiddle.x() - MARKER_WIDTH / 2.0 ) + 0.5, topMiddle.y() + 0.5 );
painter.drawPolygon( sOuterTriangle );
// draw the checkerboard background for marker
painter.setBrush( QBrush( transparentBackground() ) );
painter.setPen( Qt::NoPen );
painter.drawPolygon( sInnerTriangle );
// draw color on top
painter.setBrush( color );
painter.drawPolygon( sInnerTriangle );
painter.restore();
}
double QgsGradientStopEditor::pointToRelativePosition( int x ) const
{
int left = rect().x() + MARGIN_X + FRAME_MARGIN;
int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
if ( x <= left )
return 0;
else if ( x >= right )
return 1.0;
return static_cast< double >( x - left ) / ( right - left );
}
int QgsGradientStopEditor::relativePositionToPoint( double position ) const
{
int left = rect().x() + MARGIN_X + FRAME_MARGIN;
int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
if ( position <= 0 )
return left;
else if ( position >= 1.0 )
return right;
return left + ( right - left ) * position;
}
bool QgsGradientStopEditor::selectedStopIsMovable() const
{
// first and last stop can't be moved or deleted
return mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1;
}
void QgsGradientStopEditor::dragEnterEvent( QDragEnterEvent *e )
{
//is dragged data valid color data?
bool hasAlpha;
QColor mimeColor = QgsSymbolLayerV2Utils::colorFromMimeData( e->mimeData(), hasAlpha );
if ( mimeColor.isValid() )
{
//if so, we accept the drag
e->acceptProposedAction();
}
}
void QgsGradientStopEditor::dropEvent( QDropEvent *e )
{
//is dropped data valid color data?
bool hasAlpha = false;
QColor mimeColor = QgsSymbolLayerV2Utils::colorFromMimeData( e->mimeData(), hasAlpha );
if ( mimeColor.isValid() )
{
//accept drop and set new color
e->acceptProposedAction();
// add a new stop here
double offset = pointToRelativePosition( e->pos().x() );
mStops << QgsGradientStop( offset, mimeColor );
mSelectedStop = mStops.length();
mGradient.setStops( mStops );
update();
emit changed();
}
//could not get color from mime data
}

View File

@ -0,0 +1,179 @@
/***************************************************************************
qgsgradientstopeditor.h
-----------------------
begin : April 2016
copyright : (C) 2016 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSGRADIENTSTOPEDITOR_H
#define QGSGRADIENTSTOPEDITOR_H
#include "qgsvectorcolorrampv2.h"
#include <QWidget>
/** \ingroup gui
* \class QgsGradientStopEditor
* An interactive editor for previewing a gradient color ramp and modifying the position of color
* stops along the gradient.
* \note Added in version 2.16
*/
class GUI_EXPORT QgsGradientStopEditor : public QWidget
{
Q_OBJECT
public:
/** Constructor for QgsGradientStopEditor.
* @param parent parent widget
* @param ramp optional initial gradient ramp
*/
QgsGradientStopEditor( QWidget* parent = nullptr, QgsVectorGradientColorRampV2* ramp = nullptr );
/** Sets the current ramp shown in the editor.
* @param ramp color ramp
* @see gradientRamp()
*/
void setGradientRamp( const QgsVectorGradientColorRampV2& ramp );
/** Returns the current ramp created by the editor.
* @see setGradientRamp()
*/
QgsVectorGradientColorRampV2 gradientRamp() const { return mGradient; }
/** Sets the currently selected stop.
* @param index index of stop, where 0 corresponds to the first stop
* @see selectedStop()
*/
void selectStop( int index );
/** Returns details about the currently selected stop.
* @see selectStop()
*/
QgsGradientStop selectedStop() const;
virtual QSize sizeHint() const override;
void paintEvent( QPaintEvent* event ) override;
public slots:
/** Sets the color for the current selected stop.
* @param color new stop color
* @see setSelectedStopOffset()
* @see setSelectedStopDetails()
* @see setColor1()
* @see setColor2()
*/
void setSelectedStopColor( const QColor& color );
/** Sets the offset for the current selected stop. This slot has no effect if either the
* first or last stop is selected, as they cannot be repositioned.
* @param offset new stop offset
* @see setSelectedStopColor()
* @see setSelectedStopDetails()
*/
void setSelectedStopOffset( double offset );
/** Sets the color and offset for the current selected stop.
* @param color new stop color
* @param offset new stop offset
* @see setSelectedStopColor()
* @see setSelectedStopOffset()
*/
void setSelectedStopDetails( const QColor& color, double offset );
/** Deletes the current selected stop. This slot has no effect if either the
* first or last stop is selected, as they cannot be deleted.
*/
void deleteSelectedStop();
/** Sets the color for the first stop.
* @param color new stop color
* @see setColor2()
* @see setSelectedStopColor()
*/
void setColor1( const QColor& color );
/** Sets the color for the last stop.
* @param color new stop color
* @see setColor1()
* @see setSelectedStopColor()
*/
void setColor2( const QColor& color );
signals:
//! Emmitted when the gradient ramp is changed by a user
void changed();
/** Emmitted when the current selected stop changes.
* @param stop details about newly selected stop
*/
void selectedStopChanged( const QgsGradientStop& stop );
protected:
virtual void mouseMoveEvent( QMouseEvent *event ) override;
virtual void mousePressEvent( QMouseEvent *event ) override;
virtual void mouseDoubleClickEvent( QMouseEvent * event ) override;
virtual void keyPressEvent( QKeyEvent * event ) override;
//Reimplemented to accept dragged colors
void dragEnterEvent( QDragEnterEvent * e ) override;
//Reimplemented to accept dropped colors
void dropEvent( QDropEvent *e ) override;
private:
/** Generates a checkboard pattern pixmap for use as a background to transparent colors
* @returns checkerboard pixmap
*/
const QPixmap& transparentBackground();
/** Draws a stop marker on the specified painter.
* @param painter destination painter
* @param topMiddle coordinate corresponding to top middle point of desired marker
* @param color color of marker
* @param selected set to true to draw the marker in a selected state
*/
void drawStopMarker( QPainter& painter, QPoint topMiddle, const QColor& color, bool selected = false );
//! Converts an x-coordinate in the widget's coordinate system to a relative ramp position
double pointToRelativePosition( int x ) const;
//! Converts a relative ramp position to a x-coordinate in the widget's coordinate system
int relativePositionToPoint( double position ) const;
//! Returns true if the selected stop is movable and deletable
bool selectedStopIsMovable() const;
//! Returns the closest stop to a mouse x position, or -1 if no stops within tolerance
int findClosestStop( int x, int threshold = -1 ) const;
QgsVectorGradientColorRampV2 mGradient;
//! We keep a separate, unordered copy of the gradient stops so that the selected stop is not changed.
QgsGradientStopsList mStops;
//! Stop number of selected stop, where 0 = first stop
int mSelectedStop;
//! Polygon for stop triangle marker outer
QPolygonF sOuterTriangle;
//! Polygon for stop triangle marker inner
QPolygonF sInnerTriangle;
};
#endif // QGSGRADIENTSTOPEDITOR_H