Commit d592d520 authored by Dmitry Kazakov's avatar Dmitry Kazakov

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@kde.org
parent 30c07ba8
......@@ -25,7 +25,7 @@
#include "kis_node_model.h"
class KisLayerPropertiesIcons
class KRITAIMAGE_EXPORT KisLayerPropertiesIcons
{
public:
KisLayerPropertiesIcons();
......
......@@ -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
......
......@@ -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;
}
......@@ -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_
......
......@@ -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>&amp;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>&amp;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">