Skip to content

Commit

Permalink
[FEATURE] Show Project Colors in color bound data defined buttons
Browse files Browse the repository at this point in the history
This adds a new "Project Colors" section in data defined buttons
which are linked to a color value. The color menu contains all
colors defined as part of the current project's Project Color
Scheme (which is defined through project properties).

When a project color is selected from the button, the property
becomes linked to that color. It will automatically follow any
future changes to the color when made through project properties.

This allows users to define common colors for a project once,
and then "bind" symbol, label, layout, etc colors to these
preset colors. The link is live, so you change it once, and
the change is reflected EVERYWHERE. Sure beats updating a color
100 times when it's use has been scattered throughout a project's
symbols, labels, etc...

(Basically, this is just adding a shortcut to setting a data
defined expression "project_color(...)" for the property. The
project_color function has been around a LOOONG time, but it's
only really been usable by power users before this change)
  • Loading branch information
nyalldawson committed Jan 11, 2019
1 parent 79d01a9 commit eecfe50
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 3 deletions.
6 changes: 6 additions & 0 deletions python/gui/auto_generated/qgspropertyoverridebutton.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ Updates list of fields.
Set whether the current property override definition is to be used
%End



void aboutToShowMenu();
void menuActionTriggered( QAction *action );


signals:

void changed();
Expand Down
70 changes: 69 additions & 1 deletion src/gui/qgspropertyoverridebutton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@
#include "qgspanelwidget.h"
#include "qgspropertyassistantwidget.h"
#include "qgsauxiliarystorage.h"
#include "qgscolorschemeregistry.h"
#include "qgscolorbutton.h"

#include <QClipboard>
#include <QMenu>
#include <QMouseEvent>
#include <QPointer>
#include <QGroupBox>
#include <QRegularExpression>

QgsPropertyOverrideButton::QgsPropertyOverrideButton( QWidget *parent,
const QgsVectorLayer *layer )
Expand Down Expand Up @@ -69,6 +72,10 @@ QgsPropertyOverrideButton::QgsPropertyOverrideButton( QWidget *parent,
mVariablesMenu = new QMenu( this );
mActionVariables->setMenu( mVariablesMenu );

mActionColors = new QAction( tr( "Color" ), this );
mColorsMenu = new QMenu( this );
mActionColors->setMenu( mColorsMenu );

mActionActive = new QAction( this );
QFont f = mActionActive->font();
f.setBold( true );
Expand Down Expand Up @@ -309,6 +316,7 @@ void QgsPropertyOverrideButton::setToProperty( const QgsProperty &property )
updateGui();
}

