Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FEATURE] Add right click menu to color buttons, allowing copying and…
… pasting colors. Pasting colors accepts clipboard text in a variety of common formats, including hex and css style rgb and rgba strings.
  • Loading branch information
nyalldawson committed May 20, 2014
1 parent edbfb57 commit f31e637
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 10 deletions.
18 changes: 17 additions & 1 deletion python/core/symbology-ng/qgssymbollayerv2utils.sip
Expand Up @@ -189,9 +189,25 @@ class QgsSymbolLayerV2Utils
static QgsVectorColorRampV2* loadColorRamp( QDomElement& element ) /Factory/;
static QDomElement saveColorRamp( QString name, QgsVectorColorRampV2* ramp, QDomDocument& doc );

/** parse color definition with format "rgb(0,0,0)" or "0,0,0" */
/**
* Attempts to parse a string as a color using a variety of common formats, including hex
* codes, rgb and rgba strings.
* @param colorStr string representing the color
* @returns parsed color
* @note added in 2.3
*/
static QColor parseColor( QString colorStr );

/**
* Attempts to parse a string as a color using a variety of common formats, including hex
* codes, rgb and rgba strings.
* @param colorStr string representing the color
* @param containsAlpha if colorStr contains an explicit alpha value then containsAlpha will be set to true
* @returns parsed color
* @note added in 2.3
*/
static QColor parseColorWithAlpha( const QString colorStr, bool &containsAlpha );

/**Returns the line width scale factor depending on the unit and the paint device*/
static double lineWidthScaleFactor( const QgsRenderContext& c, QgsSymbolV2::OutputUnit u, const QgsMapUnitScale& scale = QgsMapUnitScale() );
/**Returns scale factor painter units -> pixel dimensions*/
Expand Down
5 changes: 5 additions & 0 deletions python/gui/qgscolorbutton.sip
Expand Up @@ -111,4 +111,9 @@ class QgsColorButton: QPushButton
void changeEvent( QEvent* e );
void showEvent( QShowEvent* e );
static const QPixmap& transpBkgrd();

/**
* Reimplemented to detect right mouse button clicks on the color button.
*/
void mousePressEvent( QMouseEvent* e );
};
103 changes: 95 additions & 8 deletions src/core/symbology-ng/qgssymbollayerv2utils.cpp
Expand Up @@ -2657,18 +2657,105 @@ QDomElement QgsSymbolLayerV2Utils::saveColorRamp( QString name, QgsVectorColorRa
return rampEl;
}

// parse color definition with format "rgb(0,0,0)" or "0,0,0"
// should support other formats like FFFFFF
QColor QgsSymbolLayerV2Utils::parseColor( QString colorStr )
{
if ( colorStr.startsWith( "rgb(" ) )
bool hasAlpha;
return parseColorWithAlpha( colorStr, hasAlpha );
}

