[FEATURE] Edit attributes of multiple features simultaneously

This change allows the attributes of multiple features to be edited
simultaneously. It is enabled when the attribute table dialog is in
"form mode", via a new "multi edit" button on the toolbar.

In this mode, attribute value changes will apply to all selected
features. New widgets appear next to each editor widget allowing for
display of the current multi-edit state and for rolling back changes
on a field-by-field basis.

Changes are made as a single edit command, so pressing undo will
rollback the attribute changes for all selected features at once.

Multiedit mode is only available for auto generated and drag and
drop forms - it is not supported by custom ui forms.

Sponsored by Kanton Basel Stadt
This commit is contained in:
Nyall Dawson 2016-03-08 14:11:33 +11:00
parent e2e3fba297
commit 66b51d4a81
30 changed files with 3575 additions and 47 deletions

View File

@ -363,7 +363,7 @@
<file>themes/default/mIconDataDefineExpression.svg</file>
<file>themes/default/mIconDataDefineExpressionError.svg</file>
<file>themes/default/mIconDataDefineExpressionOn.svg</file>
<file>themes/default/mIconDb2.svg</file>
<file>themes/default/mIconDb2.svg</file>
<file>themes/default/mIconDbSchema.png</file>
<file>themes/default/mIconDelete.png</file>
<file>themes/default/mIconDeselected.svg</file>
@ -488,7 +488,7 @@
<file>themes/default/rendererCategorizedSymbol.svg</file>
<file>themes/default/rendererGraduatedSymbol.png</file>
<file>themes/default/rendererGraduatedSymbol.svg</file>
<file>themes/default/rendererNullSymbol.svg</file>
<file>themes/default/rendererNullSymbol.svg</file>
<file>themes/default/rendererSingleSymbol.png</file>
<file>themes/default/rendererSingleSymbol.svg</file>
<file>themes/default/rendererRuleBasedSymbol.svg</file>
@ -576,6 +576,9 @@
<file>icons/qgis-icon-60x60_xmas.png</file>
<file>themes/default/mActionTracing.png</file>
<file>themes/default/vector_grid.png</file>
<file>themes/default/multieditChangedValues.svg</file>
<file>themes/default/multieditMixedValues.svg</file>
<file>themes/default/multieditSameValues.svg</file>
</qresource>
<qresource prefix="/images/tips">
<file alias="symbol_levels.png">qgis_tips/symbol_levels.png</file>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,957 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 4.2333333 4.2333335"
id="svg5477"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="multieditMixedValues.svg">
<defs
id="defs5479">
<inkscape:perspective
id="perspective3486"
inkscape:persp3d-origin="16 : 10.666667 : 1"
inkscape:vp_z="32 : 16 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 16 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective3496" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective3600" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective7871" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective8710" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective9811" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective4762" />
<inkscape:perspective
id="perspective2958"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3012"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective2895"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4025"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4004"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3979"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3958"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3211"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3186"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8121"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8093"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8076"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8047"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient4137-3">
<stop
id="stop4139-9"
offset="0"
style="stop-color:#555753;stop-opacity:1;" />
<stop
id="stop4141-0"
offset="1"
style="stop-color:#555753;stop-opacity:0;" />
</linearGradient>
<inkscape:perspective
id="perspective3274"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8019"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8003"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective7972"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
gradientUnits="userSpaceOnUse"
y2="21"
x2="23"
y1="15"
x1="23"
id="linearGradient4143"
xlink:href="#linearGradient4137-3"
inkscape:collect="always" />
<inkscape:perspective
id="perspective7951"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient4691">
<stop
id="stop4693"
offset="0"
style="stop-color:#8cbe8c;stop-opacity:1;" />
<stop
id="stop4695"
offset="1"
style="stop-color:#a1daa1;stop-opacity:1;" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="5"
x2="4"
y1="9"
x1="10"
id="linearGradient4697"
xlink:href="#linearGradient4691"
inkscape:collect="always" />
<inkscape:perspective
id="perspective7932"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3718"
id="linearGradient3724"
x1="11.5"
y1="23.5"
x2="11.499999"
y2="14.5"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-21,3)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3710"
id="radialGradient3716"
cx="11.72698"
cy="30.008162"
fx="11.72698"
fy="30.008162"
r="13.875"
gradientTransform="matrix(1.90991,2.4623411e-8,-1.6458322e-8,0.64864861,-31.897477,7.035247)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3696"
id="linearGradient3694"
x1="20.5"
y1="30.5"
x2="3.5"
y2="30.5"
gradientUnits="userSpaceOnUse" />
<inkscape:perspective
id="perspective4981"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4921"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4880"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4855"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4866"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2937"
id="linearGradient2947"
gradientUnits="userSpaceOnUse"
x1="7"
y1="17"
x2="2"
y2="22"
gradientTransform="matrix(-1,0,0,-1,26,30)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2937"
id="linearGradient2943"
x1="7"
y1="17"
x2="2"
y2="22"
gradientUnits="userSpaceOnUse" />
<inkscape:perspective
id="perspective6861"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective14200"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective14145"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8320"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8299"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8278"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8225-40"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8225-0"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8225-4"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8225-2"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8225"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8196-6"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8196"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8175"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8138-4"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8138-2"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8138"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8031"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective7227"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective7196"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective7131"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3221"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8612"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8590"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3528"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3494"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3800"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective6018"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5981"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5932"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5892"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5850"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5795"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3034"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective6803"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective2978"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective2842"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective2979"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective2905"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4053"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4032"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4002"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3968"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3929"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3869"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3803"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8279"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8219"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8095"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8057"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective8023"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective7934"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective6979"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 16 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="32 : 16 : 1"
inkscape:persp3d-origin="16 : 10.666667 : 1"
id="perspective3257" />
<linearGradient
id="linearGradient2835">
<stop
style="stop-color:#ccf2a6;stop-opacity:1;"
offset="0"
id="stop2837" />
<stop
style="stop-color:#8ae234;stop-opacity:1;"
offset="1"
id="stop2839" />
</linearGradient>
<linearGradient
id="linearGradient2843">
<stop
style="stop-color:#eeeeec;stop-opacity:1;"
offset="0"
id="stop2845" />
<stop
style="stop-color:#c8c8c2;stop-opacity:1;"
offset="1"
id="stop2847" />
</linearGradient>
<linearGradient
id="linearGradient7614">
<stop
style="stop-color:#d3d7cf;stop-opacity:1;"
offset="0"
id="stop7616" />
<stop
id="stop7636"
offset="0.40000001"
style="stop-color:#d3d7cf;stop-opacity:0.38666666;" />
<stop
style="stop-color:#d3d7cf;stop-opacity:0;"
offset="0.5"
id="stop7638" />
<stop
id="stop7622"
offset="0.60000002"
style="stop-color:#d3d7cf;stop-opacity:0.39215687;" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1;"
offset="1"
id="stop7618" />
</linearGradient>
<linearGradient
id="linearGradient7624">
<stop
style="stop-color:#555753;stop-opacity:1;"
offset="0"
id="stop7626" />
<stop
id="stop7634"
offset="0.40000001"
style="stop-color:#555753;stop-opacity:0.39215687;" />
<stop
style="stop-color:#555753;stop-opacity:0;"
offset="0.5"
id="stop7640" />
<stop
id="stop7632"
offset="0.60000002"
style="stop-color:#555753;stop-opacity:0.39215687;" />
<stop
style="stop-color:#555753;stop-opacity:1;"
offset="1"
id="stop7628" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient2937">
<stop
style="stop-color:#ce5c00;stop-opacity:1;"
offset="0"
id="stop2939" />
<stop
style="stop-color:#ce5c00;stop-opacity:0;"
offset="1"
id="stop2941" />
</linearGradient>
<linearGradient
id="linearGradient3688">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3690" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3692" />
</linearGradient>
<linearGradient
id="linearGradient3696">
<stop
id="stop3698"
offset="0"
style="stop-color:#ff0000;stop-opacity:1;" />
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0.5"
id="stop3702" />
<stop
id="stop3700"
offset="1"
style="stop-color:#ffffff;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3710">
<stop
style="stop-color:#9a9c96;stop-opacity:1;"
offset="0"
id="stop3712" />
<stop
style="stop-color:#eff0ef;stop-opacity:1;"
offset="1"
id="stop3714" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3718">
<stop
style="stop-color:#9a9c96;stop-opacity:1"
offset="0"
id="stop3720" />
<stop
style="stop-color:#d8d9d7;stop-opacity:1"
offset="1"
id="stop3722" />
</linearGradient>
<inkscape:path-effect
effect="spiro"
id="path-effect3780"
is_visible="true" />
<inkscape:path-effect
is_visible="true"
id="path-effect3798"
effect="spiro" />
<inkscape:path-effect
is_visible="true"
id="path-effect3802"
effect="spiro" />
<inkscape:path-effect
is_visible="true"
id="path-effect3804"
effect="spiro" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="31.678384"
inkscape:cx="10.824355"
inkscape:cy="8.1987133"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1855"
inkscape:window-height="1056"
inkscape:window-x="1345"
inkscape:window-y="24"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4283" />
</sodipodi:namedview>
<metadata
id="metadata5482">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-292.76665)">
<rect
style="fill:#ffec37;fill-opacity:1;stroke:#d3a600;stroke-width:0.29104168;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-linejoin:round;stroke-linecap:round"
id="rect4286-7"
width="2.1166668"
height="1.8520833"
x="-209.15501"
y="207.17773"
transform="matrix(0.7035026,-0.71069268,0.71069268,0.7035026,0,0)" />
<path
inkscape:connector-curvature="0"
style="display:inline;fill:#eeeeec;fill-opacity:1;stroke:#888a85;stroke-width:0.26458338;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
d="m 90.285228,76.772541 5.027083,0 0,5.027081 -5.027083,0 0,-5.027081 z"
id="rect4012-9-8-9"
sodipodi:nodetypes="ccccc" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#f7941d;fill-opacity:1;fill-rule:evenodd;stroke:#f7941d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:2.20000005;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect4471-25-2"
width="1.3229116"
height="1.3229089"
x="91.954605"
y="79.784637" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#f7941d;fill-opacity:1;fill-rule:evenodd;stroke:#f7941d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:2.20000005;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect4471-2-7-1"
width="1.3229116"
height="1.3229089"
x="91.947098"
y="77.489044" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b2b2b2;stroke-width:0.26078698;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.072268,76.903561 0,3.68319"
id="path4488-8-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b2b2b2;stroke-width:0.27906913;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.124288,78.150481 0.57817,0"
id="path4490-0-9"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b2b2b2;stroke-width:0.27906913;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.124288,80.446081 0.57817,0"
id="path4490-8-8-2"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cccccccc"
id="path2848-7-3"
d="m 94.223935,76.771801 1.455142,1.45514 -0.970095,0 -2e-6,2.457481 -0.970094,0 0,-2.457481 -0.970094,0 z"
style="display:inline;fill:#6d97c4;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
inkscape:connector-curvature="0" />
<rect
style="fill:#fff17c;fill-opacity:1;stroke:#d3a600;stroke-width:0.26454803;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4286"
width="2.1167021"
height="1.5875354"
x="1.1906073"
y="294.22186" />
<ellipse
style="fill:#ffe147;fill-opacity:1;stroke:#d3a600;stroke-width:0.32120418;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4309"
cx="2.9772344"
cy="295.81909"
rx="1.0815376"
ry="1.0648332" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 4.2333333 4.2333335"
id="svg5477"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="multieditSameValues.svg">
<defs
id="defs5479" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="31.678384"
inkscape:cx="6.1839667"
inkscape:cy="9.0510295"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1855"
inkscape:window-height="1056"
inkscape:window-x="1345"
inkscape:window-y="24"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4283" />
</sodipodi:namedview>
<metadata
id="metadata5482">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-292.76665)">
<rect
style="fill:#aeed9b;fill-opacity:1;stroke:#5ab220;stroke-width:0.26454803;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4286-0"
width="2.1167021"
height="1.5875354"
x="0.39685723"
y="293.16351" />
<path
inkscape:connector-curvature="0"
style="display:inline;fill:#eeeeec;fill-opacity:1;stroke:#888a85;stroke-width:0.26458338;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
d="m 90.285228,76.772541 5.027083,0 0,5.027081 -5.027083,0 0,-5.027081 z"
id="rect4012-9-8-9"
sodipodi:nodetypes="ccccc" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#f7941d;fill-opacity:1;fill-rule:evenodd;stroke:#f7941d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:2.20000005;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect4471-25-2"
width="1.3229116"
height="1.3229089"
x="91.954605"
y="79.784637" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#f7941d;fill-opacity:1;fill-rule:evenodd;stroke:#f7941d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:2.20000005;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect4471-2-7-1"
width="1.3229116"
height="1.3229089"
x="91.947098"
y="77.489044" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b2b2b2;stroke-width:0.26078698;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.072268,76.903561 0,3.68319"
id="path4488-8-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b2b2b2;stroke-width:0.27906913;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.124288,78.150481 0.57817,0"
id="path4490-0-9"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b2b2b2;stroke-width:0.27906913;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.124288,80.446081 0.57817,0"
id="path4490-8-8-2"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cccccccc"
id="path2848-7-3"
d="m 94.223935,76.771801 1.455142,1.45514 -0.970095,0 -2e-6,2.457481 -0.970094,0 0,-2.457481 -0.970094,0 z"
style="display:inline;fill:#6d97c4;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
inkscape:connector-curvature="0" />
<rect
style="fill:#aeed9b;fill-opacity:1;stroke:#5ab220;stroke-width:0.26454803;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4286"
width="2.1167021"
height="1.5875354"
x="1.1906073"
y="294.22183" />
<rect
style="fill:#aeed9b;fill-opacity:1;stroke:#5ab220;stroke-width:0.26454803;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4286-5"
width="2.1167021"
height="1.5875354"
x="1.9843572"
y="295.28018" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -47,9 +47,17 @@ class QgsDualView : QStackedWidget
* Change the current view mode.
*
* @param view The view mode to set
* @see view()
*/
void setView( ViewMode view );
/**
* Returns the current view mode.
* @see setView()
* @note added in QGIS 2.16
*/
ViewMode view() const;
/**
* Set the filter mode
*
@ -130,6 +138,11 @@ class QgsDualView : QStackedWidget
void openConditionalStyles();
/** Sets whether multi edit mode is enabled.
* @note added in QGIS 2.16
*/
void setMultiEditEnabled( bool enabled );
signals:
/**
* Is emitted, whenever the display expression is successfully changed

View File

@ -0,0 +1,75 @@
/** \ingroup gui
* \class QgsMultiEditToolButton
* A tool button widget which is displayed next to editor widgets in attribute forms, and
* allows for controlling how the widget behaves and interacts with the form while in multi
* edit mode.
* \note Added in version 2.16
*/
class QgsMultiEditToolButton : QToolButton
{
%TypeHeaderCode
#include <qgsmultiedittoolbutton.h>
%End
public:
//! Button states
enum State
{
Default, /*!< Default state, all features have same value for widget */
MixedValues, /*!< Mixed state, some features have different values for the widget */
Changed, /*!< Value for widget has changed but changes have not yet been committed */
};
/** Constructor for QgsMultiEditToolButton.
* @param parent parent object
*/
explicit QgsMultiEditToolButton( QWidget *parent /TransferThis/ = nullptr );
/** Returns the current displayed state of the button.
*/
State state() const;
/** Sets the field associated with this button. This is used to customise the widget menu
* and tooltips to match the field properties.
* @param field associated field
*/
void setField( const QgsField& field );
public slots:
/** Sets whether the associated field contains mixed values.
* @param mixed whether field values are mixed
* @see isMixed()
* @see setIsChanged()
* @see resetChanges()
*/
void setIsMixed( bool mixed );
/** Sets whether the associated field has changed.
* @param changed whether field has changed
* @see isChanged()
* @see setIsMixed()
* @see resetChanges()
*/
void setIsChanged( bool changed );
/** Resets the changed state for the field.
* @see setIsMixed()
* @see setIsChanged()
* @see changesCommitted()
*/
void resetChanges();
/** Called when field values have been changed and field now contains all the same values.
* @see resetChanges()
*/
void changesCommitted();
signals:
//! Emitted when the "set field value for all features" option is selected.
void setFieldValueTriggered();
//! Emitted when the "reset to original values" option is selected.
void resetFieldValueTriggered();
};