///@cond PRIVATE
void QgsPropertyOverrideButton::aboutToShowMenu()
{
mDefineMenu->clear();
Expand Down Expand Up @@ -410,6 +418,52 @@ void QgsPropertyOverrideButton::aboutToShowMenu()
mFieldsMenu->menuAction()->setCheckable( true );
mFieldsMenu->menuAction()->setChecked( fieldActive && mProperty.propertyType() == QgsProperty::FieldBasedProperty && !mProperty.transformer() );

bool colorActive = false;
mColorsMenu->clear();
if ( mDefinition.standardTemplate() == QgsPropertyDefinition::ColorWithAlpha
|| mDefinition.standardTemplate() == QgsPropertyDefinition::ColorNoAlpha )
{
// project colors menu
QAction *colorTitleAct = mDefineMenu->addAction( tr( "Project Color" ) );
colorTitleAct->setFont( titlefont );
colorTitleAct->setEnabled( false );

QList<QgsProjectColorScheme *> projectSchemes;
QgsApplication::colorSchemeRegistry()->schemes( projectSchemes );
if ( projectSchemes.length() > 0 )
{
QgsProjectColorScheme *scheme = projectSchemes.at( 0 );
const QgsNamedColorList colors = scheme->fetchColors();
for ( const auto &color : colors )
{
if ( color.second.isEmpty() )
continue;

QPixmap icon = QgsColorButton::createMenuIcon( color.first, mDefinition.standardTemplate() == QgsPropertyDefinition::ColorWithAlpha );
QAction *act = mColorsMenu->addAction( color.second );
act->setIcon( icon );
if ( mProperty.propertyType() == QgsProperty::ExpressionBasedProperty && hasExp && mExpressionString == QStringLiteral( "project_color('%1')" ).arg( color.second ) )
{
act->setCheckable( true );
act->setChecked( true );
colorActive = true;
}
}
}

if ( mColorsMenu->actions().isEmpty() )
{
QAction *act = mColorsMenu->addAction( tr( "No colors set" ) );
act->setEnabled( false );
}

mDefineMenu->addAction( mActionColors );
mColorsMenu->menuAction()->setCheckable( true );
mColorsMenu->menuAction()->setChecked( colorActive && !mProperty.transformer() );

mDefineMenu->addSeparator();
}

QAction *exprTitleAct = mDefineMenu->addAction( tr( "Expression" ) );
exprTitleAct->setFont( titlefont );
exprTitleAct->setEnabled( false );
Expand Down Expand Up @@ -470,7 +524,7 @@ void QgsPropertyOverrideButton::aboutToShowMenu()
mActionExpression->setText( expString );
}
mDefineMenu->addAction( mActionExpression );
mActionExpression->setChecked( mProperty.propertyType() == QgsProperty::ExpressionBasedProperty && !variableActive && !mProperty.transformer() );
mActionExpression->setChecked( mProperty.propertyType() == QgsProperty::ExpressionBasedProperty && !variableActive && !colorActive && !mProperty.transformer() );

mDefineMenu->addAction( mActionExpDialog );
mDefineMenu->addAction( mActionCopyExpr );
Expand Down Expand Up @@ -582,7 +636,21 @@ void QgsPropertyOverrideButton::menuActionTriggered( QAction *action )
updateGui();
emit changed();
}
else if ( mColorsMenu->actions().contains( action ) ) // a color name clicked
{
if ( mExpressionString != QStringLiteral( "project_color('%1')" ).arg( action->text() ) )
{
mExpressionString = QStringLiteral( "project_color('%1')" ).arg( action->text() );
}
mProperty.setExpressionString( mExpressionString );
mProperty.setTransformer( nullptr );
setActivePrivate( true );
updateSiblingWidgets( isActive() );
updateGui();
emit changed();
}
}
///@endcond