QColor QgsSymbolLayerV2Utils::parseColorWithAlpha( const QString colorStr, bool &containsAlpha )
{
QColor parsedColor;

//color in hex format "#aabbcc"
if ( QColor::isValidColor( colorStr ) )
{
//string is a valid hex color string
parsedColor.setNamedColor( colorStr );
if ( parsedColor.isValid() )
{
containsAlpha = false;
return parsedColor;
}
}

//color in hex format, without #
QRegExp hexColorRx2( "^\\s*(?:[0-9a-fA-F]{3}){1,2}\\s*$" );
if ( hexColorRx2.indexIn( colorStr ) != -1 )
{
//add "#" and parse
parsedColor.setNamedColor( QString( "#" ) + colorStr );
if ( parsedColor.isValid() )
{
containsAlpha = false;
return parsedColor;
}
}

//color in (rrr,ggg,bbb) format, brackets and rgb prefix optional
QRegExp rgbFormatRx( "^\\s*(?:rgb)?\\(?\\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\s*,\\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\s*,\\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\s*\\)?\\s*;?\\s*$" );
if ( rgbFormatRx.indexIn( colorStr ) != -1 )
{
int r = rgbFormatRx.cap( 1 ).toInt();
int g = rgbFormatRx.cap( 2 ).toInt();
int b = rgbFormatRx.cap( 3 ).toInt();
parsedColor.setRgb( r, g, b );
if ( parsedColor.isValid() )
{
containsAlpha = false;
return parsedColor;
}
}

//color in (r%,g%,b%) format, brackets and rgb prefix optional
QRegExp rgbPercentFormatRx( "^\\s*(?:rgb)?\\(?\\s*(100|0*\\d{1,2})\\s*%\\s*,\\s*(100|0*\\d{1,2})\\s*%\\s*,\\s*(100|0*\\d{1,2})\\s*%\\s*\\)?\\s*;?\\s*$" );
if ( rgbPercentFormatRx.indexIn( colorStr ) != -1 )
{
int r = qRound( rgbPercentFormatRx.cap( 1 ).toDouble() * 2.55 );
int g = qRound( rgbPercentFormatRx.cap( 2 ).toDouble() * 2.55 );
int b = qRound( rgbPercentFormatRx.cap( 3 ).toDouble() * 2.55 );
parsedColor.setRgb( r, g, b );
if ( parsedColor.isValid() )
{
containsAlpha = false;
return parsedColor;
}
}

//color in (r,g,b,a) format, brackets and rgba prefix optional
QRegExp rgbaFormatRx( "^\\s*(?:rgba)?\\(?\\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\s*,\\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\s*,\\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\s*,\\s*(0|0?\\.\\d*|1(?:\\.0*)?)\\s*\\)?\\s*;?\\s*$" );
if ( rgbaFormatRx.indexIn( colorStr ) != -1 )
{
colorStr = colorStr.mid( 4, colorStr.size() - 5 ).trimmed();
int r = rgbaFormatRx.cap( 1 ).toInt();
int g = rgbaFormatRx.cap( 2 ).toInt();
int b = rgbaFormatRx.cap( 3 ).toInt();
int a = qRound( rgbaFormatRx.cap( 4 ).toDouble() * 255.0 );
parsedColor.setRgb( r, g, b, a );
if ( parsedColor.isValid() )
{
containsAlpha = true;
return parsedColor;
}
}

//color in (r%,g%,b%,a) format, brackets and rgba prefix optional
QRegExp rgbaPercentFormatRx( "^\\s*(?:rgba)?\\(?\\s*(100|0*\\d{1,2})\\s*%\\s*,\\s*(100|0*\\d{1,2})\\s*%\\s*,\\s*(100|0*\\d{1,2})\\s*%\\s*,\\s*(0|0?\\.\\d*|1(?:\\.0*)?)\\s*\\)?\\s*;?\\s*$" );
if ( rgbaPercentFormatRx.indexIn( colorStr ) != -1 )
{
int r = qRound( rgbaPercentFormatRx.cap( 1 ).toDouble() * 2.55 );
int g = qRound( rgbaPercentFormatRx.cap( 2 ).toDouble() * 2.55 );
int b = qRound( rgbaPercentFormatRx.cap( 3 ).toDouble() * 2.55 );
int a = qRound( rgbaPercentFormatRx.cap( 4 ).toDouble() * 255.0 );
parsedColor.setRgb( r, g, b, a );
if ( parsedColor.isValid() )
{
containsAlpha = true;
return parsedColor;
}
}
QStringList p = colorStr.split( QChar( ',' ) );
if ( p.count() != 3 )
return QColor();
return QColor( p[0].toInt(), p[1].toInt(), p[2].toInt() );

//couldn't parse string as color
return QColor();
}

double QgsSymbolLayerV2Utils::lineWidthScaleFactor( const QgsRenderContext& c, QgsSymbolV2::OutputUnit u, const QgsMapUnitScale& scale )
Expand Down
18 changes: 17 additions & 1 deletion src/core/symbology-ng/qgssymbollayerv2utils.h
Expand Up @@ -225,9 +225,25 @@ class CORE_EXPORT QgsSymbolLayerV2Utils
static QgsVectorColorRampV2* loadColorRamp( QDomElement& element );
static QDomElement saveColorRamp( QString name, QgsVectorColorRampV2* ramp, QDomDocument& doc );