View File

@ -30,6 +30,7 @@
%Include qgsattributeeditor.sip
%Include qgsattributeeditorcontext.sip
%Include qgsattributeform.sip
%Include qgsattributeformeditorwidget.sip
%Include qgsattributeforminterface.sip
%Include qgsattributetypeloaddialog.sip
%Include qgsbrowsertreeview.sip
@ -244,6 +245,7 @@
%Include editorwidgets/core/qgswidgetwrapper.sip
%Include editorwidgets/qgsdatetimeedit.sip
%Include editorwidgets/qgsdoublespinbox.sip
%Include editorwidgets/qgsmultiedittoolbutton.sip
%Include editorwidgets/qgsrelationreferencewidget.sip
%Include editorwidgets/qgsrelationreferencewidgetwrapper.sip
%Include editorwidgets/qgsrelationwidgetwrapper.sip

View File

@ -74,8 +74,16 @@ class QgsAttributeDialog : QDialog
* If set to true, the dialog will add a new feature when the form is accepted.
*
* @param isAddDialog If set to true, turn this dialog into an add feature dialog.
* @deprecated use setMode() instead
*/
void setIsAddDialog( bool isAddDialog );
void setIsAddDialog( bool isAddDialog ) /Deprecated/;
/**
* Toggles the form mode.
* @param mode form mode. Eg if set to QgsAttributeForm::AddFeatureMode, the dialog will be editable even with an invalid feature and
* will add a new feature when the form is accepted.
*/
void setMode( QgsAttributeForm::Mode mode );
/**
* Sets the edit command message (Undo) that will be used when the dialog is accepted

View File

@ -20,6 +20,15 @@ class QgsAttributeForm : QWidget
%End
public:
//! Form modes
enum Mode
{
SingleEditMode, /*!< Single edit mode, for editing a single feature */
MultiEditMode, /*!< Multi edit mode, for editing fields of multiple features at once */
// TODO: SearchMode, /*!< Form values are used for searching/filtering the layer */
};
explicit QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature& feature = QgsFeature(), const QgsAttributeEditorContext& context = QgsAttributeEditorContext(), QWidget *parent /TransferThis/ = 0 );
~QgsAttributeForm();
@ -61,6 +70,19 @@ class QgsAttributeForm : QWidget
*/
bool editable();
/** Returns the current mode of the form.
* @note added in QGIS 2.16
* @see setMode()
*/
Mode mode() const;
/** Sets the current mode of the form.
* @param mode form mode
* @note added in QGIS 2.16
* @see mode()
*/
void setMode( Mode mode );
/**
* Toggles the form mode between edit feature and add feature.
* If set to true, the dialog will be editable even with an invalid feature.
@ -87,6 +109,12 @@ class QgsAttributeForm : QWidget
*/
bool eventFilter( QObject* object, QEvent* event );
/** Sets all feature IDs which are to be edited if the form is in multiedit mode
* @param fids feature ID list
* @note added in QGIS 2.16
*/
void setMultiEditFeatureIds( const QgsFeatureIds& fids );
signals:
/**
* Notifies about changes of attributes

View File

@ -0,0 +1,78 @@
/** \ingroup gui
* \class QgsAttributeFormEditorWidget
* A widget consisting of both an editor widget and additional widgets for controlling the behaviour
* of the editor widget depending on a number of possible modes. For instance, if the parent attribute
* form is in the multi edit mode, this widget will show both the editor widget and a tool button for
* controlling the multi edit results.
* \note Added in version 2.16
*/
class QgsAttributeFormEditorWidget : QWidget
{
%TypeHeaderCode
#include <qgsattributeformeditorwidget.h>
%End
public:
//! Widget modes
enum Mode
{
DefaultMode, /*!< Default mode, only the editor widget is shown */
MultiEditMode, /*!< Multi edit mode, both the editor widget and a QgsMultiEditToolButton is shown */
// TODO: SearchMode, /*!< Layer search/filter mode */
};
/** Constructor for QgsAttributeFormEditorWidget.
* @param editorWidget associated editor widget wrapper
* @param form parent attribute form
*/
explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form /TransferThis/ );
~QgsAttributeFormEditorWidget();
/** Sets the current mode for the widget. The widget will adapt its state and visible widgets to
* reflect the updated mode. Eg, showing multi edit tool buttons if the mode is set to MultiEditMode.
* @param mode widget mode
* @see mode()
*/
void setMode( Mode mode );
/** Returns the current mode for the widget.
* @see setMode()
*/
Mode mode() const;
/** Resets the widget to an initial value.
* @param initialValue initial value to show in widget
* @param mixedValue set to true to initially show the mixed values state
*/
void initialize( const QVariant& initialValue, bool mixedValues = false );
/** Returns true if the widget's value has been changed since it was initialized.
* @see initialize()
*/
bool hasChanged() const;
/** Returns the current value of the attached editor widget.
*/
QVariant currentValue() const;
public slots:
/** Sets whether the widget should be displayed in a "mixed values" mode.
* @param mixed set to true to show in a mixed values state
*/
void setIsMixed( bool mixed );
/** Called when field values have been committed;
*/
void changesCommitted();
signals:
//! Emitted when the widget's value changes
//! @param value new widget value
void valueChanged( const QVariant& value );
};