void QgsPropertyOverrideButton::showDescriptionDialog()
{
Expand Down
14 changes: 12 additions & 2 deletions src/gui/qgspropertyoverridebutton.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ class GUI_EXPORT QgsPropertyOverrideButton: public QToolButton
*/
void setActive( bool active );


///@cond PRIVATE

// exposed to Python for testing only
void aboutToShowMenu();
void menuActionTriggered( QAction *action );

///@endcond

signals:

//! Emitted when property definition changes
Expand Down Expand Up @@ -263,6 +272,8 @@ class GUI_EXPORT QgsPropertyOverrideButton: public QToolButton
QMenu *mFieldsMenu = nullptr;
QMenu *mVariablesMenu = nullptr;
QAction *mActionVariables = nullptr;
QMenu *mColorsMenu = nullptr;
QAction *mActionColors = nullptr;

QAction *mActionActive = nullptr;
QAction *mActionDescription = nullptr;
Expand Down Expand Up @@ -312,8 +323,7 @@ class GUI_EXPORT QgsPropertyOverrideButton: public QToolButton
std::shared_ptr< QgsSymbol > mSymbol;

private slots:
void aboutToShowMenu();
void menuActionTriggered( QAction *action );

void showHelp();
void updateSiblingWidgets( bool state );
};
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ ADD_PYTHON_TEST(PyQgsProcessingInPlace test_qgsprocessinginplace.py)
ADD_PYTHON_TEST(PyQgsProcessingAlgDecorator test_processing_alg_decorator.py)
ADD_PYTHON_TEST(PyQgsProjectionSelectionWidgets test_qgsprojectionselectionwidgets.py)
ADD_PYTHON_TEST(PyQgsProjectMetadata test_qgsprojectmetadata.py)
ADD_PYTHON_TEST(PyQgsPropertyOverrideButton test_qgspropertyoverridebutton.py)
ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py)
ADD_PYTHON_TEST(PyQgsRangeWidgets test_qgsrangewidgets.py)
ADD_PYTHON_TEST(PyQgsRasterBandComboBox test_qgsrasterbandcombobox.py)
Expand Down
90 changes: 90 additions & 0 deletions tests/src/python/test_qgspropertyoverridebutton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsPropertyOverrideButton.
.. note:: 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.
"""
__author__ = 'Nyall Dawson'
__date__ = '11/01/2019'
__copyright__ = 'Copyright 2019, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import qgis # NOQA

from qgis.core import (QgsPropertyDefinition,
QgsProperty,
QgsApplication,
QgsProjectColorScheme)

from qgis.gui import (QgsColorButton,
QgsPropertyOverrideButton)

from qgis.testing import start_app, unittest
from qgis.PyQt.QtGui import QColor


start_app()


class TestQgsPropertyOverrideButton(unittest.TestCase):

def testProjectColor(self):
definition = QgsPropertyDefinition('test', 'test', QgsPropertyDefinition.ColorWithAlpha)
button = QgsPropertyOverrideButton()
button.init(0, QgsProperty(), definition)

button.aboutToShowMenu()

self.assertIn('Project Color', [a.text() for a in button.menu().actions()])
self.assertIn('Color', [a.text() for a in button.menu().actions()])
color_action = [a for a in button.menu().actions() if a.text() == 'Color'][0]
self.assertEqual([a.text() for a in color_action.menu().actions()][0], 'No colors set')

# add some project colors
scheme = [s for s in QgsApplication.colorSchemeRegistry().schemes() if isinstance(s, QgsProjectColorScheme)][0]
scheme.setColors([[QColor(255, 0, 0), 'color 1'], [QColor(255, 255, 0), 'burnt marigold']])

button.aboutToShowMenu()
self.assertIn('Project Color', [a.text() for a in button.menu().actions()])
self.assertIn('Color', [a.text() for a in button.menu().actions()])
color_action = [a for a in button.menu().actions() if a.text() == 'Color'][0]
self.assertEqual([a.text() for a in color_action.menu().actions()], ['color 1', 'burnt marigold'])

button.menuActionTriggered(color_action.menu().actions()[1])
self.assertTrue(button.toProperty().isActive())
self.assertEqual(button.toProperty().asExpression(), 'project_color(\'burnt marigold\')')

button.menuActionTriggered(color_action.menu().actions()[0])
self.assertTrue(button.toProperty().isActive())
self.assertEqual(button.toProperty().asExpression(), 'project_color(\'color 1\')')

button.setToProperty(QgsProperty.fromExpression('project_color(\'burnt marigold\')'))
button.aboutToShowMenu()
color_action = [a for a in button.menu().actions() if a.text() == 'Color'][0]
self.assertTrue(color_action.isChecked())
self.assertEqual([a.isChecked() for a in color_action.menu().actions()], [False, True])

# should also see color menu for ColorNoAlpha properties
definition = QgsPropertyDefinition('test', 'test', QgsPropertyDefinition.ColorNoAlpha)
button = QgsPropertyOverrideButton()
button.init(0, QgsProperty(), definition)

button.aboutToShowMenu()
self.assertIn('Project Color', [a.text() for a in button.menu().actions()])
self.assertIn('Color', [a.text() for a in button.menu().actions()])

# but no color menu for other types
definition = QgsPropertyDefinition('test', 'test', QgsPropertyDefinition.Double)
button = QgsPropertyOverrideButton()
button.init(0, QgsProperty(), definition)

button.aboutToShowMenu()
self.assertNotIn('Project Color', [a.text() for a in button.menu().actions()])
self.assertNotIn('Color', [a.text() for a in button.menu().actions()])


if __name__ == '__main__':
unittest.main()

0 comments on commit eecfe50

Please sign in to comment.