/** parse color definition with format "rgb(0,0,0)" or "0,0,0" */
/**
* Attempts to parse a string as a color using a variety of common formats, including hex
* codes, rgb and rgba strings.
* @param colorStr string representing the color
* @returns parsed color
* @note added in 2.3
*/
static QColor parseColor( QString colorStr );

/**
* Attempts to parse a string as a color using a variety of common formats, including hex
* codes, rgb and rgba strings.
* @param colorStr string representing the color
* @param containsAlpha if colorStr contains an explicit alpha value then containsAlpha will be set to true
* @returns parsed color
* @note added in 2.3
*/
static QColor parseColorWithAlpha( const QString colorStr, bool &containsAlpha );

/**Returns the line width scale factor depending on the unit and the paint device*/
static double lineWidthScaleFactor( const QgsRenderContext& c, QgsSymbolV2::OutputUnit u, const QgsMapUnitScale& scale = QgsMapUnitScale() );
/**Returns scale factor painter units -> pixel dimensions*/
Expand Down
84 changes: 84 additions & 0 deletions src/gui/qgscolorbutton.cpp
Expand Up @@ -17,10 +17,14 @@
#include "qgscolordialog.h"
#include "qgsapplication.h"
#include "qgslogger.h"
#include "qgssymbollayerv2utils.h"

#include <QPainter>
#include <QSettings>
#include <QTemporaryFile>
#include <QMouseEvent>
#include <QMenu>
#include <QClipboard>

/*!
\class QgsColorButton
Expand Down Expand Up @@ -87,6 +91,86 @@ void QgsColorButton::onButtonClicked()
activateWindow();
}

void QgsColorButton::mousePressEvent( QMouseEvent *e )
{
if ( e->button() == Qt::RightButton )
{
showContextMenu( e );
}
else
{
QPushButton::mousePressEvent( e );
}
}

void QgsColorButton::showContextMenu( QMouseEvent *event )
{
QMenu colorContextMenu;

QAction* copyAsHexAction = new QAction( tr( "Copy color" ), 0 );
colorContextMenu.addAction( copyAsHexAction );
QAction* copyAsRgbAction = new QAction( tr( "Copy as rgb" ), 0 );
colorContextMenu.addAction( copyAsRgbAction );
QAction* copyAsRgbaAction = new QAction( tr( "Copy as rgba" ), 0 );
if ( mColorDialogOptions & QColorDialog::ShowAlphaChannel )
{
//alpha enabled, so add rgba action
colorContextMenu.addAction( copyAsRgbaAction );
}

QString clipboardText = QApplication::clipboard()->text();
QAction* pasteColorAction = new QAction( tr( "Paste color" ), 0 );
pasteColorAction->setEnabled( false );
colorContextMenu.addSeparator();
colorContextMenu.addAction( pasteColorAction );
QColor clipColor;
if ( !( clipboardText.isEmpty() ) )
{
bool hasAlpha = false;
clipColor = QgsSymbolLayerV2Utils::parseColorWithAlpha( clipboardText, hasAlpha );

if ( clipColor.isValid() )
{
if ( !hasAlpha )
{
//clipboard color has no explicit alpha component, so keep existing alpha
clipColor.setAlpha( mColor.alpha() );
}
pasteColorAction->setEnabled( true );
}
}

QAction* selectedAction = colorContextMenu.exec( event->globalPos( ) );
if ( selectedAction == copyAsHexAction )
{
//copy color as hex code
QString colorString = mColor.name();
QApplication::clipboard()->setText( colorString );
}
else if ( selectedAction == copyAsRgbAction )
{
//copy color as rgb
QString colorString = QString( "rgb(%1,%2,%3)" ).arg( mColor.red() ). arg( mColor.green() ).arg( mColor.blue() );
QApplication::clipboard()->setText( colorString );
}
else if ( selectedAction == copyAsRgbaAction )
{
//copy color as rgba
QString colorString = QString( "rgba(%1,%2,%3,%4)" ).arg( mColor.red() ).arg( mColor.green() ).arg( mColor.blue() ).arg( QString::number( mColor.alphaF(), 'f', 2 ) );
QApplication::clipboard()->setText( colorString );
}
else if ( selectedAction == pasteColorAction )
{
//paste color
setColor( clipColor );
}

delete copyAsHexAction;
delete copyAsRgbAction;
delete copyAsRgbaAction;
delete pasteColorAction;
}

void QgsColorButton::setValidColor( const QColor& newColor )
{
if ( newColor.isValid() )
Expand Down
10 changes: 10 additions & 0 deletions src/gui/qgscolorbutton.h 100644 → 100755
Expand Up @@ -139,6 +139,11 @@ class GUI_EXPORT QgsColorButton: public QPushButton
void showEvent( QShowEvent* e );
static const QPixmap& transpBkgrd();

/**
* Reimplemented to detect right mouse button clicks on the color button.
*/
void mousePressEvent( QMouseEvent* e );

