Commit 871d539a authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Improve tags: allow adding, editing and reordering of tags

parent 83449faa
......@@ -41,6 +41,7 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "tagwidget.hpp"
#include "titler/titlewidget.h"
#include "ui_qtextclip_ui.h"
#include "macros.hpp"
#include "undohelper.hpp"
#include "xml/xml.hpp"
#include <dialogs/textbasededit.h>
......@@ -1107,7 +1108,7 @@ Bin::Bin(std::shared_ptr<ProjectItemModel> model, QWidget *parent, bool isMainBi
m_tagsWidget = new TagWidget(this);
connect(m_tagsWidget, &TagWidget::switchTag, this, &Bin::switchTag);
connect(m_tagsWidget, &TagWidget::updateProjectTags, this, &Bin::updateTags);
m_tagsWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
m_tagsWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_layout->addWidget(m_tagsWidget);
m_tagsWidget->setVisible(false);
......@@ -1517,8 +1518,10 @@ void Bin::slotUpdatePalette()
m_videoIcon.fill(Qt::transparent);
QPainter p(&m_audioIcon);
audioIcon.paint(&p, 0, 0, m_audioIcon.width(), m_audioIcon.height());
p.end();
QPainter p2(&m_videoIcon);
videoIcon.paint(&p2, 0, 0, m_videoIcon.width(), m_videoIcon.height());
p2.end();
m_audioUsedIcon = m_audioIcon;
KIconEffect::toMonochrome(m_audioUsedIcon, palette().link().color(), palette().link().color(), 1);
m_videoUsedIcon = m_videoIcon;
......@@ -2029,19 +2032,18 @@ const QString Bin::setDocument(KdenliveDoc *project, const QString &id)
}
//setBinEffectsEnabled(!binEffectsDisabled, false);
QMap <QString, QString> projectTags = m_doc->getProjectTags();
QMap <int, QStringList> projectTags = m_doc->getProjectTags();
m_tagsWidget->rebuildTags(projectTags);
rebuildFilters(projectTags);
rebuildFilters(projectTags.size());
return folderName;
}
void Bin::rebuildFilters(const QMap <QString, QString> &tags)
void Bin::rebuildFilters(int tagsCount)
{
m_filterMenu->clear();
// Add tag filters
QAction *clearFilter = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), i18n("Clear Filters"), this);
m_filterMenu->addAction(clearFilter);
int tagsCount = tags.size();
for (int i = 1; i <= tagsCount; i++) {
QAction *tag = pCore->window()->actionCollection()->action(QString("tag_%1").arg(i));
if (tag) {
......@@ -3507,10 +3509,137 @@ void Bin::switchTag(const QString &tag, bool add)
editTags(allClips, tag, add);
}
void Bin::updateTags(const QMap <QString, QString> &tags)
void Bin::updateTags(const QMap <int, QStringList> &previousTags, const QMap <int, QStringList> &tags)
{
rebuildFilters(tags);
pCore->updateProjectTags(tags);
Fun undo = [this, previousTags, tags]() {
m_tagsWidget->rebuildTags(previousTags);
rebuildFilters(previousTags.size());
pCore->updateProjectTags(tags.size(), previousTags);
return true;
};
Fun redo = [this, previousTags, tags]() {
m_tagsWidget->rebuildTags(tags);
rebuildFilters(tags.size());
pCore->updateProjectTags(previousTags.size(), tags);
return true;
};
// Check if some tags were removed
QList<QStringList> previous = previousTags.values();
QList<QStringList> updated = tags.values();
QStringList deletedTags;
QMap <QString, QString> modifiedTags;
for (auto p : previous) {
bool tagExists = false;
for (auto n : updated) {
if (n.first() == p.first()) {
// Tag still exists
if (n.at(1) != p.at(1)) {
// Tag color has changed
modifiedTags.insert(p.at(1), n.at(1));
}
tagExists = true;
break;
}
}
if (!tagExists) {
// Tag was removed
deletedTags << p.at(1);
}
}
if (!deletedTags.isEmpty()) {
// Remove tag from clips
for (auto &t : deletedTags) {
// Find clips with the tag
const QList <QString> clips = getAllClipsWithTag(t);
Fun update_tags_redo = [this, clips, t]() {
for (auto &cid : clips) {
std::shared_ptr<ProjectClip> clip = getBinClip(cid);
if (clip) {
QString tags = clip->tags();
QStringList tagsList = tags.split(QLatin1Char(';'));
tagsList.removeAll(t);
QMap <QString, QString> props;
props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
slotUpdateClipProperties(cid, props, false);
}
}
return true;
};
Fun update_tags_undo = [this, clips, t]() {
for (auto &cid : clips) {
std::shared_ptr<ProjectClip> clip = getBinClip(cid);
if (clip) {
QString tags = clip->tags();
QStringList tagsList = tags.split(QLatin1Char(';'));
if (!tagsList.contains(t)) {
tagsList << t;
}
QMap <QString, QString> props;
props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
slotUpdateClipProperties(cid, props, false);
}
}
return true;
};
UPDATE_UNDO_REDO(update_tags_redo, update_tags_undo, undo, redo);
}
}
if (!modifiedTags.isEmpty()) {
// Replace tag in clips
QMapIterator<QString, QString> i(modifiedTags);
while (i.hasNext()) {
// Find clips with the tag
i.next();
const QList <QString> clips = getAllClipsWithTag(i.key());
Fun update_tags_redo = [this, clips, previous = i.key(), updated = i.value()]() {
for (auto &cid : clips) {
std::shared_ptr<ProjectClip> clip = getBinClip(cid);
if (clip) {
QString tags = clip->tags();
QStringList tagsList = tags.split(QLatin1Char(';'));
tagsList.removeAll(previous);
tagsList << updated;
QMap <QString, QString> props;
props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
slotUpdateClipProperties(cid, props, false);
}
}
return true;
};
Fun update_tags_undo = [this, clips, previous = i.key(), updated = i.value()]() {
for (auto &cid : clips) {
std::shared_ptr<ProjectClip> clip = getBinClip(cid);
if (clip) {
QString tags = clip->tags();
QStringList tagsList = tags.split(QLatin1Char(';'));
tagsList.removeAll(updated);
if (!tagsList.contains(previous)) {
tagsList << previous;
}
QMap <QString, QString> props;
props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
slotUpdateClipProperties(cid, props, false);
}
}
return true;
};
UPDATE_UNDO_REDO(update_tags_redo, update_tags_undo, undo, redo);
}
}
redo();
pCore->pushUndo(undo, redo, i18n("Edit Tags"));
}
const QList<QString> Bin::getAllClipsWithTag(const QString &tag)
{
QList<QString> list;
QList<std::shared_ptr<ProjectClip>> allClipIds = m_itemModel->getRootFolder()->childClips();
for(const auto &clip : qAsConst(allClipIds)) {
if (clip->tags().contains(tag)) {
list << clip->clipId();
}
}
return list;
}
void Bin::editTags(const QList <QString> &allClips, const QString &tag, bool add)
......
......@@ -396,8 +396,8 @@ private slots:
void switchTag(const QString &tag, bool add);
/** @brief Update project tags
*/
void updateTags(const QMap <QString, QString> &tags);
void rebuildFilters(const QMap <QString, QString> &tags);
void updateTags(const QMap <int, QStringList> &previousTags, const QMap <int, QStringList> &tags);
void rebuildFilters(int tagsCount);
/** @brief Switch a tag on a clip list
*/
void editTags(const QList <QString> &allClips, const QString &tag, bool add);
......@@ -549,6 +549,8 @@ private:
QStringList m_audioThumbsList;
QString m_processingAudioThumb;
QMutex m_audioThumbMutex;
/** @brief This is a lock that ensures safety in case of concurrent access */
mutable QReadWriteLock m_lock;
/** @brief Total number of milliseconds to process for audio thumbnails */
long m_audioDuration;
/** @brief Total number of milliseconds already processed for audio thumbnails */
......@@ -568,6 +570,8 @@ private:
QString m_clipsCountMessage;
/** @brief Show the clip count and key binfing info in status bar. */
void showBinInfo();
/** @brief Find all clip Ids that have a specific tag. */
const QList<QString> getAllClipsWithTag(const QString &tag);
signals:
void itemUpdated(std::shared_ptr<AbstractProjectItem>);
......
......@@ -11,6 +11,10 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <KActionCollection>
#include <KLocalizedString>
#include <KColorCombo>
#include <KLineEdit>
#include <KMessageWidget>
#include <KIconEffect>
#include <QApplication>
#include <QDebug>
......@@ -34,20 +38,20 @@ DragButton::DragButton(int ix, const QString &tag, const QString &description, Q
, m_dragging(false)
{
setToolTip(description);
int iconSize = QFontInfo(font()).pixelSize() - 2;
QPixmap pix(iconSize, iconSize);
pix.fill(Qt::transparent);
QPainter p(&pix);
p.setRenderHint(QPainter::Antialiasing, true);
p.setBrush(QColor(m_tag));
p.drawRoundedRect(0, 0, iconSize, iconSize, iconSize/2, iconSize/2);
QImage img(iconSize(), QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QIcon icon = QIcon::fromTheme(QStringLiteral("tag"));
QPainter p(&img);
icon.paint(&p, 0, 0, img.width(), img.height());
p.end();
KIconEffect::toMonochrome(img, QColor(m_tag), QColor(m_tag), 1);
setAutoRaise(true);
setText(description);
setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
setCheckable(true);
QAction *ac = new QAction(description.isEmpty() ? i18n("Tag %1", ix) : description, this);
ac->setData(m_tag);
ac->setIcon(QIcon(pix));
ac->setIcon(QIcon(QPixmap::fromImage(img)));
ac->setCheckable(true);
setDefaultAction(ac);
pCore->window()->addAction(QString("tag_%1").arg(ix), ac, {}, QStringLiteral("bintags"));
......@@ -111,7 +115,7 @@ TagWidget::TagWidget(QWidget *parent)
{
setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
auto *lay = new QHBoxLayout;
lay->setContentsMargins(2, 0, 2, 0);
lay->setContentsMargins(0, 0, 0, 0);
lay->addStretch(10);
auto *config = new QToolButton(this);
QAction *ca = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure"), this);
......@@ -133,22 +137,27 @@ void TagWidget::setTagData(const QString &tagData)
}
}
void TagWidget::rebuildTags(const QMap <QString, QString> &newTags)
void TagWidget::rebuildTags(const QMap <int, QStringList> &newTags)
{
auto *lay = static_cast<QHBoxLayout *>(layout());
qDeleteAll(tags);
tags.clear();
int ix = 1;
QMapIterator<QString, QString> i(newTags);
int width = 30;
QMapIterator<int, QStringList> i(newTags);
while (i.hasNext()) {
i.next();
DragButton *tag1 = new DragButton(ix, i.key(), i.value(), this);
DragButton *tag1 = new DragButton(ix, i.value().at(1), i.value().at(2), this);
tag1->setFont(font());
connect(tag1, &DragButton::switchTag, this, &TagWidget::switchTag);
tags << tag1;
lay->insertWidget(ix - 1, tag1);
width += tag1->sizeHint().width();
ix++;
}
setMinimumWidth(width);
setFixedHeight(tags.first()->sizeHint().height());
updateGeometry();
}
void TagWidget::showTagsConfig()
......@@ -159,29 +168,180 @@ void TagWidget::showTagsConfig()
d.setLayout(l);
QLabel lab(i18n("Configure Project Tags"), &d);
QListWidget list(&d);
list.setDragEnabled(true);
list.setDragDropMode(QAbstractItemView::InternalMove);
l->addWidget(&lab);
l->addWidget(&list);
QHBoxLayout *lay = new QHBoxLayout;
l->addLayout(lay);
l->addWidget(buttonBox);
QList<QColor> existingTagColors;
int ix = 1;
// Build list of tags
QMap <int, QStringList> originalTags;
for (DragButton *tb : qAsConst(tags)) {
const QString color = tb->tag();
existingTagColors << color;
const QString desc = tb->description();
QIcon ic = tb->icon();
list.setIconSize(tb->iconSize());
auto *item = new QListWidgetItem(ic, desc, &list);
item->setData(Qt::UserRole, color);
item->setFlags(Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
item->setData(Qt::UserRole + 1, ix);
originalTags.insert(ix, {QString::number(ix),color,desc});
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
ix++;
}
d.connect(&list, &QListWidget::itemDoubleClicked, &d, [&d, &list, &colors = existingTagColors, &existingTagColors](QListWidgetItem *item) {
// Edit an existing tag
QDialog d2(&d);
d2.setWindowTitle(i18n("Edit Tag"));
QDialogButtonBox *buttonBox2 = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
auto *l2 = new QVBoxLayout;
d2.setLayout(l2);
auto *l3 = new QHBoxLayout;
KColorCombo cb;
l3->addWidget(&cb);
KLineEdit le;
le.setText(item->text());
QColor originalColor(item->data(Qt::UserRole).toString());
colors.removeAll(originalColor);
cb.setColor(originalColor);
l3->addWidget(&le);
l2->addLayout(l3);
KMessageWidget mw;
mw.setText(i18n("This color is already used in another tag"));
mw.setMessageType(KMessageWidget::Warning);
mw.setCloseButtonVisible(false);
mw.hide();
l2->addWidget(&mw);
l2->addWidget(buttonBox2);
d2.connect(buttonBox2, &QDialogButtonBox::rejected, &d2, &QDialog::reject);
d2.connect(buttonBox2, &QDialogButtonBox::accepted, &d2, &QDialog::accept);
connect(&le, &KLineEdit::textChanged, &d2, [buttonBox2, &le, &cb, &colors]() {
if (le.text().isEmpty()) {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(false);
} else {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(!colors.contains(cb.color()));
}
});
connect(&cb, &KColorCombo::activated, &d2, [buttonBox2, &colors, &mw, &le](const QColor &selectedColor) {
if (colors.contains(selectedColor)) {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(false);
mw.animatedShow();
} else {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(!le.text().isEmpty());
mw.animatedHide();
}
});
if (colors.contains(cb.color())) {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(false);
mw.animatedShow();
}
le.setFocus();
le.selectAll();
if (d2.exec() == QDialog::Accepted) {
QImage img(list.iconSize(), QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QIcon icon = QIcon::fromTheme(QStringLiteral("tag"));
QPainter p(&img);
icon.paint(&p, 0, 0, img.width(), img.height());
p.end();
KIconEffect::toMonochrome(img, QColor(cb.color()), QColor(cb.color()), 1);
item->setIcon(QIcon(QPixmap::fromImage(img)));
item->setText(le.text());
item->setData(Qt::UserRole, cb.color());
existingTagColors.removeAll(originalColor);
existingTagColors << cb.color();
}
});
QToolButton *tb = new QToolButton(&d);
tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
lay->addWidget(tb);
QToolButton *tb2 = new QToolButton(&d);
tb2->setIcon(QIcon::fromTheme(QStringLiteral("tag-new")));
lay->addWidget(tb2);
lay->addStretch(10);
d.connect(tb2, &QToolButton::clicked, &d, [&d, &list, &existingTagColors]() {
// Add a new tag
QDialog d2(&d);
d2.setWindowTitle(i18n("Add Tag"));
QDialogButtonBox *buttonBox2 = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
auto *l2 = new QVBoxLayout;
d2.setLayout(l2);
auto *l3 = new QHBoxLayout;
KColorCombo cb;
l3->addWidget(&cb);
KLineEdit le;
le.setText(i18n("New tag"));
l3->addWidget(&le);
l2->addLayout(l3);
KMessageWidget mw;
mw.setText(i18n("This color is already used in another tag"));
mw.setMessageType(KMessageWidget::Warning);
mw.setCloseButtonVisible(false);
mw.hide();
l2->addWidget(&mw);
l2->addWidget(buttonBox2);
d2.connect(buttonBox2, &QDialogButtonBox::rejected, &d2, &QDialog::reject);
d2.connect(buttonBox2, &QDialogButtonBox::accepted, &d2, &QDialog::accept);
connect(&le, &KLineEdit::textChanged, &d2, [buttonBox2, &le, &cb, &existingTagColors]() {
if (le.text().isEmpty()) {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(false);
} else {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(!existingTagColors.contains(cb.color()));
}
});
connect(&cb, &KColorCombo::activated, &d2, [buttonBox2, &existingTagColors, &mw, &le](const QColor &selectedColor) {
if (existingTagColors.contains(selectedColor)) {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(false);
mw.animatedShow();
} else {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(!le.text().isEmpty());
mw.animatedHide();
}
});
if (existingTagColors.contains(cb.color())) {
buttonBox2->button(QDialogButtonBox::Ok)->setEnabled(false);
mw.animatedShow();
}
le.setFocus();
le.selectAll();
if (d2.exec() == QDialog::Accepted) {
QImage img(list.iconSize(), QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QIcon icon = QIcon::fromTheme(QStringLiteral("tag"));
QPainter p(&img);
icon.paint(&p, 0, 0, img.width(), img.height());
p.end();
KIconEffect::toMonochrome(img, QColor(cb.color()), QColor(cb.color()), 1);
auto *item = new QListWidgetItem(QIcon(QPixmap::fromImage(img)), le.text(), &list);
item->setData(Qt::UserRole, cb.color());
item->setData(Qt::UserRole + 1, list.count());
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
existingTagColors << cb.color();
}
});
d.connect(tb, &QToolButton::clicked, &d, [&d, &list]() {
// Delete selected tag
auto *item = list.currentItem();
if (item) {
delete item;
}
});
d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject);
d.connect(buttonBox, &QDialogButtonBox::accepted, &d, &QDialog::accept);
if (d.exec() != QDialog::Accepted) {
return;
}
QMap <QString, QString> newTags;
QMap <int, QStringList> newTags;
int newIx = 1;
for (int i = 0; i < list.count(); i++) {
QListWidgetItem *item = list.item(i);
if (item) {
newTags.insert(item->data(Qt::UserRole).toString(), item->text());
newTags.insert(newIx, {QString::number(item->data(Qt::UserRole + 1).toInt()), item->data(Qt::UserRole).toString(),item->text()});
newIx++;
}
}
rebuildTags(newTags);
emit updateProjectTags(newTags);
emit updateProjectTags(originalTags, newTags);
}
......@@ -8,6 +8,7 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QToolButton>
#include <QScrollArea>
/** @class DragButton
......@@ -51,7 +52,7 @@ class TagWidget : public QWidget
public:
explicit TagWidget(QWidget *parent = nullptr);
void setTagData(const QString &tagData = QString());
void rebuildTags(const QMap <QString, QString> &newTags);
void rebuildTags(const QMap <int, QStringList> &newTags);
private:
QList <DragButton *> tags;
......@@ -59,5 +60,5 @@ private:
signals:
void switchTag(const QString &tag, bool add);
void updateProjectTags(QMap <QString, QString> newTags);
void updateProjectTags(const QMap <int, QStringList> &oldTags, const QMap <int, QStringList> &newTags);
};
......@@ -1068,22 +1068,22 @@ void Core::processInvalidFilter(const QString &service, const QString &id, const
if (m_guiConstructed) emit m_mainWindow->assetPanelWarning(service, id, message);
}
void Core::updateProjectTags(const QMap <QString, QString> &tags)
{
// Clear previous tags
for (int i = 1 ; i< 20; i++) {
QString current = currentDoc()->getDocumentProperty(QString("tag%1").arg(i));
if (current.isEmpty()) {
break;
} else {
currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString());
void Core::updateProjectTags(int previousCount, const QMap <int, QStringList> &tags)
{
if (previousCount > tags.size()) {
// Clear previous tags
for (int i = 1 ; i <= previousCount; i++) {
QString current = currentDoc()->getDocumentProperty(QString("tag%1").arg(i));
if (!current.isEmpty()) {
currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString());
}
}
}
QMapIterator<QString, QString> j(tags);
QMapIterator<int, QStringList> j(tags);
int i = 1;
while (j.hasNext()) {
j.next();
currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString("%1:%2").arg(j.key(), j.value()));
currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString("%1:%2").arg(j.value().at(1), j.value().at(2)));
i++;
}
}
......
......@@ -231,7 +231,7 @@ public:
/** @brief An error occurred within a filter, inform user */
void processInvalidFilter(const QString &service, const QString &id, const QString &message);
/** @brief Update current project's tags */
void updateProjectTags(const QMap <QString, QString> &tags);
void updateProjectTags(int previousCount, const QMap <int, QStringList> &tags);
/** @brief Returns the project profile */
Mlt::Profile *getProjectProfile();
/** @brief Returns the consumer profile, that will be scaled
......
......@@ -1782,23 +1782,24 @@ bool KdenliveDoc::updatePreviewSettings(const QString &profile)
return false;
}
QMap <QString, QString> KdenliveDoc::getProjectTags()
QMap <int, QStringList> KdenliveDoc::getProjectTags() const
{
QMap <QString, QString> tags;
for (int i = 1 ; i< 20; i++) {
QMap <int, QStringList> tags;
int ix = 1;
for (int i = 1 ; i< 50; i++) {
QString current = getDocumentProperty(QString("tag%1").arg(i));
if (current.isEmpty()) {
break;
} else {
tags.insert(current.section(QLatin1Char(':'), 0, 0), current.section(QLatin1Char(':'), 1));
}
tags.insert(ix, {QString::number(ix), current.section(QLatin1Char(':'), 0, 0), current.section(QLatin1Char(':'), 1)});
ix++;
}
if (tags.isEmpty()) {
tags.insert(QStringLiteral("#ff0000"), i18n("Red"));
tags.insert(QStringLiteral("#00ff00"), i18n("Green"));
tags.insert(QStringLiteral("#0000ff"), i18n("Blue"));
tags.insert(QStringLiteral("#ffff00"), i18n("Yellow"));