View File

@ -62,6 +62,15 @@ static QgsExpressionContext _getExpressionContext( const void* context )
return expContext;
}
void QgsAttributeTableDialog::updateMultiEditButtonState()
{
mToggleMultiEditButton->setEnabled( mLayer->isEditable() );
if ( mLayer->isEditable() && mMainView->view() != QgsDualView::AttributeEditor )
{
mToggleMultiEditButton->setChecked( false );
}
}
QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *theLayer, QWidget *parent, Qt::WindowFlags flags )
: QDialog( parent, flags )
, mDock( nullptr )
@ -246,6 +255,9 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *theLayer, QWid
mMainView->setView( static_cast< QgsDualView::ViewMode >( initialView ) );
mMainViewButtonGroup->button( initialView )->setChecked( true );
connect( mToggleMultiEditButton, SIGNAL( toggled( bool ) ), mMainView, SLOT( setMultiEditEnabled( bool ) ) );
updateMultiEditButtonState();
editingToggled();
}
@ -648,6 +660,7 @@ void QgsAttributeTableDialog::on_mDeleteSelectedButton_clicked()
void QgsAttributeTableDialog::on_mMainView_currentChanged( int viewMode )
{
mMainViewButtonGroup->button( viewMode )->click();
updateMultiEditButtonState();
QSettings s;
s.setValue( "/qgis/attributeTableLastView", static_cast< int >( viewMode ) );
@ -670,6 +683,7 @@ void QgsAttributeTableDialog::editingToggled()
mToggleEditingButton->setChecked( mLayer->isEditable() );
mSaveEditsButton->setEnabled( mLayer->isEditable() );
mReloadButton->setEnabled( ! mLayer->isEditable() );
updateMultiEditButtonState();
mToggleEditingButton->blockSignals( false );
bool canChangeAttributes = mLayer->dataProvider()->capabilities() & QgsVectorDataProvider::ChangeAttributeValues;

View File

@ -224,6 +224,8 @@ class APP_EXPORT QgsAttributeTableDialog : public QDialog, private Ui::QgsAttrib
QgsRubberBand* mRubberBand;
QgsSearchWidgetWrapper* mCurrentSearchWidgetWrapper;
void updateMultiEditButtonState();
friend class TestQgsAttributeTable;
};

View File

