[krita] krita: Implemented multinode editing of layer properties

Dmitry Kazakov dimula73 at gmail.com
Thu Jan 14 15:44:24 UTC 2016


Git commit d592d5206712838839c7e42c52c3539f99388b75 by Dmitry Kazakov.
Committed on 14/01/2016 at 15:43.
Pushed by dkazakov into branch 'master'.

Implemented multinode editing of layer properties

Now you can edit any property of a group of layers using Properties
dialog. Near to every field there is a checkbox showing if the value
will be applied to your layers. If you disable the checkbox, the
properties of the layers will be restored back.

For boolean properties, like "Visible" or "Locked" there are tristated
checkboxes. They have the following meaning:

checked --- all the layers have the property enabled
unchecked --- all the layers have the property disabled
partially-checked --- the layers have mixed property values. They are kept
                      unchanged.

CC:kimageshop at kde.org

M  +1    -1    krita/image/kis_layer_properties_icons.h
M  +1    -1    krita/ui/CMakeLists.txt
M  +151  -147  krita/ui/dialogs/kis_dlg_layer_properties.cc
M  +15   -21   krita/ui/dialogs/kis_dlg_layer_properties.h
M  +99   -43   krita/ui/forms/wdglayerproperties.ui
M  +3    -1    krita/ui/kis_layer_manager.cc
A  +86   -0    krita/ui/kis_multinode_property.cpp     [License: GPL (v2+)]
A  +578  -0    krita/ui/kis_multinode_property.h     [License: GPL (v2+)]
M  +4    -0    krita/ui/kis_node_model.h
M  +8    -0    krita/ui/tests/CMakeLists.txt
A  +129  -0    krita/ui/tests/kis_multinode_property_test.cpp     [License: GPL (v2+)]
A  +31   -0    krita/ui/tests/kis_multinode_property_test.h     [License: GPL (v2+)]
D  +0    -88   krita/ui/widgets/kis_channelflags_widget.cpp
D  +0    -71   krita/ui/widgets/kis_channelflags_widget.h

http://commits.kde.org/krita/d592d5206712838839c7e42c52c3539f99388b75

diff --git a/krita/image/kis_layer_properties_icons.h b/krita/image/kis_layer_properties_icons.h
index cf83844..916c5ba 100644
--- a/krita/image/kis_layer_properties_icons.h
+++ b/krita/image/kis_layer_properties_icons.h
@@ -25,7 +25,7 @@
 #include "kis_node_model.h"
 
 