private:
QString mColorDialogTitle;
QColor mColor;
Expand All @@ -147,6 +152,11 @@ class GUI_EXPORT QgsColorButton: public QPushButton
QTemporaryFile mTempPNG;
bool mColorSet; // added in QGIS 2.1

/**
* Shows the color button context menu and handles copying and pasting color values.
*/
void showContextMenu( QMouseEvent* event );

private slots:
void onButtonClicked();

Expand Down
71 changes: 71 additions & 0 deletions tests/src/core/testqgsstylev2.cpp
Expand Up @@ -54,6 +54,7 @@ class TestStyleV2: public QObject
void testCreateColorRamps();
void testLoadColorRamps();
void testSaveLoad();
void testParseColor();
};


Expand Down Expand Up @@ -213,6 +214,76 @@ void TestStyleV2::testSaveLoad()
testLoadColorRamps();
}

void TestStyleV2::testParseColor()
{
// values for color tests
QMap< QString, QPair< QColor, bool> > colorTests;
colorTests.insert( "bad color", qMakePair( QColor( ), false ) );
colorTests.insert( "red", qMakePair( QColor( 255, 0, 0 ), false ) );
colorTests.insert( "#ff00ff", qMakePair( QColor( 255, 0, 255 ), false ) );
colorTests.insert( "#99AA00", qMakePair( QColor( 153, 170, 0 ), false ) );
colorTests.insert( "#GG0000", qMakePair( QColor(), false ) );
colorTests.insert( "000000", qMakePair( QColor( 0, 0, 0 ), false ) );
colorTests.insert( "00ff00", qMakePair( QColor( 0, 255, 0 ), false ) );
colorTests.insert( "00gg00", qMakePair( QColor( ), false ) );
colorTests.insert( "00ff000", qMakePair( QColor(), false ) );
colorTests.insert( "fff", qMakePair( QColor( 255, 255, 255 ), false ) );
colorTests.insert( "fff0", qMakePair( QColor(), false ) );
colorTests.insert( "0,0,0", qMakePair( QColor( 0, 0, 0 ), false ) );
colorTests.insert( "127,60,0", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "255,255,255", qMakePair( QColor( 255, 255, 255 ), false ) );
colorTests.insert( "256,60,0", qMakePair( QColor(), false ) );
colorTests.insert( "rgb(127,60,0)", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "rgb(255,255,255)", qMakePair( QColor( 255, 255, 255 ), false ) );
colorTests.insert( "rgb(256,60,0)", qMakePair( QColor(), false ) );
colorTests.insert( " rgb( 127, 60 , 0 ) ", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "rgb(127,60,0);", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "(127,60,0);", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "(127,60,0)", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "127,060,000", qMakePair( QColor( 127, 60, 0 ), false ) );
colorTests.insert( "0,0,0,0", qMakePair( QColor( 0, 0, 0, 0 ), true ) );
colorTests.insert( "127,60,0,0.5", qMakePair( QColor( 127, 60, 0, 128 ), true ) );
colorTests.insert( "255,255,255,0.1", qMakePair( QColor( 255, 255, 255, 26 ), true ) );
colorTests.insert( "rgba(127,60,0,1.0)", qMakePair( QColor( 127, 60, 0, 255 ), true ) );
colorTests.insert( "rgba(255,255,255,0.0)", qMakePair( QColor( 255, 255, 255, 0 ), true ) );
colorTests.insert( " rgba( 127, 60 , 0 , 0.2 ) ", qMakePair( QColor( 127, 60, 0, 51 ), true ) );
colorTests.insert( "rgba(127,60,0,0.1);", qMakePair( QColor( 127, 60, 0, 26 ), true ) );
colorTests.insert( "(127,60,0,1);", qMakePair( QColor( 127, 60, 0, 255 ), true ) );
colorTests.insert( "(127,60,0,1.0)", qMakePair( QColor( 127, 60, 0, 255 ), true ) );
colorTests.insert( "127,060,000,1", qMakePair( QColor( 127, 60, 0, 255 ), true ) );
colorTests.insert( "0%,0%,0%", qMakePair( QColor( 0, 0, 0 ), false ) );
colorTests.insert( "50 %,60 %,0 %", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "100%, 100%, 100%", qMakePair( QColor( 255, 255, 255 ), false ) );
colorTests.insert( "rgb(50%,60%,0%)", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "rgb(100%, 100%, 100%)", qMakePair( QColor( 255, 255, 255 ), false ) );
colorTests.insert( " rgb( 50 % , 60 % , 0 % ) ", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "rgb(50%,60%,0%);", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "(50%,60%,0%);", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "(50%,60%,0%)", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "050%,060%,000%", qMakePair( QColor( 127, 153, 0 ), false ) );
colorTests.insert( "0%,0%,0%,0", qMakePair( QColor( 0, 0, 0, 0 ), true ) );
colorTests.insert( "50 %,60 %,0 %,0.5", qMakePair( QColor( 127, 153, 0, 128 ), true ) );
colorTests.insert( "100%, 100%, 100%, 1.0", qMakePair( QColor( 255, 255, 255, 255 ), true ) );
colorTests.insert( "rgba(50%,60%,0%, 1.0)", qMakePair( QColor( 127, 153, 0, 255 ), true ) );
colorTests.insert( "rgba(100%, 100%, 100%, 0.0)", qMakePair( QColor( 255, 255, 255, 0 ), true ) );
colorTests.insert( " rgba( 50 % , 60 % , 0 %, 0.5 ) ", qMakePair( QColor( 127, 153, 0, 128 ), true ) );
colorTests.insert( "rgba(50%,60%,0%,0);", qMakePair( QColor( 127, 153, 0, 0 ), true ) );
colorTests.insert( "(50%,60%,0%,1);", qMakePair( QColor( 127, 153, 0, 255 ), true ) );
colorTests.insert( "(50%,60%,0%,1.0)", qMakePair( QColor( 127, 153, 0, 255 ), true ) );
colorTests.insert( "050%,060%,000%,0", qMakePair( QColor( 127, 153, 0, 0 ), true ) );

QMap<QString, QPair< QColor, bool> >::const_iterator i = colorTests.constBegin();
while ( i != colorTests.constEnd() )
{
QgsDebugMsg( "color string: " + i.key() );
bool hasAlpha = false;
QColor result = QgsSymbolLayerV2Utils::parseColorWithAlpha( i.key(), hasAlpha );
QVERIFY( result == i.value().first );
QVERIFY( hasAlpha == i.value().second );
++i;
}
}


QTEST_MAIN( TestStyleV2 )
#include "moc_testqgsstylev2.cxx"

0 comments on commit f31e637

Please sign in to comment.