@ -111,7 +111,7 @@ bool QgsFeatureAction::editFeature( bool showModal )
QgsAttributeDialog *dialog = newDialog( false );
if ( !mFeature->isValid() )
dialog->setIsAddDialog( true );
dialog->setMode( QgsAttributeForm::AddFeatureMode );
if ( showModal )
{
@ -193,7 +193,7 @@ bool QgsFeatureAction::addFeature( const QgsAttributeMap& defaultAttributes, boo
else
{
QgsAttributeDialog *dialog = newDialog( false );
dialog->setIsAddDialog( true );
dialog->setMode( QgsAttributeForm::AddFeatureMode );
dialog->setEditCommandMessage( text() );
connect( dialog->attributeForm(), SIGNAL( featureSaved( const QgsFeature & ) ), this, SLOT( onFeatureSaved( const QgsFeature & ) ) );

View File

@ -157,7 +157,7 @@ void QgsMapToolFillRing::cadCanvasReleaseEvent( QgsMapMouseEvent * e )
else
{
QgsAttributeDialog *dialog = new QgsAttributeDialog( vlayer, ft, false, nullptr, true );
dialog->setIsAddDialog( true );
dialog->setMode( QgsAttributeForm::AddFeatureMode );
res = dialog->exec(); // will also add the feature
}

View File

@ -112,6 +112,7 @@ SET(QGIS_GUI_SRCS
editorwidgets/qgsfilenamewidgetfactory.cpp
editorwidgets/qgshiddenwidgetwrapper.cpp
editorwidgets/qgshiddenwidgetfactory.cpp
editorwidgets/qgsmultiedittoolbutton.cpp
editorwidgets/qgsphotoconfigdlg.cpp
editorwidgets/qgsphotowidgetwrapper.cpp
editorwidgets/qgsphotowidgetfactory.cpp
@ -155,6 +156,7 @@ SET(QGIS_GUI_SRCS
qgsattributedialog.cpp
qgsattributeeditor.cpp
qgsattributeform.cpp
qgsattributeformeditorwidget.cpp
qgsattributeforminterface.cpp
qgsattributeformlegacyinterface.cpp
qgsattributetypeloaddialog.cpp
@ -301,6 +303,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsattributedialog.h
qgsattributeeditor.h
qgsattributeform.h
qgsattributeformeditorwidget.h
qgsattributetypeloaddialog.h
qgsblendmodecombobox.h
qgsbrowsertreeview.h
@ -514,6 +517,7 @@ SET(QGIS_GUI_MOC_HDRS
editorwidgets/qgsexternalresourcewidgetwrapper.h
editorwidgets/qgsfilenamewidgetwrapper.h
editorwidgets/qgshiddenwidgetwrapper.h
editorwidgets/qgsmultiedittoolbutton.h
editorwidgets/qgsphotoconfigdlg.h
editorwidgets/qgsphotowidgetwrapper.h
editorwidgets/qgsrangeconfigdlg.h

View File

@ -203,6 +203,11 @@ void QgsDualView::setView( QgsDualView::ViewMode view )
setCurrentIndex( view );
}
QgsDualView::ViewMode QgsDualView::view() const
{
return static_cast< QgsDualView::ViewMode >( currentIndex() );
}
void QgsDualView::setFilterMode( QgsAttributeTableFilterModel::FilterMode filterMode )
{
mFilterModel->setFilterMode( filterMode );
@ -290,6 +295,14 @@ void QgsDualView::openConditionalStyles()
mConditionalFormatWidget->viewRules();
}
void QgsDualView::setMultiEditEnabled( bool enabled )
{
if ( enabled )
setView( AttributeEditor );
mAttributeForm->setMode( enabled ? QgsAttributeForm::MultiEditMode : QgsAttributeForm::SingleEditMode );
}
void QgsDualView::previewExpressionBuilder()
{
// Show expression builder

View File

@ -85,9 +85,17 @@ class GUI_EXPORT QgsDualView : public QStackedWidget, private Ui::QgsDualViewBas
* Change the current view mode.
*
* @param view The view mode to set
* @see view()
*/
void setView( ViewMode view );
/**
* Returns the current view mode.
* @see setView()
* @note added in QGIS 2.16
*/
ViewMode view() const;
/**
* Set the filter mode
*
@ -165,6 +173,11 @@ class GUI_EXPORT QgsDualView : public QStackedWidget, private Ui::QgsDualViewBas
void openConditionalStyles();
/** Sets whether multi edit mode is enabled.
* @note added in QGIS 2.16
*/
void setMultiEditEnabled( bool enabled );
signals:
/**
* Is emitted, whenever the display expression is successfully changed

View File

@ -82,6 +82,7 @@ void QgsFeatureListView::setModel( QgsFeatureListModel* featureListModel )
connect( mCurrentEditSelectionModel, SIGNAL( selectionChanged( QItemSelection, QItemSelection ) ), SLOT( editSelectionChanged( QItemSelection, QItemSelection ) ) );
connect( mModel->layerCache()->layer(), SIGNAL( attributeValueChanged( QgsFeatureId, int, QVariant ) ), this, SLOT( repaintRequested() ) );
}
bool QgsFeatureListView::setDisplayExpression( const QString& expression )

View File

@ -0,0 +1,115 @@
/***************************************************************************
qgsmultiedittoolbutton.cpp
--------------------------
Date : March 2016
Copyright : (C) 2016 Nyall Dawson
Email : nyall dot dawson at gmail.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 "qgsmultiedittoolbutton.h"
#include "qgsapplication.h"
#include <QMenu>
QgsMultiEditToolButton::QgsMultiEditToolButton( QWidget* parent )
: QToolButton( parent )
, mIsMixedValues( false )
, mIsChanged( false )
, mState( Default )
, mMenu( nullptr )
{
setFocusPolicy( Qt::StrongFocus );
// set default tool button icon properties
setFixedSize( 22, 22 );
setStyleSheet( QString( "QToolButton{ background: none; border: 1px solid rgba(0, 0, 0, 0%);} QToolButton:focus { border: 1px solid palette(highlight); }" ) );
setIconSize( QSize( 16, 16 ) );
setPopupMode( QToolButton::InstantPopup );
mMenu = new QMenu( this );
connect( mMenu, SIGNAL( aboutToShow() ), this, SLOT( aboutToShowMenu() ) );
setMenu( mMenu );
// sets initial appearance
updateState();
}
void QgsMultiEditToolButton::aboutToShowMenu()
{
mMenu->clear();
switch ( mState )
{
case Default:
{
QAction* noAction = mMenu->addAction( tr( "No changes to commit" ) );
noAction->setEnabled( false );
break;
}
case MixedValues:
{
QString title = !mField.name().isEmpty() ? tr( "Set %1 for all selected features" ).arg( mField.name() )
: tr( "Set field for all selected features" );
QAction* setFieldAction = mMenu->addAction( title );
connect( setFieldAction, SIGNAL( triggered( bool ) ), this, SLOT( setFieldTriggered() ) );
break;
}
case Changed:
{
QAction* resetFieldAction = mMenu->addAction( tr( "Reset to original values" ) );
connect( resetFieldAction, SIGNAL( triggered( bool ) ), this, SLOT( resetFieldTriggered() ) );
break;
}
}
}
void QgsMultiEditToolButton::setFieldTriggered()
{
mIsChanged = true;
updateState();
emit setFieldValueTriggered();
}
void QgsMultiEditToolButton::resetFieldTriggered()
{
mIsChanged = false;
updateState();
emit resetFieldValueTriggered();
}
void QgsMultiEditToolButton::updateState()
{
//changed state takes priority over mixed values state
if ( mIsChanged )
mState = Changed;
else if ( mIsMixedValues )
mState = MixedValues;
else
mState = Default;
QIcon icon;
QString tooltip;
switch ( mState )
{
case Default:
icon = QgsApplication::getThemeIcon( "/multieditSameValues.svg" );
tooltip = tr( "All features in selection have equal value for '%1'" ).arg( mField.name() );
break;
case MixedValues:
icon = QgsApplication::getThemeIcon( "/multieditMixedValues.svg" );
tooltip = tr( "Some features in selection have different values for '%1'" ).arg( mField.name() );
break;
case Changed:
icon = QgsApplication::getThemeIcon( "/multieditChangedValues.svg" );
tooltip = tr( "Values for '%1' have unsaved changes" ).arg( mField.name() );
break;
}
setIcon( icon );
setToolTip( tooltip );
}

View File

@ -0,0 +1,115 @@
/***************************************************************************
qgsmultiedittoolbutton.h
------------------------
Date : March 2016
Copyright : (C) 2016 Nyall Dawson
Email : nyall dot dawson at gmail.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 QGSMULTIEDITTOOLBUTTON_H
#define QGSMULTIEDITTOOLBUTTON_H
#include "qgsfield.h"
#include <QToolButton>
/** \ingroup gui
* \class QgsMultiEditToolButton
* A tool button widget which is displayed next to editor widgets in attribute forms, and
* allows for controlling how the widget behaves and interacts with the form while in multi
* edit mode.
* \note Added in version 2.16
*/
class GUI_EXPORT QgsMultiEditToolButton : public QToolButton
{
Q_OBJECT
public:
//! Button states
enum State
{
Default, /*!< Default state, all features have same value for widget */
MixedValues, /*!< Mixed state, some features have different values for the widget */
Changed, /*!< Value for widget has changed but changes have not yet been committed */
};
/** Constructor for QgsMultiEditToolButton.
* @param parent parent object
*/
explicit QgsMultiEditToolButton( QWidget *parent = nullptr );
/** Returns the current displayed state of the button.
*/
State state() const { return mState; }
/** Sets the field associated with this button. This is used to customise the widget menu
* and tooltips to match the field properties.
* @param field associated field
*/
void setField( const QgsField& field ) { mField = field; }
public slots:
/** Sets whether the associated field contains mixed values.
* @param mixed whether field values are mixed
* @see isMixed()
* @see setIsChanged()
* @see resetChanges()
*/
void setIsMixed( bool mixed ) { mIsMixedValues = mixed; updateState(); }
/** Sets whether the associated field has changed.
* @param changed whether field has changed
* @see isChanged()
* @see setIsMixed()
* @see resetChanges()
*/
void setIsChanged( bool changed ) { mIsChanged = changed; updateState(); }
/** Resets the changed state for the field.
* @see setIsMixed()
* @see setIsChanged()
* @see changesCommitted()
*/
void resetChanges() { mIsChanged = false; updateState(); }
/** Called when field values have been changed and field now contains all the same values.
* @see resetChanges()
*/
void changesCommitted() { mIsMixedValues = false; mIsChanged = false; updateState(); }
signals:
//! Emitted when the "set field value for all features" option is selected.
void setFieldValueTriggered();
//! Emitted when the "reset to original values" option is selected.
void resetFieldValueTriggered();
private slots:
void aboutToShowMenu();
void setFieldTriggered();
void resetFieldTriggered();
private:
bool mIsMixedValues;
bool mIsChanged;
State mState;
QgsField mField;
QMenu* mMenu;
void updateState();
};
#endif // QGSMULTIEDITTOOLBUTTON_H

View File

@ -108,8 +108,16 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog
* If set to true, the dialog will add a new feature when the form is accepted.
*
* @param isAddDialog If set to true, turn this dialog into an add feature dialog.
* @deprecated use setMode() instead
*/
void setIsAddDialog( bool isAddDialog ) { mAttributeForm->setIsAddDialog( isAddDialog ); }
Q_DECL_DEPRECATED void setIsAddDialog( bool isAddDialog ) { mAttributeForm->setMode( isAddDialog ? QgsAttributeForm::AddFeatureMode : QgsAttributeForm::SingleEditMode ); }
/**
* Toggles the form mode.
* @param mode form mode. Eg if set to QgsAttributeForm::AddFeatureMode, the dialog will be editable even with an invalid feature and
* will add a new feature when the form is accepted.
*/
void setMode( QgsAttributeForm::Mode mode ) { mAttributeForm->setMode( mode ); }
/**
* Sets the edit command message (Undo) that will be used when the dialog is accepted

View File

@ -23,6 +23,9 @@
#include "qgspythonrunner.h"
#include "qgsrelationwidgetwrapper.h"
#include "qgsvectordataprovider.h"
#include "qgsattributeformeditorwidget.h"
#include "qgsmessagebar.h"
#include "qgsmessagebaritem.h"
#include <QDir>
#include <QTextStream>
@ -38,19 +41,25 @@
#include <QTabWidget>
#include <QUiLoader>
#include <QMessageBox>
#include <QSettings>
int QgsAttributeForm::sFormCounter = 0;
QgsAttributeForm::QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature &feature, const QgsAttributeEditorContext &context, QWidget* parent )
: QWidget( parent )
, mLayer( vl )
, mMessageBar( nullptr )
, mMultiEditUnsavedMessageBarItem( nullptr )
, mMultiEditMessageBarItem( nullptr )
, mContext( context )
, mButtonBox( nullptr )
, mFormNr( sFormCounter++ )
, mIsSaving( false )
, mIsAddDialog( false )
, mPreventFeatureRefresh( false )
, mIsSettingFeature( false )
, mIsSettingMultiEditFeatures( false )
, mEditCommandMessage( tr( "Attributes changed" ) )
, mMode( SingleEditMode )
{
init();
initPython();
@ -65,6 +74,7 @@ QgsAttributeForm::QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature &featur
connect( vl, SIGNAL( updatedFields() ), this, SLOT( onUpdatedFields() ) );
connect( vl, SIGNAL( beforeAddingExpressionField( QString ) ), this, SLOT( preventFeatureRefresh() ) );
connect( vl, SIGNAL( beforeRemovingExpressionField( int ) ), this, SLOT( preventFeatureRefresh() ) );
connect( vl, SIGNAL( selectionChanged() ), this, SLOT( layerSelectionChanged() ) );
}
QgsAttributeForm::~QgsAttributeForm()
@ -78,7 +88,7 @@ void QgsAttributeForm::hideButtonBox()
mButtonBox->hide();
// Make sure that changes are taken into account if somebody tries to figure out if there have been some
if ( !mIsAddDialog )
if ( mMode == SingleEditMode )
connect( mLayer, SIGNAL( beforeModifiedCheck() ), this, SLOT( save() ) );
}
@ -105,9 +115,72 @@ bool QgsAttributeForm::editable()
return mFeature.isValid() && mLayer->isEditable();
}
void QgsAttributeForm::setMode( QgsAttributeForm::Mode mode )
{
if ( mode == mMode )
return;
if ( mMode == MultiEditMode )
{
//switching out of multi edit mode triggers a save
save();
}
mMode = mode;
if ( mButtonBox->isVisible() && mMode == SingleEditMode )
{
connect( mLayer, SIGNAL( beforeModifiedCheck() ), this, SLOT( save() ) );
}
else
{
disconnect( mLayer, SIGNAL( beforeModifiedCheck() ), this, SLOT( save() ) );
}
//update all form editor widget modes to match
Q_FOREACH ( QgsAttributeFormEditorWidget* w, findChildren< QgsAttributeFormEditorWidget* >() )
{
switch ( mode )
{
case QgsAttributeForm::SingleEditMode:
w->setMode( QgsAttributeFormEditorWidget::DefaultMode );
break;
case QgsAttributeForm::AddFeatureMode:
w->setMode( QgsAttributeFormEditorWidget::DefaultMode );
break;
case QgsAttributeForm::MultiEditMode:
w->setMode( QgsAttributeFormEditorWidget::MultiEditMode );
break;
#if 0
case QgsAttributeForm::SearchMode:
w->setMode( QgsAttributeFormEditorWidget::SearchMode );
break;
#endif
}
}
switch ( mode )
{
case QgsAttributeForm::SingleEditMode:
setFeature( mFeature );
break;
case QgsAttributeForm::AddFeatureMode:
break;
case QgsAttributeForm::MultiEditMode:
resetMultiEdit( false );
break;
}
}
void QgsAttributeForm::setIsAddDialog( bool isAddDialog )
{
mIsAddDialog = isAddDialog;
setMode( isAddDialog ? AddFeatureMode : SingleEditMode );
synchronizeEnabledState();
}
@ -126,44 +199,47 @@ void QgsAttributeForm::changeAttribute( const QString& field, const QVariant& va
void QgsAttributeForm::setFeature( const QgsFeature& feature )
{
mIsSettingFeature = true;
mFeature = feature;
resetValues();
synchronizeEnabledState();
Q_FOREACH ( QgsAttributeFormInterface* iface, mInterfaces )
switch ( mMode )
{
iface->featureChanged();
case SingleEditMode:
case AddFeatureMode:
{
resetValues();
synchronizeEnabledState();
Q_FOREACH ( QgsAttributeFormInterface* iface, mInterfaces )
{
iface->featureChanged();
}
break;
}
case MultiEditMode:
{
//ignore setFeature
break;
}
}
mIsSettingFeature = false;
}
bool QgsAttributeForm::save()
bool QgsAttributeForm::saveEdits()
{
if ( mIsSaving )
return true;
mIsSaving = true;
bool changedLayer = false;
bool success = true;
emit beforeSave( success );
// Somebody wants to prevent this form from saving
if ( !success )
return false;
bool changedLayer = false;
QgsFeature updatedFeature = QgsFeature( mFeature );
if ( mFeature.isValid() || mIsAddDialog )
if ( mFeature.isValid() || mMode == AddFeatureMode )
{
bool doUpdate = false;
// An add dialog should perform an action by default
// and not only if attributes have "changed"
if ( mIsAddDialog )
if ( mMode == AddFeatureMode )
doUpdate = true;
QgsAttributes src = mFeature.attributes();
@ -199,7 +275,7 @@ bool QgsAttributeForm::save()
if ( doUpdate )
{
if ( mIsAddDialog )
if ( mMode == AddFeatureMode )
{
mFeature.setValid( true );
mLayer->beginEditCommand( mEditCommandMessage );
@ -208,7 +284,7 @@ bool QgsAttributeForm::save()
{
mFeature.setAttributes( updatedFeature.attributes() );
mLayer->endEditCommand();
mIsAddDialog = false;
setMode( SingleEditMode );
changedLayer = true;
}
else
@ -262,6 +338,120 @@ bool QgsAttributeForm::save()
if ( changedLayer )
mLayer->triggerRepaint();
return success;
}
void QgsAttributeForm::resetMultiEdit( bool promptToSave )
{
if ( promptToSave )
save();
setMultiEditFeatureIds( mLayer->selectedFeaturesIds() );
}
void QgsAttributeForm::multiEditMessageClicked( const QString& link )
{
clearMultiEditMessages();
resetMultiEdit( link == "#apply" );
}
bool QgsAttributeForm::saveMultiEdits()
{
//find changed attributes
QgsAttributeMap newAttributeValues;
QMap< int, QgsAttributeFormEditorWidget* >::const_iterator wIt = mFormEditorWidgets.constBegin();
for ( ; wIt != mFormEditorWidgets.constEnd(); ++ wIt )
{
QgsAttributeFormEditorWidget* w = wIt.value();
if ( !w->hasChanged() )
continue;
if ( !w->currentValue().isValid() // if the widget returns invalid (== do not change)
|| mLayer->editFormConfig()->readOnly( wIt.key() ) ) // or the field cannot be edited ...
{
continue;
}
// let editor know we've accepted the changes
w->changesCommitted();
newAttributeValues.insert( wIt.key(), w->currentValue() );
}
if ( newAttributeValues.isEmpty() )
{
//nothing to change
return true;
}
#if 0
// prompt for save
int res = QMessageBox::information( this, tr( "Multiedit attributes" ),
tr( "Edits will be applied to all selected features" ), QMessageBox::Ok | QMessageBox::Cancel );
if ( res != QMessageBox::Ok )
{
resetMultiEdit();
return false;
}
#endif
mLayer->beginEditCommand( tr( "Updated multiple feature attributes" ) );
bool success = true;
Q_FOREACH ( QgsFeatureId fid, mMultiEditFeatureIds )
{
QgsAttributeMap::const_iterator aIt = newAttributeValues.constBegin();
for ( ; aIt != newAttributeValues.constEnd(); ++aIt )
{
success &= mLayer->changeAttributeValue( fid, aIt.key(), aIt.value() );
}
}
clearMultiEditMessages();
if ( success )
{
mLayer->endEditCommand();
mLayer->triggerRepaint();
mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Attribute changes for multiple features applied" ), QgsMessageBar::SUCCESS, messageTimeout() );
}
else
{
mLayer->destroyEditCommand();
mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Changes could not be applied" ), QgsMessageBar::WARNING, messageTimeout() );
}
mMessageBar->pushItem( mMultiEditMessageBarItem );
return success;
}
bool QgsAttributeForm::save()
{
if ( mIsSaving )
return true;
mIsSaving = true;
bool success = true;
emit beforeSave( success );
// Somebody wants to prevent this form from saving
if ( !success )
return false;
switch ( mMode )
{
case SingleEditMode:
case AddFeatureMode:
success = saveEdits();
break;
case MultiEditMode:
success = saveMultiEdits();
break;
}
mIsSaving = false;
return success;
@ -275,13 +465,54 @@ void QgsAttributeForm::resetValues()
}
}
void QgsAttributeForm::clearMultiEditMessages()
{
if ( mMultiEditUnsavedMessageBarItem )
{
mMessageBar->popWidget( mMultiEditUnsavedMessageBarItem );
mMultiEditUnsavedMessageBarItem = nullptr;
}
if ( mMultiEditMessageBarItem )
{
mMessageBar->popWidget( mMultiEditMessageBarItem );
mMultiEditMessageBarItem = nullptr;
}
}
void QgsAttributeForm::onAttributeChanged( const QVariant& value )
{
QgsEditorWidgetWrapper* eww = qobject_cast<QgsEditorWidgetWrapper*>( sender() );
Q_ASSERT( eww );
emit attributeChanged( eww->field().name(), value );
switch ( mMode )
{
case SingleEditMode:
case AddFeatureMode:
{
// don't emit signal if it was triggered by a feature change
if ( !mIsSettingFeature )
{
emit attributeChanged( eww->field().name(), value );
}
break;
}
case MultiEditMode:
{
if ( !mIsSettingMultiEditFeatures )
{
QLabel *msgLabel = new QLabel( tr( "Unsaved multiedit changes: <a href=\"#apply\">apply changes</a> or <a href=\"#reset\">reset changes</a>." ), mMessageBar );
msgLabel->setAlignment( Qt::AlignLeft | Qt::AlignVCenter );
msgLabel->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
connect( msgLabel, SIGNAL( linkActivated( QString ) ), this, SLOT( multiEditMessageClicked( QString ) ) );
clearMultiEditMessages();
mMultiEditUnsavedMessageBarItem = new QgsMessageBarItem( msgLabel, QgsMessageBar::WARNING );
mMessageBar->pushItem( mMultiEditUnsavedMessageBarItem );
}
break;
}
}
}
void QgsAttributeForm::onAttributeAdded( int idx )
@ -362,7 +593,7 @@ void QgsAttributeForm::refreshFeature()
void QgsAttributeForm::synchronizeEnabledState()
{
bool isEditable = ( mFeature.isValid() || mIsAddDialog ) && mLayer->isEditable();
bool isEditable = ( mFeature.isValid() || mMode == AddFeatureMode ) && mLayer->isEditable();
Q_FOREACH ( QgsWidgetWrapper* ww, mWidgets )
{
@ -408,7 +639,16 @@ void QgsAttributeForm::init()
delete layout();
// Get a layout
setLayout( new QGridLayout( this ) );
QGridLayout* layout = new QGridLayout();
setLayout( layout );
mFormEditorWidgets.clear();
// a bar to warn the user with non-blocking messages
setContentsMargins( 0, 0, 0, 0 );
mMessageBar = new QgsMessageBar( this );
mMessageBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
layout->addWidget( mMessageBar, 0, 0, 1, 1 );
// Try to load Ui-File for layout
if ( mLayer->editFormConfig()->layout() == QgsEditFormConfig::UiFileLayout && !mLayer->editFormConfig()->uiForm().isEmpty() )
@ -423,7 +663,7 @@ void QgsAttributeForm::init()
loader.setWorkingDirectory( fi.dir() );
formWidget = loader.load( &file, this );
formWidget->setWindowFlags( Qt::Widget );
layout()->addWidget( formWidget );
layout->addWidget( formWidget );
formWidget->show();
file.close();
mButtonBox = findChild<QDialogButtonBox*>();
@ -437,7 +677,7 @@ void QgsAttributeForm::init()
if ( !formWidget && mLayer->editFormConfig()->layout() == QgsEditFormConfig::TabLayout )
{
QTabWidget* tabWidget = new QTabWidget();
layout()->addWidget( tabWidget );
layout->addWidget( tabWidget );
Q_FOREACH ( QgsAttributeEditorElement* widgDef, mLayer->editFormConfig()->tabs() )
{
@ -481,7 +721,7 @@ void QgsAttributeForm::init()
scrollArea->setFrameShape( QFrame::NoFrame );
scrollArea->setFrameShadow( QFrame::Plain );
scrollArea->setFocusProxy( this );
layout()->addWidget( scrollArea );
layout->addWidget( scrollArea );
int row = 0;
Q_FOREACH ( const QgsField& field, mLayer->fields().toList() )
@ -504,10 +744,17 @@ void QgsAttributeForm::init()
// This will also create the widget
QWidget *l = new QLabel( fieldName );
QgsEditorWidgetWrapper* eww = QgsEditorWidgetRegistry::instance()->create( widgetType, mLayer, idx, widgetConfig, nullptr, this, mContext );
QWidget *w = eww ? eww->widget() : new QLabel( QString( "<p style=\"color: red; font-style: italic;\">Failed to create widget with type '%1'</p>" ).arg( widgetType ) );
if ( w )
w->setObjectName( field.name() );
QWidget* w = nullptr;
if ( eww )
{
w = new QgsAttributeFormEditorWidget( eww, this );
mFormEditorWidgets.insert( idx, static_cast< QgsAttributeFormEditorWidget* >( w ) );
}
else
{
w = new QLabel( QString( "<p style=\"color: red; font-style: italic;\">Failed to create widget with type '%1'</p>" ).arg( widgetType ) );
}
if ( eww )
addWidgetWrapper( eww );
@ -545,7 +792,7 @@ void QgsAttributeForm::init()
{
mButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
mButtonBox->setObjectName( "buttonBox" );
layout()->addWidget( mButtonBox );
layout->addWidget( mButtonBox );
}
mButtonBox->setVisible( buttonBoxVisible );
@ -701,7 +948,11 @@ QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAtt
const QgsEditorWidgetConfig widgetConfig = mLayer->editFormConfig()->widgetConfig( fldIdx );
QgsEditorWidgetWrapper* eww = QgsEditorWidgetRegistry::instance()->create( widgetType, mLayer, fldIdx, widgetConfig, nullptr, this, mContext );
newWidgetInfo.widget = eww->widget();
QWidget* w = new QgsAttributeFormEditorWidget( eww, this );
mFormEditorWidgets.insert( fldIdx, static_cast< QgsAttributeFormEditorWidget* >( w ) );
newWidgetInfo.widget = w;
addWidgetWrapper( eww );
newWidgetInfo.widget->setObjectName( mLayer->fields().at( fldIdx ).name() );
@ -922,3 +1173,108 @@ bool QgsAttributeForm::eventFilter( QObject* object, QEvent* e )
return false;
}
void QgsAttributeForm::scanForEqualAttributes( QgsFeatureIterator& fit, QSet< int >& mixedValueFields, QHash< int, QVariant >& fieldSharedValues ) const
{
mixedValueFields.clear();
fieldSharedValues.clear();
QgsFeature f;
bool first = true;
while ( fit.nextFeature( f ) )
{
for ( int i = 0; i < mLayer->fields().count(); ++i )
{
if ( mixedValueFields.contains( i ) )
continue;
if ( first )
{
fieldSharedValues[i] = f.attribute( i );
}
else
{
if ( fieldSharedValues.value( i ) != f.attribute( i ) )
{
fieldSharedValues.remove( i );
mixedValueFields.insert( i );
}
}
}
first = false;
if ( mixedValueFields.count() == mLayer->fields().count() )
{
// all attributes are mixed, no need to keep scanning
break;
}
}
}
void QgsAttributeForm::layerSelectionChanged()
{
switch ( mMode )
{
case SingleEditMode:
case AddFeatureMode:
break;
case MultiEditMode:
resetMultiEdit( true );
break;
}
}
void QgsAttributeForm::setMultiEditFeatureIds( const QgsFeatureIds& fids )
{
mIsSettingMultiEditFeatures = true;
mMultiEditFeatureIds = fids;
if ( fids.isEmpty() )
{
// no selected features
QMap< int, QgsAttributeFormEditorWidget* >::const_iterator wIt = mFormEditorWidgets.constBegin();
for ( ; wIt != mFormEditorWidgets.constEnd(); ++ wIt )
{
wIt.value()->initialize( QVariant() );
}
mIsSettingMultiEditFeatures = false;
return;
}
QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFids( fids ) );
// Scan through all features to determine which attributes are initially the same
QSet< int > mixedValueFields;
QHash< int, QVariant > fieldSharedValues;
scanForEqualAttributes( fit, mixedValueFields, fieldSharedValues );
// also fetch just first feature
fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( *fids.constBegin() ) );
QgsFeature firstFeature;
fit.nextFeature( firstFeature );
Q_FOREACH ( int field, mixedValueFields )
{
if ( QgsAttributeFormEditorWidget* w = mFormEditorWidgets.value( field, nullptr ) )
{
w->initialize( firstFeature.attribute( field ), true );
}
}
QHash< int, QVariant >::const_iterator sharedValueIt = fieldSharedValues.constBegin();
for ( ; sharedValueIt != fieldSharedValues.constEnd(); ++sharedValueIt )
{
if ( QgsAttributeFormEditorWidget* w = mFormEditorWidgets.value( sharedValueIt.key(), nullptr ) )
{
w->initialize( sharedValueIt.value(), false );
}
}
mIsSettingMultiEditFeatures = false;
}
int QgsAttributeForm::messageTimeout()
{
QSettings settings;
return settings.value( "/qgis/messageTimeout", 5 ).toInt();
}

View File

@ -25,12 +25,26 @@
#include <QDialogButtonBox>
class QgsAttributeFormInterface;
class QgsAttributeFormEditorWidget;
class QgsMessageBar;
class QgsMessageBarItem;
class GUI_EXPORT QgsAttributeForm : public QWidget
{
Q_OBJECT
public:
//! Form modes
enum Mode
{
SingleEditMode, /*!< Single edit mode, for editing a single feature */
AddFeatureMode, /*!< Add feature mode, for setting attributes for a new feature. In this mode the dialog will be editable even with an invalid feature and
will add a new feature when the form is accepted. */
MultiEditMode, /*!< Multi edit mode, for editing fields of multiple features at once */
// TODO: SearchMode, /*!< Form values are used for searching/filtering the layer */
};
explicit QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature &feature = QgsFeature(), const QgsAttributeEditorContext& context = QgsAttributeEditorContext(), QWidget *parent = nullptr );
~QgsAttributeForm();
@ -72,14 +86,28 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
*/
bool editable();
/** Returns the current mode of the form.
* @note added in QGIS 2.16
* @see setMode()
*/
Mode mode() const { return mMode; }
/** Sets the current mode of the form.
* @param mode form mode
* @note added in QGIS 2.16
* @see mode()
*/
void setMode( Mode mode );
/**
* Toggles the form mode between edit feature and add feature.
* If set to true, the dialog will be editable even with an invalid feature.
* If set to true, the dialog will add a new feature when the form is accepted.
*
* @param isAddDialog If set to true, turn this dialog into an add feature dialog.
* @deprecated use setMode() instead
*/
void setIsAddDialog( bool isAddDialog );
Q_DECL_DEPRECATED void setIsAddDialog( bool isAddDialog );
/**
* Sets the edit command message (Undo) that will be used when the dialog is accepted
@ -98,6 +126,12 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
*/
bool eventFilter( QObject* object, QEvent* event ) override;
/** Sets all feature IDs which are to be edited if the form is in multiedit mode
* @param fids feature ID list
* @note added in QGIS 2.16
*/
void setMultiEditFeatureIds( const QgsFeatureIds& fids );
signals:
/**
* Notifies about changes of attributes
@ -177,6 +211,13 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
void preventFeatureRefresh();
void synchronizeEnabledState();
void layerSelectionChanged();
//! Save multi edit changes
bool saveMultiEdits();
void resetMultiEdit( bool promptToSave = false );
void multiEditMessageClicked( const QString& link );
private:
void init();
@ -210,12 +251,24 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
void createWrappers();
void connectWrappers();
void scanForEqualAttributes( QgsFeatureIterator& fit, QSet< int >& mixedValueFields, QHash< int, QVariant >& fieldSharedValues ) const;
//! Save single feature or add feature edits
bool saveEdits();
int messageTimeout();
void clearMultiEditMessages();
QgsVectorLayer* mLayer;
QgsFeature mFeature;
QgsMessageBar* mMessageBar;
QgsMessageBarItem* mMultiEditUnsavedMessageBarItem;
QgsMessageBarItem* mMultiEditMessageBarItem;
QList<QgsWidgetWrapper*> mWidgets;
QgsAttributeEditorContext mContext;
QDialogButtonBox* mButtonBox;
QList<QgsAttributeFormInterface*> mInterfaces;
QMap< int, QgsAttributeFormEditorWidget* > mFormEditorWidgets;
// Variables below are used for python
static int sFormCounter;
@ -224,12 +277,22 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
//! Set to true while saving to prevent recursive saves
bool mIsSaving;
bool mIsAddDialog;
//! Flag to prevent refreshFeature() to change mFeature
bool mPreventFeatureRefresh;
//! Set to true while setting feature to prevent attributeChanged signal
bool mIsSettingFeature;
bool mIsSettingMultiEditFeatures;
QgsFeatureIds mMultiEditFeatureIds;
QString mEditCommandMessage;
Mode mMode;
friend class TestQgsDualView;
};
#endif // QGSATTRIBUTEFORM_H

View File

@ -0,0 +1,173 @@
/***************************************************************************
qgsattributeformeditorwidget.cpp
-------------------------------
Date : March 2016
Copyright : (C) 2016 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 "qgsattributeformeditorwidget.h"
#include "qgsattributeform.h"
#include "qgsmultiedittoolbutton.h"
#include "qgseditorwidgetwrapper.h"
#include <QLayout>
#include <QLabel>
QgsAttributeFormEditorWidget::QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form )
: QWidget( form )
, mWidget( editorWidget )
, mForm( form )
, mMode( DefaultMode )
, mMultiEditButton( new QgsMultiEditToolButton() )
, mBlockValueUpdate( false )
, mIsMixed( false )
, mIsChanged( false )
{
if ( !mWidget || !mForm )
return;
QLayout* l = new QHBoxLayout();
l->setMargin( 0 );
l->setContentsMargins( 0, 0, 0, 0 );
l->addWidget( mWidget->widget() );
if ( mWidget->widget() )
{
mWidget->widget()->setObjectName( mWidget->field().name() );
}
connect( mWidget, SIGNAL( valueChanged( const QVariant& ) ), this, SLOT( editorWidgetChanged( const QVariant & ) ) );
connect( mMultiEditButton, SIGNAL( resetFieldValueTriggered() ), this, SLOT( resetValue() ) );
connect( mMultiEditButton, SIGNAL( setFieldValueTriggered() ), this, SLOT( setFieldTriggered() ) );
mMultiEditButton->setField( mWidget->field() );
setLayout( l );
updateWidgets();
}
QgsAttributeFormEditorWidget::~QgsAttributeFormEditorWidget()
{
//there's a chance these widgets are not currently added to the layout, so have no parent set
delete mMultiEditButton;
}
void QgsAttributeFormEditorWidget::setMode( QgsAttributeFormEditorWidget::Mode mode )
{
mMode = mode;
updateWidgets();
}
void QgsAttributeFormEditorWidget::setIsMixed( bool mixed )
{
if ( mixed )
mWidget->showIndeterminateState( );
mMultiEditButton->setIsMixed( mixed );
mIsMixed = mixed;
}
void QgsAttributeFormEditorWidget::changesCommitted()
{
if ( mWidget )
mPreviousValue = mWidget->value();
setIsMixed( false );
mMultiEditButton->changesCommitted();
mIsChanged = false;
}
void QgsAttributeFormEditorWidget::initialize( const QVariant& initialValue, bool mixedValues )
{
if ( mWidget )
{
mBlockValueUpdate = true;
mWidget->setValue( initialValue );
mBlockValueUpdate = false;
}
mPreviousValue = initialValue;
setIsMixed( mixedValues );
mMultiEditButton->setIsChanged( false );
mIsChanged = false;
}
QVariant QgsAttributeFormEditorWidget::currentValue() const
{
return mWidget->value();
}
void QgsAttributeFormEditorWidget::editorWidgetChanged( const QVariant& value )
{
if ( mBlockValueUpdate )
return;
mIsChanged = true;
switch ( mMode )
{
case DefaultMode:
break;
case MultiEditMode:
mMultiEditButton->setIsChanged( true );
}
emit valueChanged( value );
}
void QgsAttributeFormEditorWidget::resetValue()
{
mIsChanged = false;
mBlockValueUpdate = true;
mWidget->setValue( mPreviousValue );
mBlockValueUpdate = false;
switch ( mMode )
{
case DefaultMode:
break;
case MultiEditMode:
{
mMultiEditButton->setIsChanged( false );
if ( mWidget && mIsMixed )
mWidget->showIndeterminateState();
break;
}
}
}
void QgsAttributeFormEditorWidget::setFieldTriggered()
{
mIsChanged = true;
}
QgsVectorLayer* QgsAttributeFormEditorWidget::layer()
{
return mForm ? mForm->layer() : nullptr;
}
void QgsAttributeFormEditorWidget::updateWidgets()
{
bool hasMultiEditButton = ( layout()->indexOf( mMultiEditButton ) >= 0 );
bool fieldReadOnly = layer()->editFormConfig()->readOnly( mWidget->fieldIdx() );
if ( hasMultiEditButton )
{
if ( mMode != MultiEditMode || fieldReadOnly )
{
layout()->removeWidget( mMultiEditButton );
mMultiEditButton->setParent( nullptr );
}
}
else
{
if ( mMode == MultiEditMode && !fieldReadOnly )
{
layout()->addWidget( mMultiEditButton );
}
}
}

View File

@ -0,0 +1,130 @@
/***************************************************************************
qgsattributeformeditorwidget.h
-----------------------------
Date : March 2016
Copyright : (C) 2016 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 QGSATTRIBUTEFORMEDITORWIDGET_H
#define QGSATTRIBUTEFORMEDITORWIDGET_H
#include <QWidget>
#include <QVariant>
class QgsAttributeForm;
class QgsEditorWidgetWrapper;
class QgsMultiEditToolButton;
class QgsVectorLayer;
/** \ingroup gui
* \class QgsAttributeFormEditorWidget
* A widget consisting of both an editor widget and additional widgets for controlling the behaviour
* of the editor widget depending on a number of possible modes. For instance, if the parent attribute
* form is in the multi edit mode, this widget will show both the editor widget and a tool button for
* controlling the multi edit results.
* \note Added in version 2.16
*/
class GUI_EXPORT QgsAttributeFormEditorWidget : public QWidget
{
Q_OBJECT
public:
//! Widget modes
enum Mode
{
DefaultMode, /*!< Default mode, only the editor widget is shown */
MultiEditMode, /*!< Multi edit mode, both the editor widget and a QgsMultiEditToolButton is shown */
// TODO: SearchMode, /*!< Layer search/filter mode */
};
/** Constructor for QgsAttributeFormEditorWidget.
* @param editorWidget associated editor widget wrapper
* @param form parent attribute form
*/
explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form );
~QgsAttributeFormEditorWidget();
/** Sets the current mode for the widget. The widget will adapt its state and visible widgets to
* reflect the updated mode. Eg, showing multi edit tool buttons if the mode is set to MultiEditMode.
* @param mode widget mode
* @see mode()
*/
void setMode( Mode mode );
/** Returns the current mode for the widget.
* @see setMode()
*/
Mode mode() const { return mMode; }
/** Resets the widget to an initial value.
* @param initialValue initial value to show in widget
* @param mixedValue set to true to initially show the mixed values state
*/
void initialize( const QVariant& initialValue, bool mixedValues = false );
/** Returns true if the widget's value has been changed since it was initialized.
* @see initialize()
*/
bool hasChanged() const { return mIsChanged; }
/** Returns the current value of the attached editor widget.
*/
QVariant currentValue() const;
public slots:
/** Sets whether the widget should be displayed in a "mixed values" mode.
* @param mixed set to true to show in a mixed values state
*/
void setIsMixed( bool mixed );
/** Called when field values have been committed;
*/
void changesCommitted();
signals:
//! Emitted when the widget's value changes
//! @param value new widget value
void valueChanged( const QVariant& value );
private slots:
//! Triggered when editor widget's value changes
void editorWidgetChanged( const QVariant& value );
//! Triggered when multi edit tool button requests value reset
void resetValue();
//! Triggered when the multi edit tool button "set field value" action is selected
void setFieldTriggered();
private:
QgsEditorWidgetWrapper* mWidget;
QgsAttributeForm* mForm;
Mode mMode;
QgsMultiEditToolButton* mMultiEditButton;
QVariant mPreviousValue;
bool mBlockValueUpdate;
bool mIsMixed;
bool mIsChanged;
QgsVectorLayer* layer();
void updateWidgets();
};
#endif // QGSATTRIBUTEFORMEDITORWIDGET_H

View File

@ -75,6 +75,35 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mToggleMultiEditButton">
<property name="toolTip">
<string>Toggle multi edit mode</string>
</property>
<property name="whatsThis">
<string/>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionAllEdits.svg</normaloff>:/images/themes/default/mActionAllEdits.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>18</width>
<height>18</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mSaveEditsButton">
<property name="toolTip">
@ -858,6 +887,7 @@
</customwidgets>
<tabstops>
<tabstop>mToggleEditingButton</tabstop>
<tabstop>mToggleMultiEditButton</tabstop>
<tabstop>mSaveEditsButton</tabstop>
<tabstop>mReloadButton</tabstop>
<tabstop>mAddFeature</tabstop>

View File

@ -10,6 +10,8 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}/../../../src/ui
${CMAKE_CURRENT_SOURCE_DIR}/../core #for render checker class
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/editorwidgets
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/editorwidgets/core
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/symbology-ng
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/raster
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core

View File

@ -19,8 +19,10 @@
#include <editorwidgets/core/qgseditorwidgetregistry.h>
#include <attributetable/qgsattributetableview.h>
#include <attributetable/qgsdualview.h>
#include "qgsattributeform.h"
#include <qgsapplication.h>
#include <qgsvectorlayer.h>
#include "qgsvectordataprovider.h"
#include <qgsmapcanvas.h>
#include <qgsfeature.h>
@ -42,6 +44,8 @@ class TestQgsDualView : public QObject
void testSelectAll();
void testAttributeFormSharedValueScanning();
private:
QgsMapCanvas* mCanvas;
QgsVectorLayer* mPointsLayer;
@ -103,6 +107,72 @@ void TestQgsDualView::testSelectAll()
QVERIFY( mPointsLayer->selectedFeatureCount() == 1 );
}
void TestQgsDualView::testAttributeFormSharedValueScanning()
{
// test QgsAttributeForm::scanForEqualAttributes
QSet< int > mixedValueFields;
QHash< int, QVariant > fieldSharedValues;
// make a temporary layer to check through
QgsVectorLayer* layer = new QgsVectorLayer( "Point?field=col1:integer&field=col2:integer&field=col3:integer&field=col4:integer", "test", "memory" );
QVERIFY( layer->isValid() );
QgsFeature f1( layer->dataProvider()->fields(), 1 );
f1.setAttribute( "col1", 1 );
f1.setAttribute( "col2", 1 );
f1.setAttribute( "col3", 3 );
f1.setAttribute( "col4", 1 );
QgsFeature f2( layer->dataProvider()->fields(), 2 );
f2.setAttribute( "col1", 1 );
f2.setAttribute( "col2", 2 );
f2.setAttribute( "col3", 3 );
f2.setAttribute( "col4", 2 );
QgsFeature f3( layer->dataProvider()->fields(), 3 );
f3.setAttribute( "col1", 1 );
f3.setAttribute( "col2", 2 );
f3.setAttribute( "col3", 3 );
f3.setAttribute( "col4", 2 );
QgsFeature f4( layer->dataProvider()->fields(), 4 );
f4.setAttribute( "col1", 1 );
f4.setAttribute( "col2", 1 );
f4.setAttribute( "col3", 3 );
f4.setAttribute( "col4", 2 );
layer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 << f4 );
QgsAttributeForm form( layer );
QgsFeatureIterator it = layer->getFeatures();
form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
QCOMPARE( mixedValueFields, QSet< int >() << 1 << 3 );
QCOMPARE( fieldSharedValues.value( 0 ).toInt(), 1 );
QCOMPARE( fieldSharedValues.value( 2 ).toInt(), 3 );
// add another feature so all attributes are different
QgsFeature f5( layer->dataProvider()->fields(), 5 );
f5.setAttribute( "col1", 11 );
f5.setAttribute( "col2", 11 );
f5.setAttribute( "col3", 13 );
f5.setAttribute( "col4", 12 );
layer->dataProvider()->addFeatures( QgsFeatureList() << f5 );
it = layer->getFeatures();
form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
QCOMPARE( mixedValueFields, QSet< int >() << 0 << 1 << 2 << 3 );
QVERIFY( fieldSharedValues.isEmpty() );
// single feature, all attributes should be shared
it = layer->getFeatures( QgsFeatureRequest().setFilterFid( 4 ) );
form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
QCOMPARE( fieldSharedValues.value( 0 ).toInt(), 1 );
QCOMPARE( fieldSharedValues.value( 1 ).toInt(), 1 );
QCOMPARE( fieldSharedValues.value( 2 ).toInt(), 3 );
QCOMPARE( fieldSharedValues.value( 3 ).toInt(), 2 );
QVERIFY( mixedValueFields.isEmpty() );
}
QTEST_MAIN( TestQgsDualView )
#include "testqgsdualview.moc"