-class KisLayerPropertiesIcons
+class KRITAIMAGE_EXPORT KisLayerPropertiesIcons
 {
 public:
     KisLayerPropertiesIcons();
diff --git a/krita/ui/CMakeLists.txt b/krita/ui/CMakeLists.txt
index 2979c6a..85f7885 100644
--- a/krita/ui/CMakeLists.txt
+++ b/krita/ui/CMakeLists.txt
@@ -133,6 +133,7 @@ set(kritaui_LIB_SRCS
     kis_view_plugin.cpp
     kis_canvas_controls_manager.cpp
     kis_tooltip_manager.cpp
+    kis_multinode_property.cpp
     kisexiv2/kis_exif_io.cpp
     kisexiv2/kis_exiv2.cpp
     kisexiv2/kis_iptc_io.cpp
@@ -187,7 +188,6 @@ set(kritaui_LIB_SRCS
     tool/strokes/freehand_stroke.cpp
     tool/strokes/kis_painter_based_stroke_strategy.cpp
     tool/strokes/kis_filter_stroke_strategy.cpp
-    widgets/kis_channelflags_widget.cpp
     widgets/kis_cmb_composite.cc
     widgets/kis_cmb_contour.cpp
     widgets/kis_cmb_gradient.cpp
diff --git a/krita/ui/dialogs/kis_dlg_layer_properties.cc b/krita/ui/dialogs/kis_dlg_layer_properties.cc
index 1b65822..18f0925 100644
--- a/krita/ui/dialogs/kis_dlg_layer_properties.cc
+++ b/krita/ui/dialogs/kis_dlg_layer_properties.cc
@@ -22,57 +22,65 @@
 
 #include <QLabel>
 #include <QLayout>
-#include <QSlider>
 #include <QString>
-#include <QBitArray>
-#include <QVector>
 #include <QGroupBox>
 #include <QVBoxLayout>
 
 #include <klocalizedstring.h>
 
-#include <KoChannelInfo.h>
 #include <KoColorSpace.h>
-#include <KoCompositeOp.h>
 
-#include "kis_undo_adapter.h"
-#include "QTimer"
-#include "commands/kis_layer_commands.h"
-#include "kis_layer.h"
 #include "KisViewManager.h"
-#include "KisDocument.h"
-#include "kis_cursor.h"
 #include <kis_debug.h>
 #include <kis_global.h>
 
-#include "widgets/squeezedcombobox.h"
-
 #include "widgets/kis_cmb_composite.h"
-#include "widgets/kis_cmb_idlist.h"
 #include "KoColorProfile.h"
-#include "widgets/kis_channelflags_widget.h"
-#include <kis_composite_ops_model.h>
+#include "kis_multinode_property.h"
+#include "kis_layer_utils.h"
+#include "kis_image.h"
+#include "kis_layer_properties_icons.h"
+#include "kis_signal_compressor.h"
 
 
 struct KisDlgLayerProperties::Private
 {
-    QString deviceName;
+    Private() : updatesCompressor(500, KisSignalCompressor::POSTPONE) {}
+
+    KisNodeList nodes;
     const KoColorSpace *colorSpace;
-    const KoCompositeOp *compositeOp;
-    QBitArray channelFlags;
-    quint8 opacity;
-    KisLayerSP layer;
     KisViewManager *view;
-    KisDocument *doc;
     WdgLayerProperties *page;
-    KisChannelFlagsWidget *channelFlagsWidget;
-    QTimer previewTimer;
+
+    QSharedPointer<KisMultinodeCompositeOpProperty> compositeOpProperty;
+    QSharedPointer<KisMultinodeOpacityProperty> opacityProperty;
+    QSharedPointer<KisMultinodeNameProperty> nameProperty;
+
+    QList<KisMultinodePropertyInterfaceSP> layerProperties;
+    QList<QPointer<QCheckBox> > layerPropCheckboxes;
+
+    QList<KisMultinodePropertyInterfaceSP> channelFlagsProps;
+    QList<QPointer<QCheckBox> > channelFlagsCheckboxes;
+
+    KisSignalCompressor updatesCompressor;
+
+    QList<KisMultinodePropertyInterfaceSP> allProperties() const {
+        QList<KisMultinodePropertyInterfaceSP> props;
+        props << compositeOpProperty;
+        props << opacityProperty;
+        props << nameProperty;
+        props << layerProperties;
+        props << channelFlagsProps;
+        return props;
+    }
 };
 
-KisDlgLayerProperties::KisDlgLayerProperties(KisLayerSP layer, KisViewManager *view, KisDocument *doc, QWidget *parent, const char *name, Qt::WFlags f)
+KisDlgLayerProperties::KisDlgLayerProperties(KisNodeList nodes, KisViewManager *view, QWidget *parent, const char *name, Qt::WFlags f)
     : KoDialog(parent)
     , d(new Private())
 {
+    nodes = KisLayerUtils::sortMergableNodes(view->image()->root(), nodes);
+    d->nodes = nodes;
 
     Q_UNUSED(f);
     setCaption(i18n("Layer Properties"));
@@ -82,175 +90,171 @@ KisDlgLayerProperties::KisDlgLayerProperties(KisLayerSP layer, KisViewManager *v
 
     setObjectName(name);
     d->page = new WdgLayerProperties(this);
-    d->layer = layer;
-    d->view = view;
-    d->doc = doc;
-    d->deviceName = layer->name();
-    d->colorSpace = layer->colorSpace();
-    d->compositeOp = layer->compositeOp();
-    d->channelFlags = layer->channelFlags();
-    d->opacity = layer->opacity();
-
-    quint8 sliderOpacity = int((d->opacity * 100.0) / 255 + 0.5);
-
     setMainWidget(d->page);
+    d->view = view;
+    d->colorSpace = d->nodes.first()->colorSpace();
 
-    d->page->editName->setText(d->deviceName);
     d->page->editName->setFocus();
-    connect(d->page->editName, SIGNAL(textChanged(const QString &)), this, SLOT(slotNameChanged(const QString &)));
-
-    d->page->lblColorSpace->setText(d->colorSpace->name());
-
-    if (const KoColorProfile* profile = d->colorSpace->profile()) {
-        d->page->lblProfile->setText(profile->name());
-    }
+    d->nameProperty.reset(new KisMultinodeNameProperty(nodes));
+    d->nameProperty->connectIgnoreCheckBox(d->page->chkName);
+    d->nameProperty->connectValueChangedSignal(this, SLOT(slotNameValueChangedInternally()));
+    connect(d->page->editName, SIGNAL(textChanged(const QString &)), SLOT(slotNameValueChangedExternally()));
 
     d->page->intOpacity->setRange(0, 100);
-    d->page->intOpacity->setValue(sliderOpacity);
     d->page->intOpacity->setSuffix("%");
-    connect(d->page->intOpacity, SIGNAL(valueChanged(int)), SLOT(kickTimer()));
-
-    d->page->cmbComposite->setEnabled(d->compositeOp);
-    connect(d->page->cmbComposite, SIGNAL(currentIndexChanged(int)), SLOT(kickTimer()));
-
-    if (d->compositeOp) {
-        d->page->cmbComposite->validate(d->colorSpace);
-        d->page->cmbComposite->selectCompositeOp(KoID(d->compositeOp->id()));
+    d->opacityProperty.reset(new KisMultinodeOpacityProperty(nodes));
+    d->opacityProperty->connectIgnoreCheckBox(d->page->chkOpacity);
+    d->opacityProperty->connectValueChangedSignal(this, SLOT(slotOpacityValueChangedInternally()));
+    d->opacityProperty->connectValueChangedSignal(&d->updatesCompressor, SLOT(start()));
+    connect(d->page->intOpacity, SIGNAL(valueChanged(int)), SLOT(slotOpacityValueChangedExternally()));
+
+    d->compositeOpProperty.reset(new KisMultinodeCompositeOpProperty(nodes));
+    d->compositeOpProperty->connectIgnoreCheckBox(d->page->chkCompositeOp);
+    d->compositeOpProperty->connectValueChangedSignal(this, SLOT(slotCompositeOpValueChangedInternally()));
+    d->compositeOpProperty->connectValueChangedSignal(&d->updatesCompressor, SLOT(start()));
+    connect(d->page->cmbComposite, SIGNAL(currentIndexChanged(int)), SLOT(slotCompositeOpValueChangedExternally()));
+
+    if (!checkNodesDiffer<const KoColorSpace*>(d->nodes, [](KisNodeSP node) { return node->colorSpace(); })) {
+
+        d->page->lblColorSpace->setText(d->colorSpace->name());
+        if (const KoColorProfile* profile = d->colorSpace->profile()) {
+            d->page->lblProfile->setText(profile->name());
+        }
+
+        ChannelFlagAdapter::PropertyList props = ChannelFlagAdapter::adaptersList(nodes);
+        if (!props.isEmpty()) {
+            QVBoxLayout *vbox = new QVBoxLayout;
+            Q_FOREACH (const ChannelFlagAdapter::Property &prop, props) {
+                QCheckBox *chk = new QCheckBox(prop.name, this);
+                vbox->addWidget(chk);
+
+                KisMultinodePropertyInterface *multiprop =
+                    new KisMultinodeProperty<ChannelFlagAdapter>(
+                        nodes,
+                        ChannelFlagAdapter(prop));
+
+                multiprop->connectIgnoreCheckBox(chk);
+                multiprop->connectValueChangedSignal(this, SLOT(slotFlagsValueChangedInternally()));
+                multiprop->connectValueChangedSignal(&d->updatesCompressor, SLOT(start()));
+
+                d->channelFlagsCheckboxes << chk;
+                d->channelFlagsProps << toQShared(multiprop);
+            }
+
+            d->page->grpActiveChannels->setLayout(vbox);
+        } else {
+            d->page->grpActiveChannels->setVisible(false);
+            d->page->lineActiveChannels->setVisible(false);
+        }
+    } else {
+        d->page->grpActiveChannels->setVisible(false);
+        d->page->lineActiveChannels->setVisible(false);
+        d->page->cmbComposite->setEnabled(false);
+        d->page->chkCompositeOp->setEnabled(false);
+
+        d->page->lblColorSpace->setText(i18n("*varies*"));
+        d->page->lblProfile->setText(i18n("*varies*"));
     }
 
-    slotNameChanged(d->page->editName->text());
+    {
+        QVBoxLayout *vbox = new QVBoxLayout;
 
-    QVBoxLayout * vbox = new QVBoxLayout;
-    d->channelFlagsWidget = new KisChannelFlagsWidget(d->colorSpace);
-    connect(d->channelFlagsWidget, SIGNAL(channelSelectionChanced()), SLOT(kickTimer()));
+        KisNodeModel::PropertyList props = LayerPropertyAdapter::adaptersList(nodes);
+        Q_FOREACH (const KisNodeModel::Property &prop, props) {
+            QCheckBox *chk = new QCheckBox(prop.name, this);
+            chk->setIcon(prop.onIcon);
+            vbox->addWidget(chk);
 
-    vbox->addWidget(d->channelFlagsWidget);
-    vbox->addStretch(1);
-    d->page->grpActiveChannels->setLayout(vbox);
+            KisMultinodePropertyInterface *multiprop =
+                new KisMultinodeProperty<LayerPropertyAdapter>(
+                    nodes,
+                    LayerPropertyAdapter(prop.name));
 
-    d->channelFlagsWidget->setChannelFlags(d->channelFlags);
+            multiprop->connectIgnoreCheckBox(chk);
+            multiprop->connectValueChangedSignal(this, SLOT(slotPropertyValueChangedInternally()));
+            multiprop->connectValueChangedSignal(&d->updatesCompressor, SLOT(start()));
 
-    QMetaObject::invokeMethod(this, "adjustSize", Qt::QueuedConnection);
+            d->layerPropCheckboxes << chk;
+            d->layerProperties << toQShared(multiprop);
+        }
+
+        d->page->grpProperties->setLayout(vbox);
+    }
 
-    connect(&d->previewTimer, SIGNAL(timeout()), SLOT(updatePreview()));
+    connect(&d->updatesCompressor, SIGNAL(timeout()), SLOT(updatePreview()));
 }
 
 KisDlgLayerProperties::~KisDlgLayerProperties()
 {
     if (result() == QDialog::Accepted) {
-        applyNewProperties();
+        if (d->updatesCompressor.isActive()) {
+            d->updatesCompressor.stop();
+            updatePreview();
+        }
+        // TODO: save the undo command
     }
-    else { // QDialog::Rejected
-        cleanPreviewChanges();
-        d->doc->setModified(true);
-        d->layer->setDirty();
+    else /* if (result() == QDialog::Rejected) */ {
+        Q_FOREACH(auto prop, d->allProperties()) {
+            prop->setIgnored(true);
+        }
+        updatePreview();
     }
-
-    delete d;
 }
 
-void KisDlgLayerProperties::updatePreview()
+void KisDlgLayerProperties::slotCompositeOpValueChangedInternally()
 {
-    if (!d->layer) return;
-
-    if (d->page->checkBoxPreview->isChecked()) {
-        d->layer->setOpacity(getOpacity());
-        d->layer->setCompositeOpId(getCompositeOp());
-        d->layer->setName(getName());
-        d->layer->setChannelFlags(getChannelFlags());
-        d->doc->setModified(true);
-        d->layer->setDirty();
-    }
+    d->page->cmbComposite->validate(d->colorSpace);
+    d->page->cmbComposite->selectCompositeOp(KoID(d->compositeOpProperty->value()));
+    d->page->cmbComposite->setEnabled(!d->compositeOpProperty->isIgnored());
 }
 
-void KisDlgLayerProperties::adjustSize()
+void KisDlgLayerProperties::slotCompositeOpValueChangedExternally()
 {
-    KoDialog::adjustSize();
-    setMinimumSize(size());
+    if (d->compositeOpProperty->isIgnored()) return;
+    d->compositeOpProperty->setValue(d->page->cmbComposite->selectedCompositeOp().id());
 }
 
-
-bool KisDlgLayerProperties::haveChanges() const
+void KisDlgLayerProperties::slotOpacityValueChangedInternally()
 {
-    return d->layer->name() !=  getName()
-        || d->layer->opacity() !=  getOpacity()
-        || d->layer->channelFlags() !=  getChannelFlags()
-        || (d->compositeOp && d->layer->compositeOp() &&
-            d->layer->compositeOp()->id()!= getCompositeOp());
-
+    d->page->intOpacity->setValue(d->opacityProperty->value());
+    d->page->intOpacity->setEnabled(!d->opacityProperty->isIgnored());
 }
 
-
-void KisDlgLayerProperties::applyNewProperties()
+void KisDlgLayerProperties::slotOpacityValueChangedExternally()
 {
-    if (!d->layer) return;
-
-    cleanPreviewChanges();
-
-    if (haveChanges()) {
-        QApplication::setOverrideCursor(KisCursor::waitCursor());
-        KUndo2Command *change = new KisLayerPropsCommand(d->layer,
-                                                         d->layer->opacity(),       getOpacity(),
-                                                         d->layer->compositeOpId(), getCompositeOp(),
-                                                         d->layer->name(),          getName(),
-                                                         d->layer->channelFlags(),  getChannelFlags(),
-                                                         true);
-        d->view->undoAdapter()->addCommand(change);
-        QApplication::restoreOverrideCursor();
-        d->doc->setModified(true);
-        d->layer->setDirty();
-    }
+    if (d->opacityProperty->isIgnored()) return;
+    d->opacityProperty->setValue(d->page->intOpacity->value());
 }
 
-void KisDlgLayerProperties::cleanPreviewChanges()
+void KisDlgLayerProperties::slotNameValueChangedInternally()
 {
-    d->layer->setOpacity(d->opacity);
-    d->layer->setName(d->deviceName);
-    d->layer->setChannelFlags(d->channelFlags);
-
-    if (d->compositeOp) {
-        d->layer->setCompositeOpId(d->compositeOp->id());
-    }
+    d->page->editName->setText(d->nameProperty->value());
+    d->page->editName->setEnabled(!d->nameProperty->isIgnored());
 }
 
-void KisDlgLayerProperties::kickTimer()
+void KisDlgLayerProperties::slotNameValueChangedExternally()
 {
-    d->previewTimer.start(200);
+    if (d->nameProperty->isIgnored()) return;
+    d->nameProperty->setValue(d->page->editName->text());
 }
 
-void KisDlgLayerProperties::slotNameChanged(const QString &_text)
+void KisDlgLayerProperties::slotPropertyValueChangedInternally()
 {
-    enableButtonOk(!_text.isEmpty());
-}
-
-QString KisDlgLayerProperties::getName() const
-{
-    return d->page->editName->text();
-}
-
-int KisDlgLayerProperties::getOpacity() const
-{
-    qint32 opacity = d->page->intOpacity->value();
-    if (opacity > 0 ) {
-        opacity = int((opacity * 255.0) / 100 + 0.5);
+    Q_FOREACH (KisMultinodePropertyInterfaceSP prop, d->channelFlagsProps) {
+        prop->rereadCurrentValue();
     }
-    if (opacity > 255) {
-        opacity = 255;
-    }
-    return opacity;
 }
 
-QString KisDlgLayerProperties::getCompositeOp() const
+void KisDlgLayerProperties::slotFlagsValueChangedInternally()
 {
-    return d->page->cmbComposite->selectedCompositeOp().id();
+    Q_FOREACH (KisMultinodePropertyInterfaceSP prop, d->layerProperties) {
+        prop->rereadCurrentValue();
+    }
 }
 
-QBitArray KisDlgLayerProperties::getChannelFlags() const
+void KisDlgLayerProperties::updatePreview()
 {
-    QBitArray flags = d->channelFlagsWidget->channelFlags();
-    for (int i = 0; i < flags.size(); ++i) {
-        dbgUI << "Received flag from channelFlags widget, flag " << i << " is " << flags.testBit(i);
+    Q_FOREACH(KisNodeSP node, d->nodes) {
+        node->setDirty(node->extent());
     }
-    return flags;
 }
-
diff --git a/krita/ui/dialogs/kis_dlg_layer_properties.h b/krita/ui/dialogs/kis_dlg_layer_properties.h
index 09426c2..59b8ab0 100644
--- a/krita/ui/dialogs/kis_dlg_layer_properties.h
+++ b/krita/ui/dialogs/kis_dlg_layer_properties.h
@@ -21,6 +21,8 @@
 
 #include <QList>
 #include <QCheckBox>
+#include <QScopedPointer>
+
 
 #include "kis_types.h"
 #include <KoDialog.h>
@@ -53,36 +55,28 @@ class KisDlgLayerProperties : public KoDialog
     Q_OBJECT
 
 public:
-    KisDlgLayerProperties(KisLayerSP layer, KisViewManager *view, KisDocument *doc, QWidget *parent = 0, const char *name = 0, Qt::WFlags f = 0);
+    KisDlgLayerProperties(KisNodeList nodes, KisViewManager *view, QWidget *parent = 0, const char *name = 0, Qt::WFlags f = 0);
 
     virtual ~KisDlgLayerProperties();
 
-private:
+protected Q_SLOTS:
+    void updatePreview();
 
-    bool haveChanges() const;
-    QString getName() const;
-    qint32 getOpacity() const;
-    QString getCompositeOp() const;
+    void slotCompositeOpValueChangedInternally();
+    void slotCompositeOpValueChangedExternally();
 
-    /**
-     * @return a bit array of channel flags in the order in which the
-     * channels appear in the pixel, not in the list of KoChannelInfo
-     * objects from the colorspace.
-     */
-    QBitArray getChannelFlags() const;
+    void slotOpacityValueChangedInternally();
+    void slotOpacityValueChangedExternally();
 
-protected Q_SLOTS:
-    void slotNameChanged(const QString &);
-    void applyNewProperties();
-    void cleanPreviewChanges();
-    void kickTimer();
-    void updatePreview();
-    void adjustSize();
+    void slotNameValueChangedInternally();
+    void slotNameValueChangedExternally();
 
-private:
+    void slotPropertyValueChangedInternally();
+    void slotFlagsValueChangedInternally();
 
+private:
     struct Private;
-    Private * const d;
+    const QScopedPointer<Private> d;
 };
 
 #endif // KIS_DLG_LAYER_PROPERTIES_H_
diff --git a/krita/ui/forms/wdglayerproperties.ui b/krita/ui/forms/wdglayerproperties.ui
index b681280..32f06d4 100644
--- a/krita/ui/forms/wdglayerproperties.ui
+++ b/krita/ui/forms/wdglayerproperties.ui
@@ -2,28 +2,24 @@
 <ui version="4.0">
  <class>WdgLayerProperties</class>
  <widget class="QWidget" name="WdgLayerProperties">
-  <layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0">
-   <property name="spacing">
-    <number>2</number>
-   </property>
-   <property name="leftMargin">
-    <number>0</number>
-   </property>
-   <property name="topMargin">
-    <number>0</number>
-   </property>
-   <property name="rightMargin">
-    <number>0</number>
-   </property>
-   <property name="bottomMargin">
-    <number>0</number>
-   </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>740</width>
+    <height>514</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
    <item>
-    <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+    <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
-      <layout class="QFormLayout" name="formLayout">
+      <layout class="QGridLayout" name="gridLayout" rowstretch="1,1,1,1,1">
        <item row="0" column="0">
-        <widget class="QLabel" name="textLabel1">
+        <widget class="QCheckBox" name="chkName"/>
+       </item>
+       <item row="0" column="1">
+        <widget class="QLabel" name="lblName">
          <property name="text">
           <string>&Name:</string>
          </property>
@@ -35,7 +31,7 @@
          </property>
         </widget>
        </item>
-       <item row="0" column="1">
+       <item row="0" column="2">
         <widget class="QLineEdit" name="editName">
          <property name="sizePolicy">
           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
@@ -46,7 +42,10 @@
         </widget>
        </item>
        <item row="1" column="0">
-        <widget class="QLabel" name="textLabel3">
+        <widget class="QCheckBox" name="chkOpacity"/>
+       </item>
+       <item row="1" column="1">
+        <widget class="QLabel" name="lblOpacity">
          <property name="text">
           <string>&Opacity:</string>
          </property>
@@ -58,8 +57,8 @@
          </property>
         </widget>
        </item>
-       <item row="1" column="1">
-        <widget class="KisSliderSpinBox" name="intOpacity">
+       <item row="1" column="2">
+        <widget class="KisSliderSpinBox" name="intOpacity" native="true">
          <property name="sizePolicy">
           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
            <horstretch>0</horstretch>
@@ -69,7 +68,10 @@
         </widget>
        </item>
        <item row="2" column="0">
-        <widget class="QLabel" name="textLabel4">
+        <widget class="QCheckBox" name="chkCompositeOp"/>
+       </item>
+       <item row="2" column="1">
+        <widget class="QLabel" name="lblCompositeOp">
          <property name="text">
           <string>Composite mode:</string>
          </property>
@@ -78,7 +80,7 @@
          </property>
         </widget>
        </item>
-       <item row="2" column="1">
+       <item row="2" column="2">
         <widget class="KisCompositeOpComboBox" name="cmbComposite">
          <property name="sizePolicy">
           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
@@ -88,8 +90,14 @@
          </property>
         </widget>
        </item>
-       <item row="3" column="0">
-        <widget class="QLabel" name="lblColorSpaces">
+       <item row="3" column="1">
+        <widget class="QLabel" name="lblColorSpaceTitle">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="text">
           <string>Color space:</string>
          </property>
@@ -98,15 +106,27 @@
          </property>
         </widget>
        </item>
-       <item row="3" column="1">
+       <item row="3" column="2">
         <widget class="QLabel" name="lblColorSpace">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="text">
           <string comment="KDE::DoNotExtract">TextLabel</string>
          </property>
         </widget>
        </item>
-       <item row="4" column="0">
-        <widget class="QLabel" name="lblProfiles">
+       <item row="4" column="1">
+        <widget class="QLabel" name="lblProfileTitle">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="text">
           <string>Profile:</string>
          </property>
@@ -115,8 +135,14 @@
          </property>
         </widget>
        </item>
-       <item row="4" column="1">
+       <item row="4" column="2">
         <widget class="QLabel" name="lblProfile">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="text">
           <string comment="KDE::DoNotExtract">TextLabel</string>
          </property>
@@ -131,6 +157,43 @@
       </layout>
      </item>
      <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Fixed</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>10</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="Line" name="line">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QGroupBox" name="grpProperties">
+       <property name="title">
+        <string>Properties</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="Line" name="lineActiveChannels">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
       <widget class="QGroupBox" name="grpActiveChannels">
        <property name="toolTip">
         <string>Select the set of active channels.</string>
@@ -150,24 +213,17 @@
      <property name="orientation">
       <enum>Qt::Vertical</enum>
      </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::MinimumExpanding</enum>
+     </property>
      <property name="sizeHint" stdset="0">
       <size>
-       <width>0</width>
-       <height>0</height>
+       <width>20</width>
+       <height>40</height>
       </size>
      </property>
     </spacer>
    </item>
-   <item>
-    <widget class="QCheckBox" name="checkBoxPreview">
-     <property name="text">
-      <string>&Preview</string>
-     </property>
-     <property name="checked">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
   </layout>
  </widget>
  <customwidgets>
diff --git a/krita/ui/kis_layer_manager.cc b/krita/ui/kis_layer_manager.cc
index 67335fe..5b90358 100644
--- a/krita/ui/kis_layer_manager.cc
+++ b/krita/ui/kis_layer_manager.cc
@@ -457,7 +457,9 @@ void KisLayerManager::layerProperties()
 
         }
     } else { // If layer == normal painting layer, vector layer, or group layer
-        KisDlgLayerProperties *dialog = new KisDlgLayerProperties(layer, m_view, m_view->document());
+        QList<KisNodeSP> selectedNodes = m_view->nodeManager()->selectedNodes();
+
+        KisDlgLayerProperties *dialog = new KisDlgLayerProperties(selectedNodes, m_view);
         dialog->resize(dialog->minimumSizeHint());
         dialog->setAttribute(Qt::WA_DeleteOnClose);
         Qt::WindowFlags flags = dialog->windowFlags();
diff --git a/krita/ui/kis_multinode_property.cpp b/krita/ui/kis_multinode_property.cpp
new file mode 100644
index 0000000..78d62c0
--- /dev/null
+++ b/krita/ui/kis_multinode_property.cpp
@@ -0,0 +1,86 @@
+/*
+ *  Copyright (c) 2016 Dmitry Kazakov <dimula73 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.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_multinode_property.h"
+
+/******************************************************************/
+/*               MultinodePropertyConnectorInterface              */
+/******************************************************************/
+
+MultinodePropertyConnectorInterface::~MultinodePropertyConnectorInterface()
+{
+}
+
+void MultinodePropertyConnectorInterface::connectValueChangedSignal(const QObject *receiver, const char *method, Qt::ConnectionType type) {
+    connect(this, SIGNAL(sigValueChanged()), receiver, method, type);
+    notifyValueChanged();
+}
+
+void MultinodePropertyConnectorInterface::notifyValueChanged() {
+    emit sigValueChanged();
+}
+
+/******************************************************************/
+/*               MultinodePropertyBaseConnector                   */
+/******************************************************************/
+
+MultinodePropertyBaseConnector::MultinodePropertyBaseConnector(KisMultinodePropertyInterface *parent)
+    : m_parent(parent)
+{
+}
+
+void MultinodePropertyBaseConnector::connectIgnoreCheckBox(QCheckBox *ignoreBox) {
+    m_ignoreBox = ignoreBox;
+
+    if (!m_parent->isIgnored() && !m_parent->savedValuesDiffer()) {
+        m_ignoreBox->setEnabled(false);
+        m_ignoreBox->setChecked(true);
+
+        if (m_parent->haveTheOnlyNode()) {
+            m_ignoreBox->setVisible(false);
+        }
+    } else {
+        connect(m_ignoreBox, SIGNAL(stateChanged(int)), SLOT(slotIgnoreCheckBoxChanged(int)));
+        m_ignoreBox->setEnabled(true);
+        m_ignoreBox->setChecked(!m_parent->isIgnored());
+    }
+}
+
+void MultinodePropertyBaseConnector::slotIgnoreCheckBoxChanged(int state) {
+    m_parent->setIgnored(state != Qt::Checked);
+}
+
+void MultinodePropertyBaseConnector::notifyIgnoreChanged() {
+    if (!m_ignoreBox) return;
+
+    if (m_ignoreBox->isChecked() != !m_parent->isIgnored()) {
+        m_ignoreBox->setChecked(!m_parent->isIgnored());
+    }
+}
+
+/******************************************************************/
+/*               KisMultinodePropertyInterface                    */
+/******************************************************************/
+
+KisMultinodePropertyInterface::KisMultinodePropertyInterface()
+{
+}
+
+KisMultinodePropertyInterface::~KisMultinodePropertyInterface()
+{
+}
diff --git a/krita/ui/kis_multinode_property.h b/krita/ui/kis_multinode_property.h
new file mode 100644
index 0000000..63d186b
--- /dev/null
+++ b/krita/ui/kis_multinode_property.h
@@ -0,0 +1,578 @@
+/*
+ *  Copyright (c) 2016 Dmitry Kazakov <dimula73 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.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_MULTINODE_PROPERTY_H
+#define __KIS_MULTINODE_PROPERTY_H
+
+#include <QObject>
+#include <QCheckBox>
+#include <QPointer>
+#include <QRegExp>
+#include <QBitArray>
+
+#include <KoColorSpace.h>
+#include <KoChannelInfo.h>
+
+#include "kis_node.h"
+#include "kis_layer.h"
+
+class KisMultinodePropertyInterface;
+class MultinodePropertyBaseConnector;
+template <class PropertyAdapter> class MultinodePropertyBoolConnector;
+template <class PropertyAdapter> class KisMultinodeProperty;
+
+/******************************************************************/
+/*               Adapters                                         */
+/******************************************************************/
+
+struct BaseAdapter {
+    static KisNodeList filterNodes(KisNodeList nodes) { return nodes; }
+
+    void setNumNodes(int numNodes) { m_numNodes = numNodes; }
+    int m_numNodes = 0;
+};
+
+struct CompositeOpAdapter : public BaseAdapter {
+    typedef QString ValueType;
+    typedef MultinodePropertyBaseConnector ConnectorType;
+    static const bool forceIgnoreByDefault = false;
+
+    static ValueType propForNode(KisNodeSP node) {
+        return node->compositeOpId();
+    }
+
+    static void setPropForNode(KisNodeSP node, const ValueType &value, int index) {
+        Q_UNUSED(index);
+        node->setCompositeOpId(value);
+    }
+};
+
+struct NameAdapter : public BaseAdapter {
+    typedef QString ValueType;
+    typedef MultinodePropertyBaseConnector ConnectorType;
+    static const bool forceIgnoreByDefault = true;
+
+    ValueType propForNode(KisNodeSP node) {
+        return m_numNodes == 1 ? node->name() : stripName(node->name());
+    }
+
+    void setPropForNode(KisNodeSP node, const ValueType &value, int index) {
+        QString name;
+
+        if (index < 0 || m_numNodes == 1) {
+            name = value;
+        } else {
+            name = QString("%1 %2").arg(stripName(value)).arg(index);
+        }
+
+        node->setName(name);
+    }
+
+private:
+    static QString stripName(QString name) {
+        QRegExp rexp("^(.+) (\\d{1,3})$");
+        int pos = rexp.indexIn(name);
+        if (pos > -1) {
+            name = rexp.cap(1);
+        }
+
+        return name;
+    }
+};
+
+struct OpacityAdapter : public BaseAdapter {
+    typedef int ValueType;
+    typedef MultinodePropertyBaseConnector ConnectorType;
+    static const bool forceIgnoreByDefault = false;
+
+    static ValueType propForNode(KisNodeSP node) {
+        return qRound(node->opacity() / 255.0 * 100);
+    }
+
+    static void setPropForNode(KisNodeSP node, const ValueType &value, int index) {
+        Q_UNUSED(index);
+        node->setOpacity(qRound(value * 255.0 / 100));
+    }
+};
+
+inline uint qHash(const KisNodeModel::Property &prop, uint seed = 0) {
+    return qHash(prop.name, seed);
+}
+
+struct LayerPropertyAdapter : public BaseAdapter {
+    typedef bool ValueType;
+    typedef MultinodePropertyBoolConnector<LayerPropertyAdapter> ConnectorType;
+    static const bool forceIgnoreByDefault = false;
+
+    LayerPropertyAdapter(const QString &propName) : m_propName(propName) {}
+
+    ValueType propForNode(KisNodeSP node) {
+        KisNodeModel::PropertyList props = node->sectionModelProperties();
+        Q_FOREACH (const KisNodeModel::Property &prop, props) {
+            if (prop.name == m_propName) {
+                return prop.state.toBool();
+            }
+        }
+
+        return false;
+    }
+
+    void setPropForNode(KisNodeSP node, const ValueType &value, int index) {
+        Q_UNUSED(index);
+        bool stateChanged = false;
+
+        KisNodeModel::PropertyList props = node->sectionModelProperties();
+        KisNodeModel::PropertyList::iterator it = props.begin();
+        KisNodeModel::PropertyList::iterator end = props.end();
+        for (; it != end; ++it) {
+            if (it->name == m_propName) {
+                it->state = value;
+                stateChanged = true;
+                break;
+            }
+        }
+
+        if (stateChanged) {
+            node->setSectionModelProperties(props);
+        }
+    }
+
+    QString name() const {
+        return m_propName;
+    }
+
+    static KisNodeModel::PropertyList adaptersList(KisNodeList nodes) {
+        QHash<KisNodeModel::Property, int> adapters;
+
+        Q_FOREACH (KisNodeSP node, nodes) {
+            int sortingIndex = 0;
+            KisNodeModel::PropertyList props = node->sectionModelProperties();
+            Q_FOREACH (const KisNodeModel::Property &prop, props) {
+                if (prop.state.type() != QVariant::Bool) continue;
+
+                if (!adapters.contains(prop)) {
+                    adapters.insert(prop, sortingIndex);
+                } else {
+                    adapters[prop] = qMin(adapters[prop], sortingIndex);
+                }
+                sortingIndex++;
+            }
+        }
+
+        QMultiMap<int, KisNodeModel::Property> sortedAdapters;
+        auto it = adapters.constBegin();
+        auto end = adapters.constEnd();
+        for (; it != end; ++it) {
+            sortedAdapters.insert(it.value(), it.key());
+        }
+
+        return sortedAdapters.values();
+    }
+
+private:
+    QString m_propName;
+};
+
+// TODO: move to KisLayerUtils
+#include <functional>
+
+template <typename T>
+bool checkNodesDiffer(KisNodeList nodes, std::function<T(KisNodeSP)> checkerFunc)
+{
+    bool valueDiffers = false;
+    bool initialized = false;
+    T currentValue;
+    Q_FOREACH (KisNodeSP node, nodes) {
+        if (!initialized) {
+            currentValue = checkerFunc(node);
+            initialized = true;
+        } else if (currentValue != checkerFunc(node)) {
+            valueDiffers = true;
+            break;
+        }
+    }
+    return valueDiffers;
+}
+
+struct ChannelFlagAdapter : public BaseAdapter {
+    typedef bool ValueType;
+    typedef MultinodePropertyBoolConnector<ChannelFlagAdapter> ConnectorType;
+    static const bool forceIgnoreByDefault = false;
+
+    struct Property {
+        Property(QString _name, int _channelIndex) : name(_name), channelIndex(_channelIndex) {}
+        QString name;
+        int channelIndex;
+    };
+    typedef QList<Property> PropertyList;
+
+    ChannelFlagAdapter(const Property &prop) : m_prop(prop) {}
+
+    ValueType propForNode(KisNodeSP node) {
+        KisLayerSP layer = toLayer(node);
+        Q_ASSERT(layer);
+
+        QBitArray flags = layer->channelFlags();
+        if (flags.isEmpty()) return true;
+
+        return flags.testBit(m_prop.channelIndex);
+    }
+
+    void setPropForNode(KisNodeSP node, const ValueType &value, int index) {
+        Q_UNUSED(index);
+        KisLayerSP layer = toLayer(node);
+        Q_ASSERT(layer);
+
+        QBitArray flags = layer->channelFlags();
+        if (flags.isEmpty()) {
+            flags = QBitArray(layer->colorSpace()->channelCount(), true);
+        }
+
+        if (flags.testBit(m_prop.channelIndex) != value) {
+            flags.setBit(m_prop.channelIndex, value);
+            layer->setChannelFlags(flags);
+        }
+    }
+
+    QString name() const {
+        return m_prop.name;;
+    }
+
+    static PropertyList adaptersList(KisNodeList nodes) {
+        PropertyList props;
+
+        {
+            bool nodesDiffer = checkNodesDiffer<const KoColorSpace*>(nodes, [](KisNodeSP node) { return node->colorSpace(); });
+
+            if (nodesDiffer) {
+                return props;
+            }
+        }
+
+
+        QList<KoChannelInfo*> channels = nodes.first()->colorSpace()->channels();
+
+        int index = 0;
+        Q_FOREACH (KoChannelInfo *info, channels) {
+            props << Property(info->name(), index);
+            index++;
+        }
+
+        return props;
+    }
+
+    static KisNodeList filterNodes(KisNodeList nodes) {
+        KisNodeList filteredNodes;
+        Q_FOREACH (KisNodeSP node, nodes) {
+            if (toLayer(node)) {
+                filteredNodes << node;
+            }
+        }
+        return filteredNodes;
+    }
+private:
+    static KisLayerSP toLayer(KisNodeSP node) {
+        return dynamic_cast<KisLayer*>(node.data());
+    }
+private:
+    Property m_prop;
+};
+
+/******************************************************************/
+/*               MultinodePropertyConnectorInterface              */
+/******************************************************************/
+
+class KRITAUI_EXPORT MultinodePropertyConnectorInterface : public QObject
+{
+    Q_OBJECT
+public:
+    virtual ~MultinodePropertyConnectorInterface();
+
+    /**
+     * Public interface
+     */
+    virtual void connectIgnoreCheckBox(QCheckBox *ignoreBox) = 0;
+    void connectValueChangedSignal(const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
+
+Q_SIGNALS:
+    void sigValueChanged();
+
+protected Q_SLOTS:
+    virtual void slotIgnoreCheckBoxChanged(int state) = 0;
+
+public:
+    /**
+     * Interface for KisMultinodeProperty's notifications
+     */
+    virtual void notifyValueChanged();
+    virtual void notifyIgnoreChanged() = 0;
+};
+
+/******************************************************************/
+/*               MultinodePropertyBaseConnector                   */
+/******************************************************************/
+
+class KRITAUI_EXPORT MultinodePropertyBaseConnector : public MultinodePropertyConnectorInterface
+{
+public:
+    MultinodePropertyBaseConnector(KisMultinodePropertyInterface *parent);
+
+    void connectIgnoreCheckBox(QCheckBox *ignoreBox);
+    void notifyIgnoreChanged();
+
+protected:
+    void slotIgnoreCheckBoxChanged(int state);
+
+private:
+    QPointer<QCheckBox> m_ignoreBox;
+    KisMultinodePropertyInterface *m_parent;
+};
+
+/******************************************************************/
+/*               MultinodePropertyBoolConnector                   */
+/******************************************************************/
+
+template <class PropertyAdapter>
+class MultinodePropertyBoolConnector : public MultinodePropertyConnectorInterface
+{
+    typedef KisMultinodeProperty<PropertyAdapter> PropertyType;
+public:
+    MultinodePropertyBoolConnector(PropertyType *parent)
+        : m_parent(parent)
+    {
+    }
+
+    void connectIgnoreCheckBox(QCheckBox *ignoreBox) {
+        m_ignoreBox = ignoreBox;
+
+        if ((!m_parent->isIgnored() && !m_parent->savedValuesDiffer())
+            || m_parent->haveTheOnlyNode()) {
+
+            m_ignoreBox->setTristate(false);
+        } else {
+            m_ignoreBox->setTristate(true);
+        }
+        connect(m_ignoreBox, SIGNAL(stateChanged(int)), SLOT(slotIgnoreCheckBoxChanged(int)));
+    }
+
+    void notifyIgnoreChanged() {
+        // noop
+    }
+
+    void notifyValueChanged() {
+        if (m_ignoreBox) {
+            Qt::CheckState newState =
+                m_parent->isIgnored() ? Qt::PartiallyChecked :
+                bool(m_parent->value()) ? Qt::Checked :
+                Qt::Unchecked;
+
+            if (m_ignoreBox->checkState() != newState) {
+                m_ignoreBox->setCheckState(newState);
+            }
+        }
+        MultinodePropertyConnectorInterface::notifyValueChanged();
+    }
+protected:
+    void slotIgnoreCheckBoxChanged(int state) {
+        if (state == Qt::PartiallyChecked) {
+            m_parent->setIgnored(true);
+        } else {
+            m_parent->setIgnored(false);
+            m_parent->setValue(bool(state == Qt::Checked));
+        }
+    }
+
+private:
+    QPointer<QCheckBox> m_ignoreBox;
+    PropertyType *m_parent;
+};
+
+/******************************************************************/
+/*               KisMultinodePropertyInterface                    */
+/******************************************************************/
+
+class KRITAUI_EXPORT KisMultinodePropertyInterface
+{
+public:
+    KisMultinodePropertyInterface();
+    virtual ~KisMultinodePropertyInterface();
+
+    virtual void rereadCurrentValue() = 0;
+
+    virtual void setIgnored(bool value) = 0;
+    virtual bool isIgnored() const = 0;
+
+    virtual bool savedValuesDiffer() const = 0;
+    virtual bool haveTheOnlyNode() const = 0;
+
+    virtual void connectValueChangedSignal(const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection) = 0;
+    virtual void connectIgnoreCheckBox(QCheckBox *ignoreBox) = 0;
+};
+
+typedef QSharedPointer<KisMultinodePropertyInterface> KisMultinodePropertyInterfaceSP;
+
+/******************************************************************/
+/*               KisMultinodeProperty                             */
+/******************************************************************/
+
+template <class PropertyAdapter>
+class KisMultinodeProperty : public KisMultinodePropertyInterface
+{
+public:
+    typedef typename PropertyAdapter::ValueType ValueType;
+    typedef typename PropertyAdapter::ConnectorType ConnectorType;
+public:
+    KisMultinodeProperty(KisNodeList nodes, PropertyAdapter adapter = PropertyAdapter())
+        : m_nodes(PropertyAdapter::filterNodes(nodes)),
+          m_savedValuesDiffer(false),
+          m_propAdapter(adapter),
+          m_connector(new ConnectorType(this))
+    {
+        Q_ASSERT(!m_nodes.isEmpty());
+        m_propAdapter.setNumNodes(m_nodes.size());
+
+        ValueType lastValue = m_propAdapter.propForNode(m_nodes.first());
+        Q_FOREACH (KisNodeSP node, m_nodes) {
+            ValueType value = m_propAdapter.propForNode(node);
+            m_savedValues.append(value);
+
+            if (value != lastValue) {
+                m_savedValuesDiffer = true;
+            }
+
+            lastValue = value;
+        }
+
+        m_isIgnored =
+            m_nodes.size() > 1 && PropertyAdapter::forceIgnoreByDefault ?
+            true : m_savedValuesDiffer;
+
+        m_currentValue = defaultValue();
+    }
+    ~KisMultinodeProperty() {}
+
+    void rereadCurrentValue() {
+        if (m_isIgnored) return;
+
+        ValueType lastValue = m_propAdapter.propForNode(m_nodes.first());
+        Q_FOREACH (KisNodeSP node, m_nodes) {
+            ValueType value = m_propAdapter.propForNode(node);
+
+            if (value != lastValue) {
+                qWarning() << "WARNING: mutiprops: values differ after reread!";
+            }
+
+            lastValue = value;
+        }
+
+        if (lastValue != m_currentValue) {
+            m_currentValue = lastValue;
+            m_connector->notifyValueChanged();
+        }
+    }
+
+    void setValue(const ValueType &value) {
+        Q_ASSERT(!m_isIgnored);
+        if (value == m_currentValue) return;
+
+        int index = 0;
+
+        Q_FOREACH (KisNodeSP node, m_nodes) {
+            m_propAdapter.setPropForNode(node, value, index);
+            index++;
+        }
+
+        m_currentValue = value;
+        m_connector->notifyValueChanged();
+    }
+
+    ValueType value() const {
+        return m_currentValue;
+    }
+
+    void setIgnored(bool value) {
+        if (value == m_isIgnored) return;
+
+        m_isIgnored = value;
+        if (m_isIgnored) {
+            int index = 0;
+            Q_FOREACH (KisNodeSP node, m_nodes) {
+                m_propAdapter.setPropForNode(node, m_savedValues[index], -1);
+                index++;
+            }
+            m_currentValue = defaultValue();
+        } else {
+            int index = 0;
+            Q_FOREACH (KisNodeSP node, m_nodes) {
+                m_propAdapter.setPropForNode(node, m_currentValue, index);
+                index++;
+            }
+        }
+
+        m_connector->notifyValueChanged();
+        m_connector->notifyIgnoreChanged();
+    }
+
+    bool isIgnored() const {
+        return m_isIgnored;
+    }
+
+    // TODO: disconnect methods...
+    void connectIgnoreCheckBox(QCheckBox *ignoreBox) {
+        m_connector->connectIgnoreCheckBox(ignoreBox);
+    }
+
+    void connectValueChangedSignal(const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection) {
+        m_connector->connectValueChangedSignal(receiver, method, type);
+    }
+
+    /**
+     * Interface for the connector
+     */
+
+    bool savedValuesDiffer() const {
+        return m_savedValuesDiffer;
+    }
+
+    bool haveTheOnlyNode() const {
+        return m_nodes.size() == 1;
+    }
+
+private:
+    ValueType defaultValue() const {
+        return m_savedValues.first();
+    }
+
+private:
+    bool m_isIgnored;
+    ValueType m_currentValue;
+
+    KisNodeList m_nodes;
+    QList<ValueType> m_savedValues;
+
+    bool m_savedValuesDiffer;
+    PropertyAdapter m_propAdapter;
+    QScopedPointer<MultinodePropertyConnectorInterface> m_connector;
+};
+
+
+typedef KisMultinodeProperty<CompositeOpAdapter> KisMultinodeCompositeOpProperty;
+typedef KisMultinodeProperty<OpacityAdapter> KisMultinodeOpacityProperty;
+typedef KisMultinodeProperty<NameAdapter> KisMultinodeNameProperty;
+
+#endif /* __KIS_MULTINODE_PROPERTY_H */
diff --git a/krita/ui/kis_node_model.h b/krita/ui/kis_node_model.h
index c8f789f..f4208df 100644
--- a/krita/ui/kis_node_model.h
+++ b/krita/ui/kis_node_model.h
@@ -191,6 +191,10 @@ public:
         state while in stasis */
         bool stateInStasis;
 
+        bool operator==(const Property &rhs) const {
+            return rhs.name == name;
+        }
+
         /// Default constructor. Use if you want to assign the members manually.
         Property(): isMutable( false ) { }
 
diff --git a/krita/ui/tests/CMakeLists.txt b/krita/ui/tests/CMakeLists.txt
index d032568..87928ca 100644
--- a/krita/ui/tests/CMakeLists.txt
+++ b/krita/ui/tests/CMakeLists.txt
@@ -203,10 +203,18 @@ set(kis_asl_layer_style_serializer_test_SRCS kis_asl_layer_style_serializer_test
 kde4_add_unit_test(KisAslLayerStyleSerializerTest TESTNAME krita-ui-KisAslLayerStyleSerializerTest ${kis_asl_layer_style_serializer_test_SRCS})
 target_link_libraries(KisAslLayerStyleSerializerTest kritaui kritaimage ${QT_QTTEST_LIBRARY})
 
+########### next target ###############
+
 set(kis_node_juggler_compressed_test_SRCS kis_node_juggler_compressed_test.cpp  ../../sdk/tests/testutil.cpp)
 kde4_add_unit_test(KisNodeJugglerCompressedTest TESTNAME krita-image-BaseNodeTest ${kis_node_juggler_compressed_test_SRCS})
 target_link_libraries(KisNodeJugglerCompressedTest kritaimage kritaui Qt5::Test)
 
+########### next target ###############
+
+set(kis_multinode_property_test_SRCS kis_multinode_property_test.cpp)
+kde4_add_unit_test(KisMultinodePropertyTest TESTNAME krita-image-BaseNodeTest ${kis_multinode_property_test_SRCS})
+target_link_libraries(KisMultinodePropertyTest kritaimage kritaui Qt5::Test)
+
 if (HAVE_OPENGL)
 
 ########### next target ###############
diff --git a/krita/ui/tests/kis_multinode_property_test.cpp b/krita/ui/tests/kis_multinode_property_test.cpp
new file mode 100644
index 0000000..afe2100
--- /dev/null
+++ b/krita/ui/tests/kis_multinode_property_test.cpp
@@ -0,0 +1,129 @@
+/*
+ *  Copyright (c) 2016 Dmitry Kazakov <dimula73 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.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_multinode_property_test.h"
+
+#include <QTest>
+#include "testutil.h"
+
+#include <KoCompositeOpRegistry.h>
+
+#include "kis_multinode_property.h"
+
+
+void KisMultinodePropertyTest::test()
+{
+    TestUtil::MaskParent p;
+
+    KisPaintLayerSP layer1 = p.layer;
+    KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8);
+    KisPaintLayerSP layer3 = new KisPaintLayer(p.image, "paint3", OPACITY_OPAQUE_U8);
+
+    KisNodeList nodes;
+    nodes << layer1;
+    nodes << layer2;
+    nodes << layer3;
+
+    {
+        QScopedPointer<QCheckBox> box(new QCheckBox("test ignore"));
+        KisMultinodeCompositeOpProperty prop(nodes);
+
+        prop.connectIgnoreCheckBox(box.data());
+
+        QCOMPARE(prop.isIgnored(), false);
+        QCOMPARE(prop.value(), COMPOSITE_OVER);
+        QCOMPARE(box->isEnabled(), false);
+        QCOMPARE(box->isChecked(), true);
+
+        prop.setValue(COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(prop.value(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(box->isEnabled(), false);
+        QCOMPARE(box->isChecked(), true);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+
+        prop.setIgnored(true);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(prop.value(), COMPOSITE_OVER);
+        QCOMPARE(box->isEnabled(), false);
+        QCOMPARE(box->isChecked(), false);
+
+        prop.setIgnored(false);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(prop.value(), COMPOSITE_OVER);
+        QCOMPARE(box->isEnabled(), false);
+        QCOMPARE(box->isChecked(), true);
+    }
+
+    layer1->setCompositeOpId(COMPOSITE_ALPHA_DARKEN);
+    layer2->setCompositeOpId(COMPOSITE_OVER);
+    layer3->setCompositeOpId(COMPOSITE_OVER);
+
+    {
+        QScopedPointer<QCheckBox> box(new QCheckBox("test ignore"));
+        KisMultinodeCompositeOpProperty prop(nodes);
+
+        prop.connectIgnoreCheckBox(box.data());
+
+        QCOMPARE(prop.isIgnored(), true);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(prop.value(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(box->isEnabled(), true);
+        QCOMPARE(box->isChecked(), false);
+
+        prop.setIgnored(false);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(prop.value(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(box->isEnabled(), true);
+        QCOMPARE(box->isChecked(), true);
+
+        prop.setValue(COMPOSITE_OVER);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(prop.value(), COMPOSITE_OVER);
+        QCOMPARE(box->isEnabled(), true);
+        QCOMPARE(box->isChecked(), true);
+
+        prop.setIgnored(true);
+
+        QCOMPARE(layer1->compositeOpId(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(layer2->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(layer3->compositeOpId(), COMPOSITE_OVER);
+        QCOMPARE(prop.value(), COMPOSITE_ALPHA_DARKEN);
+        QCOMPARE(box->isEnabled(), true);
+        QCOMPARE(box->isChecked(), false);
+    }
+}
+
+QTEST_MAIN(KisMultinodePropertyTest)
diff --git a/krita/ui/tests/kis_multinode_property_test.h b/krita/ui/tests/kis_multinode_property_test.h
new file mode 100644
index 0000000..3b39900
--- /dev/null
+++ b/krita/ui/tests/kis_multinode_property_test.h
@@ -0,0 +1,31 @@
+/*
+ *  Copyright (c) 2016 Dmitry Kazakov <dimula73 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.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_MULTINODE_PROPERTY_TEST_H
+#define __KIS_MULTINODE_PROPERTY_TEST_H
+
+#include <QtTest/QtTest>
+
+class KisMultinodePropertyTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void test();
+};
+
+#endif /* __KIS_MULTINODE_PROPERTY_TEST_H */
diff --git a/krita/ui/widgets/kis_channelflags_widget.cpp b/krita/ui/widgets/kis_channelflags_widget.cpp
deleted file mode 100644
index 2557561..0000000
--- a/krita/ui/widgets/kis_channelflags_widget.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- *  Copyright (c) 2007 Boudewijn Rempt <boud at valdyas.org>
- *
- *  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.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  You should have received a copy of the GNU General Public License
- *  along with this program; if not, write to the Free Software
- *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-#include "widgets/kis_channelflags_widget.h"
-
-#include <QBitArray>
-#include <QScrollArea>
-#include <QWidget>
-#include <QCheckBox>
-#include <QVBoxLayout>
-
-#include <klocalizedstring.h>
-
-#include <KoChannelInfo.h>
-#include <KoColorSpace.h>
-
-#include <kis_debug.h>
-
-KisChannelFlagsWidget::KisChannelFlagsWidget(const KoColorSpace * colorSpace, QWidget * parent)
-        : QScrollArea(parent)
-{
-    setObjectName("KisChannelFlagsWidget");
-    setToolTip(i18n("Check the active channels in this layer. Only these channels will be affected by any operation."));
-    QWidget * w = new QWidget();
-    setBackgroundRole(QPalette::Window);
-    QVBoxLayout * vbl = new QVBoxLayout();
-
-    for (int i = 0; i < colorSpace->channels().size(); ++i) {
-        KoChannelInfo * channel = colorSpace->channels().at(i);
-        QCheckBox * bx = new QCheckBox(channel->name(), w);
-        connect(bx, SIGNAL(toggled(bool)), SIGNAL(channelSelectionChanced()));
-        bx->setCheckState(Qt::Checked);
-        vbl->addWidget(bx);
-        m_channelChecks.append(bx);
-    }
-
-    w->setLayout(vbl);
-    setWidget(w);
-
-}
-
-KisChannelFlagsWidget::~KisChannelFlagsWidget()
-{
-}
-
-void KisChannelFlagsWidget::setChannelFlags(const QBitArray & cf)
-{
-    dbgUI << "KisChannelFlagsWidget::setChannelFlags " << cf.isEmpty();
-    if (cf.isEmpty()) return;
-
-    for (int i = 0; i < qMin(m_channelChecks.size(), cf.size()); ++i) {
-        m_channelChecks.at(i)->setChecked(cf.testBit(i));
-    }
-}
-
-QBitArray KisChannelFlagsWidget::channelFlags() const
-{
-    bool allTrue = true;
-    QBitArray ba(m_channelChecks.size());
-
-    for (int i = 0; i < m_channelChecks.size(); ++i) {
-        bool flag = m_channelChecks.at(i)->isChecked();
-        if (!flag) allTrue = false;
-        qint32 idx = i;
-        ba.setBit(idx, flag);
-        dbgUI << " channel " << i << " is " << flag << ", allTrue = " << allTrue << ", so ba.testBit(" << i << ")" << " is " << ba.testBit(idx) << " at " << idx;
-    }
-    if (allTrue)
-        return QBitArray();
-    else {
-        return ba;
-    }
-}
diff --git a/krita/ui/widgets/kis_channelflags_widget.h b/krita/ui/widgets/kis_channelflags_widget.h
deleted file mode 100644
index a112fd7..0000000
--- a/krita/ui/widgets/kis_channelflags_widget.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- *  Copyright (c) 2007 Boudewijn Rempt <boud at valdyas.org>
- *
- *  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.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  You should have received a copy of the GNU General Public License
- *  along with this program; if not, write to the Free Software
- *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-#ifndef KIS_CHANNELFLAGS_WIDGET_H
-#define KIS_CHANNELFLAGS_WIDGET_H
-
-#include <QList>
-#include <QCheckBox>
-#include "kritaui_export.h"
-
-#include <QScrollArea>
-#include <QVector>
-
-class QBitArray;
-class KoColorSpace;
-
-
-/**
- * A simple widget that shows a list of checkboxes in a scrollable
- * area. The checkboxes correspond to the channels in a colorspace and
- * are in the order the channels are packed in the pixel. The
- * resulting QBitArray can be used to decide which channels to modify
- * or not for filters, composition and eveything else.
- */
-class KRITAUI_EXPORT KisChannelFlagsWidget : public QScrollArea
-{
-
-    Q_OBJECT
-
-public:
-
-    KisChannelFlagsWidget(const KoColorSpace *colorSpace, QWidget * parent = 0);
-    ~KisChannelFlagsWidget();
-
-public:
-
-    /**
-     * Set the channelflags -- they are supposed to be in pixel order.
-     */
-    void setChannelFlags(const QBitArray & channelFlags);
-
-    /**
-     * retrieve the channel flags, in pixel order.
-     */
-    QBitArray channelFlags() const;
-
-Q_SIGNALS:
-
-    void channelSelectionChanced();
-
-private:
-
-    QList<QCheckBox*> m_channelChecks;
-
-};
-
-#endif


More information about the kimageshop mailing list