478 lines
16 KiB
C++
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"
|