raven-rhel8/extras/KF6/gear/gwenview/gwenview-24.05.1/app/semanticinfocontextmanageritem.cpp

478 lines
16 KiB
C++

// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
Copyright 2008 Aurélien Gâteau <agateau@kde.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, Cambridge, MA 02110-1301, USA.
*/
// Self
#include "semanticinfocontextmanageritem.h"
// Qt
#include <QAction>
#include <QDialog>
#include <QEvent>
#include <QPainter>
#include <QShortcut>
#include <QStyle>
#include <QTimer>
#include <QVBoxLayout>
// KF
#include <KActionCategory>
#include <KActionCollection>
#include <KIconLoader>
#include <KLocalizedString>
#include <KRatingPainter>
#include <KSharedConfig>
#include <KWindowConfig>
// Local
#include "sidebar.h"
#include "ui_semanticinfodialog.h"
#include "ui_semanticinfosidebaritem.h"
#include "viewmainpage.h"
#include <lib/contextmanager.h>
#include <lib/decoratedtag/decoratedtag.h>
#include <lib/documentview/documentview.h>
#include <lib/eventwatcher.h>
#include <lib/flowlayout.h>
#include <lib/hud/hudwidget.h>
#include <lib/semanticinfo/semanticinfodirmodel.h>
#include <lib/semanticinfo/sorteddirmodel.h>
#include <lib/signalblocker.h>
#include <lib/widgetfloater.h>
namespace Gwenview
{
static const int RATING_INDICATOR_HIDE_DELAY = 2000;
struct SemanticInfoDialog : public QDialog, public Ui_SemanticInfoDialog {
SemanticInfoDialog(QWidget *parent)
: QDialog(parent)
{
setLayout(new QVBoxLayout);
auto mainWidget = new QWidget;
layout()->addWidget(mainWidget);
setupUi(mainWidget);
mainWidget->layout()->setContentsMargins(0, 0, 0, 0);
setWindowTitle(mainWidget->windowTitle());
KWindowConfig::restoreWindowSize(windowHandle(), configGroup());
}
~SemanticInfoDialog() override
{
KConfigGroup group = configGroup();
KWindowConfig::saveWindowSize(windowHandle(), group);
}
KConfigGroup configGroup() const
{
KSharedConfigPtr config = KSharedConfig::openConfig();
return KConfigGroup(config, "SemanticInfoDialog");
}
};
/**
* A QGraphicsPixmapItem-like class, but which inherits from QGraphicsWidget
*/
class GraphicsPixmapWidget : public QGraphicsWidget
{
public:
void setPixmap(const QPixmap &pix)
{
mPix = pix;
setMinimumSize(pix.size());
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->drawPixmap((size().width() - mPix.width()) / 2, (size().height() - mPix.height()) / 2, mPix);
}
private:
QPixmap mPix;
};
class RatingIndicator : public HudWidget
{
public:
RatingIndicator()
: HudWidget()
, mPixmapWidget(new GraphicsPixmapWidget)
, mDeleteTimer(new QTimer(this))
{
updatePixmap(0);
setOpacity(0);
init(mPixmapWidget, OptionNone);
mDeleteTimer->setInterval(RATING_INDICATOR_HIDE_DELAY);
mDeleteTimer->setSingleShot(true);
connect(mDeleteTimer, &QTimer::timeout, this, &HudWidget::fadeOut);
connect(this, &HudWidget::fadedOut, this, &QObject::deleteLater);
}
void setRating(int rating)
{
updatePixmap(rating);
update();
mDeleteTimer->start();
fadeIn();
}
private:
GraphicsPixmapWidget *mPixmapWidget = nullptr;
QTimer *mDeleteTimer = nullptr;
void updatePixmap(int rating)
{
KRatingPainter ratingPainter;
const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Small);
QPixmap pix(iconSize * 5 + ratingPainter.spacing() * 4, iconSize);
pix.fill(Qt::transparent);
{
QPainter painter(&pix);
ratingPainter.paint(&painter, pix.rect(), rating);
}
mPixmapWidget->setPixmap(pix);
}
};
struct SemanticInfoContextManagerItemPrivate : public Ui_SemanticInfoSideBarItem {
SemanticInfoContextManagerItem *q;
SideBarGroup *mGroup;
KActionCollection *mActionCollection;
ViewMainPage *mViewMainPage;
QPointer<SemanticInfoDialog> mSemanticInfoDialog;
TagInfo mTagInfo;
QAction *mEditTagsAction;
/** A list of all actions, so that we can disable them when necessary */
QList<QAction *> mActions;
QPointer<RatingIndicator> mRatingIndicator;
FlowLayout *mTagLayout;
QLabel *mEditLabel;
void setupGroup()
{
mGroup = new SideBarGroup();
q->setWidget(mGroup);
EventWatcher::install(mGroup, QEvent::Show, q, SLOT(update()));
auto container = new QWidget;
setupUi(container);
container->layout()->setContentsMargins(0, 0, 0, 0);
mGroup->addWidget(container);
mTagLayout = new FlowLayout;
mTagLayout->setHorizontalSpacing(2);
mTagLayout->setVerticalSpacing(2);
mTagLayout->setContentsMargins(0, 0, 0, 0);
mTagContainerWidget->setLayout(mTagLayout);
DecoratedTag tempTag;
tempTag.setVisible(false);
mEditLabel = new QLabel(QStringLiteral("<a href='edit'>%1</a>").arg(i18n("Edit")));
mEditLabel->setVisible(false);
mEditLabel->setContentsMargins(tempTag.contentsMargins().left() / 2,
tempTag.contentsMargins().top(),
tempTag.contentsMargins().right() / 2,
tempTag.contentsMargins().bottom());
label_2->setContentsMargins(mEditLabel->contentsMargins());
QObject::connect(mRatingWidget, SIGNAL(ratingChanged(int)), q, SLOT(slotRatingChanged(int)));
mDescriptionTextEdit->installEventFilter(q);
QObject::connect(mEditLabel, &QLabel::linkActivated, mEditTagsAction, &QAction::trigger);
}
void setupActions()
{
auto edit = new KActionCategory(i18nc("@title actions category", "Edit"), mActionCollection);
mEditTagsAction = edit->addAction(QStringLiteral("edit_tags"));
mEditTagsAction->setText(i18nc("@action", "Edit Tags"));
mEditTagsAction->setIcon(QIcon::fromTheme(QStringLiteral("tag")));
mActionCollection->setDefaultShortcut(mEditTagsAction, Qt::CTRL | Qt::Key_T);
QObject::connect(mEditTagsAction, &QAction::triggered, q, &SemanticInfoContextManagerItem::showSemanticInfoDialog);
mActions << mEditTagsAction;
for (int rating = 0; rating <= 5; ++rating) {
QAction *action = edit->addAction(QStringLiteral("rate_%1").arg(rating));
if (rating == 0) {
action->setText(i18nc("@action Rating value of zero", "Zero"));
} else {
action->setText(QString(rating, QChar(0x22C6))); /* 0x22C6 is the 'star' character */
}
mActionCollection->setDefaultShortcut(action, Qt::Key_0 + rating);
QObject::connect(action, &QAction::triggered, q, [this, rating]() {
mRatingWidget->setRating(rating * 2);
});
mActions << action;
}
}
void updateTags()
{
QLayoutItem *item;
while ((item = mTagLayout->takeAt(0))) {
auto tag = item->widget();
if (tag != nullptr && tag != mEditLabel) {
tag->deleteLater();
}
}
if (q->contextManager()->selectedFileItemList().isEmpty()) {
mEditLabel->setVisible(false);
return;
}
AbstractSemanticInfoBackEnd *backEnd = q->contextManager()->dirModel()->semanticInfoBackEnd();
TagInfo::ConstIterator it = mTagInfo.constBegin(), end = mTagInfo.constEnd();
QMap<QString, QString> labelMap;
for (; it != end; ++it) {
SemanticInfoTag tag = it.key();
QString label = backEnd->labelForTag(tag);
if (!it.value()) {
// Tag is not present for all urls
label += '*';
}
labelMap[label.toLower()] = label;
}
const QStringList labels(labelMap.values());
for (const QString &label : labels) {
auto decoratedTag = new DecoratedTag(label);
mTagLayout->addWidget(decoratedTag);
}
mTagLayout->addWidget(mEditLabel);
mEditLabel->setVisible(true);
mTagLayout->update();
}
void updateSemanticInfoDialog()
{
mSemanticInfoDialog->mTagWidget->setEnabled(!q->contextManager()->selectedFileItemList().isEmpty());
mSemanticInfoDialog->mTagWidget->setTagInfo(mTagInfo);
}
};
SemanticInfoContextManagerItem::SemanticInfoContextManagerItem(ContextManager *manager, KActionCollection *actionCollection, ViewMainPage *viewMainPage)
: AbstractContextManagerItem(manager)
, d(new SemanticInfoContextManagerItemPrivate)
{
d->q = this;
d->mActionCollection = actionCollection;
d->mViewMainPage = viewMainPage;
connect(contextManager(), &ContextManager::selectionChanged, this, &SemanticInfoContextManagerItem::slotSelectionChanged);
connect(contextManager(), &ContextManager::selectionDataChanged, this, &SemanticInfoContextManagerItem::update);
connect(contextManager(), &ContextManager::currentDirUrlChanged, this, &SemanticInfoContextManagerItem::update);
d->setupActions();
d->setupGroup();
}
SemanticInfoContextManagerItem::~SemanticInfoContextManagerItem()
{
delete d;
}
inline int ratingForVariant(const QVariant &variant)
{
if (variant.isValid()) {
return variant.toInt();
} else {
return 0;
}
}
void SemanticInfoContextManagerItem::slotSelectionChanged()
{
update();
}
void SemanticInfoContextManagerItem::update()
{
const KFileItemList itemList = contextManager()->selectedFileItemList();
bool first = true;
int rating = 0;
QString description;
SortedDirModel *dirModel = contextManager()->dirModel();
// This hash stores for how many items the tag is present
// If you have 3 items, and only 2 have the "Holiday" tag,
// then tagHash["Holiday"] will be 2 at the end of the loop.
using TagHash = QHash<QString, int>;
TagHash tagHash;
for (const KFileItem &item : itemList) {
QModelIndex index = dirModel->indexForItem(item);
QVariant value = dirModel->data(index, SemanticInfoDirModel::RatingRole);
if (first) {
rating = ratingForVariant(value);
} else if (rating != ratingForVariant(value)) {
// Ratings aren't the same, reset
rating = 0;
}
QString indexDescription = index.data(SemanticInfoDirModel::DescriptionRole).toString();
if (first) {
description = indexDescription;
} else if (description != indexDescription) {
description.clear();
}
// Fill tagHash, incrementing the tag count if it's already there
const TagSet tagSet = TagSet::fromVariant(index.data(SemanticInfoDirModel::TagsRole));
for (const QString &tag : tagSet) {
TagHash::Iterator it = tagHash.find(tag);
if (it == tagHash.end()) {
tagHash[tag] = 1;
} else {
++it.value();
}
}
first = false;
}
{
SignalBlocker blocker(d->mRatingWidget);
d->mRatingWidget->setRating(rating);
}
d->mDescriptionTextEdit->setText(description);
// Init tagInfo from tagHash
d->mTagInfo.clear();
int itemCount = itemList.count();
TagHash::ConstIterator it = tagHash.constBegin(), end = tagHash.constEnd();
for (; it != end; ++it) {
QString tag = it.key();
int count = it.value();
d->mTagInfo[tag] = count == itemCount;
}
bool enabled = !contextManager()->selectedFileItemList().isEmpty();
for (QAction *action : qAsConst(d->mActions)) {
action->setEnabled(enabled);
}
d->updateTags();
if (d->mSemanticInfoDialog) {
d->updateSemanticInfoDialog();
}
}
void SemanticInfoContextManagerItem::slotRatingChanged(int rating)
{
const KFileItemList itemList = contextManager()->selectedFileItemList();
// Show rating indicator in view mode, and only if sidebar is not visible
if (d->mViewMainPage->isVisible() && !d->mRatingWidget->isVisible()) {
if (!d->mRatingIndicator.data()) {
d->mRatingIndicator = new RatingIndicator;
d->mViewMainPage->showMessageWidget(d->mRatingIndicator, Qt::AlignBottom | Qt::AlignHCenter);
}
d->mRatingIndicator->setRating(rating);
}
SortedDirModel *dirModel = contextManager()->dirModel();
for (const KFileItem &item : itemList) {
QModelIndex index = dirModel->indexForItem(item);
dirModel->setData(index, rating, SemanticInfoDirModel::RatingRole);
}
}
void SemanticInfoContextManagerItem::storeDescription()
{
if (!d->mDescriptionTextEdit->document()->isModified()) {
return;
}
d->mDescriptionTextEdit->document()->setModified(false);
QString description = d->mDescriptionTextEdit->toPlainText();
const KFileItemList itemList = contextManager()->selectedFileItemList();
SortedDirModel *dirModel = contextManager()->dirModel();
for (const KFileItem &item : itemList) {
QModelIndex index = dirModel->indexForItem(item);
dirModel->setData(index, description, SemanticInfoDirModel::DescriptionRole);
}
}
void SemanticInfoContextManagerItem::assignTag(const SemanticInfoTag &tag)
{
const KFileItemList itemList = contextManager()->selectedFileItemList();
SortedDirModel *dirModel = contextManager()->dirModel();
for (const KFileItem &item : itemList) {
QModelIndex index = dirModel->indexForItem(item);
TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole));
if (!tags.contains(tag)) {
tags << tag;
dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole);
}
}
}
void SemanticInfoContextManagerItem::removeTag(const SemanticInfoTag &tag)
{
const KFileItemList itemList = contextManager()->selectedFileItemList();
SortedDirModel *dirModel = contextManager()->dirModel();
for (const KFileItem &item : itemList) {
QModelIndex index = dirModel->indexForItem(item);
TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole));
if (tags.contains(tag)) {
tags.remove(tag);
dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole);
}
}
}
void SemanticInfoContextManagerItem::showSemanticInfoDialog()
{
if (!d->mSemanticInfoDialog) {
d->mSemanticInfoDialog = new SemanticInfoDialog(d->mGroup);
d->mSemanticInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true);
connect(d->mSemanticInfoDialog->mPreviousButton,
&QAbstractButton::clicked,
d->mActionCollection->action(QStringLiteral("go_previous")),
&QAction::trigger);
connect(d->mSemanticInfoDialog->mNextButton, &QAbstractButton::clicked, d->mActionCollection->action(QStringLiteral("go_next")), &QAction::trigger);
connect(d->mSemanticInfoDialog->mButtonBox, &QDialogButtonBox::rejected, d->mSemanticInfoDialog.data(), &QWidget::close);
AbstractSemanticInfoBackEnd *backEnd = contextManager()->dirModel()->semanticInfoBackEnd();
d->mSemanticInfoDialog->mTagWidget->setSemanticInfoBackEnd(backEnd);
connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagAssigned, this, &SemanticInfoContextManagerItem::assignTag);
connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagRemoved, this, &SemanticInfoContextManagerItem::removeTag);
}
d->updateSemanticInfoDialog();
d->mSemanticInfoDialog->show();
}
bool SemanticInfoContextManagerItem::eventFilter(QObject *, QEvent *event)
{
if (event->type() == QEvent::FocusOut) {
storeDescription();
}
return false;
}
} // namespace
#include "moc_semanticinfocontextmanageritem.cpp"