View File

@ -43,6 +43,7 @@ ADD_PYTHON_TEST(PyQgsGeometryTest test_qgsgeometry.py)
ADD_PYTHON_TEST(PyQgsGraduatedSymbolRendererV2 test_qgsgraduatedsymbolrendererv2.py)
ADD_PYTHON_TEST(PyQgsMapUnitScale test_qgsmapunitscale.py)
ADD_PYTHON_TEST(PyQgsMemoryProvider test_provider_memory.py)
ADD_PYTHON_TEST(PyQgsMultiEditToolButton test_qgsmultiedittoolbutton.py)
ADD_PYTHON_TEST(PyQgsNetworkContentFetcher test_qgsnetworkcontentfetcher.py)
ADD_PYTHON_TEST(PyQgsNullSymbolRenderer test_qgsnullsymbolrenderer.py)
ADD_PYTHON_TEST(PyQgsPalLabelingBase test_qgspallabeling_base.py)

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsMultiEditToolButton.
.. 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__ = '16/03/2016'
__copyright__ = 'Copyright 2016, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis # switch sip api
from qgis.gui import QgsMultiEditToolButton
from qgis.testing import (start_app,
unittest
)
start_app()
class TestQgsMultiEditToolButton(unittest.TestCase):
def test_state_logic(self):
"""
Test that the logic involving button states is correct
"""
w = QgsMultiEditToolButton()
self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
# set is changed should update state to changed
w.setIsChanged(True)
self.assertEqual(w.state(), QgsMultiEditToolButton.Changed)
w.setIsChanged(False)
self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
#resetting changes should fall back to default state
w.setIsChanged(True)
w.resetChanges()
self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
#setting changes committed should result in default state
w.setIsChanged(True)
w.changesCommitted()
self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
#Test with mixed values
w.setIsMixed(True)
self.assertEqual(w.state(), QgsMultiEditToolButton.MixedValues)
#changed state takes priority over mixed state
w.setIsChanged(True)
self.assertEqual(w.state(), QgsMultiEditToolButton.Changed)
w.setIsChanged(False)
#should reset to mixed state
self.assertEqual(w.state(), QgsMultiEditToolButton.MixedValues)
#resetting changes should fall back to mixed state
w.setIsChanged(True)
w.resetChanges()
self.assertEqual(w.state(), QgsMultiEditToolButton.MixedValues)
#setting changes committed should result in default state
w.setIsChanged(True)
w.changesCommitted()
self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
if __name__ == '__main__':
unittest.main()