File indexing completed on 2024-05-05 04:19:19
0001 // vim: set tabstop=4 shiftwidth=4 expandtab: 0002 /* 0003 Gwenview: an image viewer 0004 Copyright 2008 Aurélien Gâteau <agateau@kde.org> 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License 0008 as published by the Free Software Foundation; either version 2 0009 of the License, or (at your option) any later version. 0010 0011 This program is distributed in the hope that it will be useful, 0012 but WITHOUT ANY WARRANTY; without even the implied warranty of 0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0014 GNU General Public License for more details. 0015 0016 You should have received a copy of the GNU General Public License 0017 along with this program; if not, write to the Free Software 0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. 0019 0020 */ 0021 // Self 0022 #include "semanticinfocontextmanageritem.h" 0023 0024 // Qt 0025 #include <QAction> 0026 #include <QDialog> 0027 #include <QEvent> 0028 #include <QPainter> 0029 #include <QShortcut> 0030 #include <QStyle> 0031 #include <QTimer> 0032 #include <QVBoxLayout> 0033 0034 // KF 0035 #include <KActionCategory> 0036 #include <KActionCollection> 0037 #include <KIconLoader> 0038 #include <KLocalizedString> 0039 #include <KRatingPainter> 0040 #include <KSharedConfig> 0041 #include <KWindowConfig> 0042 0043 // Local 0044 #include "sidebar.h" 0045 #include "ui_semanticinfodialog.h" 0046 #include "ui_semanticinfosidebaritem.h" 0047 #include "viewmainpage.h" 0048 #include <lib/contextmanager.h> 0049 #include <lib/decoratedtag/decoratedtag.h> 0050 #include <lib/documentview/documentview.h> 0051 #include <lib/eventwatcher.h> 0052 #include <lib/flowlayout.h> 0053 #include <lib/hud/hudwidget.h> 0054 #include <lib/semanticinfo/semanticinfodirmodel.h> 0055 #include <lib/semanticinfo/sorteddirmodel.h> 0056 #include <lib/signalblocker.h> 0057 #include <lib/widgetfloater.h> 0058 0059 namespace Gwenview 0060 { 0061 static const int RATING_INDICATOR_HIDE_DELAY = 2000; 0062 0063 struct SemanticInfoDialog : public QDialog, public Ui_SemanticInfoDialog { 0064 SemanticInfoDialog(QWidget *parent) 0065 : QDialog(parent) 0066 { 0067 setLayout(new QVBoxLayout); 0068 auto mainWidget = new QWidget; 0069 layout()->addWidget(mainWidget); 0070 setupUi(mainWidget); 0071 mainWidget->layout()->setContentsMargins(0, 0, 0, 0); 0072 setWindowTitle(mainWidget->windowTitle()); 0073 0074 KWindowConfig::restoreWindowSize(windowHandle(), configGroup()); 0075 } 0076 0077 ~SemanticInfoDialog() override 0078 { 0079 KConfigGroup group = configGroup(); 0080 KWindowConfig::saveWindowSize(windowHandle(), group); 0081 } 0082 0083 KConfigGroup configGroup() const 0084 { 0085 KSharedConfigPtr config = KSharedConfig::openConfig(); 0086 return KConfigGroup(config, "SemanticInfoDialog"); 0087 } 0088 }; 0089 0090 /** 0091 * A QGraphicsPixmapItem-like class, but which inherits from QGraphicsWidget 0092 */ 0093 class GraphicsPixmapWidget : public QGraphicsWidget 0094 { 0095 public: 0096 void setPixmap(const QPixmap &pix) 0097 { 0098 mPix = pix; 0099 setMinimumSize(pix.size()); 0100 } 0101 0102 void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override 0103 { 0104 painter->drawPixmap((size().width() - mPix.width()) / 2, (size().height() - mPix.height()) / 2, mPix); 0105 } 0106 0107 private: 0108 QPixmap mPix; 0109 }; 0110 0111 class RatingIndicator : public HudWidget 0112 { 0113 public: 0114 RatingIndicator() 0115 : HudWidget() 0116 , mPixmapWidget(new GraphicsPixmapWidget) 0117 , mDeleteTimer(new QTimer(this)) 0118 { 0119 updatePixmap(0); 0120 setOpacity(0); 0121 init(mPixmapWidget, OptionNone); 0122 0123 mDeleteTimer->setInterval(RATING_INDICATOR_HIDE_DELAY); 0124 mDeleteTimer->setSingleShot(true); 0125 connect(mDeleteTimer, &QTimer::timeout, this, &HudWidget::fadeOut); 0126 connect(this, &HudWidget::fadedOut, this, &QObject::deleteLater); 0127 } 0128 0129 void setRating(int rating) 0130 { 0131 updatePixmap(rating); 0132 update(); 0133 mDeleteTimer->start(); 0134 fadeIn(); 0135 } 0136 0137 private: 0138 GraphicsPixmapWidget *mPixmapWidget = nullptr; 0139 QTimer *mDeleteTimer = nullptr; 0140 0141 void updatePixmap(int rating) 0142 { 0143 KRatingPainter ratingPainter; 0144 const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Small); 0145 QPixmap pix(iconSize * 5 + ratingPainter.spacing() * 4, iconSize); 0146 pix.fill(Qt::transparent); 0147 { 0148 QPainter painter(&pix); 0149 ratingPainter.paint(&painter, pix.rect(), rating); 0150 } 0151 mPixmapWidget->setPixmap(pix); 0152 } 0153 }; 0154 0155 struct SemanticInfoContextManagerItemPrivate : public Ui_SemanticInfoSideBarItem { 0156 SemanticInfoContextManagerItem *q; 0157 SideBarGroup *mGroup; 0158 KActionCollection *mActionCollection; 0159 ViewMainPage *mViewMainPage; 0160 QPointer<SemanticInfoDialog> mSemanticInfoDialog; 0161 TagInfo mTagInfo; 0162 QAction *mEditTagsAction; 0163 /** A list of all actions, so that we can disable them when necessary */ 0164 QList<QAction *> mActions; 0165 QPointer<RatingIndicator> mRatingIndicator; 0166 FlowLayout *mTagLayout; 0167 QLabel *mEditLabel; 0168 0169 void setupGroup() 0170 { 0171 mGroup = new SideBarGroup(); 0172 q->setWidget(mGroup); 0173 EventWatcher::install(mGroup, QEvent::Show, q, SLOT(update())); 0174 0175 auto container = new QWidget; 0176 setupUi(container); 0177 container->layout()->setContentsMargins(0, 0, 0, 0); 0178 mGroup->addWidget(container); 0179 mTagLayout = new FlowLayout; 0180 mTagLayout->setHorizontalSpacing(2); 0181 mTagLayout->setVerticalSpacing(2); 0182 mTagLayout->setContentsMargins(0, 0, 0, 0); 0183 mTagContainerWidget->setLayout(mTagLayout); 0184 DecoratedTag tempTag; 0185 tempTag.setVisible(false); 0186 mEditLabel = new QLabel(QStringLiteral("<a href='edit'>%1</a>").arg(i18n("Edit"))); 0187 mEditLabel->setVisible(false); 0188 mEditLabel->setContentsMargins(tempTag.contentsMargins().left() / 2, 0189 tempTag.contentsMargins().top(), 0190 tempTag.contentsMargins().right() / 2, 0191 tempTag.contentsMargins().bottom()); 0192 label_2->setContentsMargins(mEditLabel->contentsMargins()); 0193 0194 QObject::connect(mRatingWidget, SIGNAL(ratingChanged(int)), q, SLOT(slotRatingChanged(int))); 0195 0196 mDescriptionTextEdit->installEventFilter(q); 0197 0198 QObject::connect(mEditLabel, &QLabel::linkActivated, mEditTagsAction, &QAction::trigger); 0199 } 0200 0201 void setupActions() 0202 { 0203 auto edit = new KActionCategory(i18nc("@title actions category", "Edit"), mActionCollection); 0204 0205 mEditTagsAction = edit->addAction(QStringLiteral("edit_tags")); 0206 mEditTagsAction->setText(i18nc("@action", "Edit Tags")); 0207 mEditTagsAction->setIcon(QIcon::fromTheme(QStringLiteral("tag"))); 0208 mActionCollection->setDefaultShortcut(mEditTagsAction, Qt::CTRL | Qt::Key_T); 0209 QObject::connect(mEditTagsAction, &QAction::triggered, q, &SemanticInfoContextManagerItem::showSemanticInfoDialog); 0210 mActions << mEditTagsAction; 0211 0212 for (int rating = 0; rating <= 5; ++rating) { 0213 QAction *action = edit->addAction(QStringLiteral("rate_%1").arg(rating)); 0214 if (rating == 0) { 0215 action->setText(i18nc("@action Rating value of zero", "Zero")); 0216 } else { 0217 action->setText(QString(rating, QChar(0x22C6))); /* 0x22C6 is the 'star' character */ 0218 } 0219 mActionCollection->setDefaultShortcut(action, Qt::Key_0 + rating); 0220 QObject::connect(action, &QAction::triggered, q, [this, rating]() { 0221 mRatingWidget->setRating(rating * 2); 0222 }); 0223 mActions << action; 0224 } 0225 } 0226 0227 void updateTags() 0228 { 0229 QLayoutItem *item; 0230 while ((item = mTagLayout->takeAt(0))) { 0231 auto tag = item->widget(); 0232 if (tag != nullptr && tag != mEditLabel) { 0233 tag->deleteLater(); 0234 } 0235 } 0236 if (q->contextManager()->selectedFileItemList().isEmpty()) { 0237 mEditLabel->setVisible(false); 0238 return; 0239 } 0240 0241 AbstractSemanticInfoBackEnd *backEnd = q->contextManager()->dirModel()->semanticInfoBackEnd(); 0242 0243 TagInfo::ConstIterator it = mTagInfo.constBegin(), end = mTagInfo.constEnd(); 0244 QMap<QString, QString> labelMap; 0245 for (; it != end; ++it) { 0246 SemanticInfoTag tag = it.key(); 0247 QString label = backEnd->labelForTag(tag); 0248 if (!it.value()) { 0249 // Tag is not present for all urls 0250 label += '*'; 0251 } 0252 labelMap[label.toLower()] = label; 0253 } 0254 const QStringList labels(labelMap.values()); 0255 0256 for (const QString &label : labels) { 0257 auto decoratedTag = new DecoratedTag(label); 0258 mTagLayout->addWidget(decoratedTag); 0259 } 0260 mTagLayout->addWidget(mEditLabel); 0261 mEditLabel->setVisible(true); 0262 mTagLayout->update(); 0263 } 0264 0265 void updateSemanticInfoDialog() 0266 { 0267 mSemanticInfoDialog->mTagWidget->setEnabled(!q->contextManager()->selectedFileItemList().isEmpty()); 0268 mSemanticInfoDialog->mTagWidget->setTagInfo(mTagInfo); 0269 } 0270 }; 0271 0272 SemanticInfoContextManagerItem::SemanticInfoContextManagerItem(ContextManager *manager, KActionCollection *actionCollection, ViewMainPage *viewMainPage) 0273 : AbstractContextManagerItem(manager) 0274 , d(new SemanticInfoContextManagerItemPrivate) 0275 { 0276 d->q = this; 0277 d->mActionCollection = actionCollection; 0278 d->mViewMainPage = viewMainPage; 0279 0280 connect(contextManager(), &ContextManager::selectionChanged, this, &SemanticInfoContextManagerItem::slotSelectionChanged); 0281 connect(contextManager(), &ContextManager::selectionDataChanged, this, &SemanticInfoContextManagerItem::update); 0282 connect(contextManager(), &ContextManager::currentDirUrlChanged, this, &SemanticInfoContextManagerItem::update); 0283 0284 d->setupActions(); 0285 d->setupGroup(); 0286 } 0287 0288 SemanticInfoContextManagerItem::~SemanticInfoContextManagerItem() 0289 { 0290 delete d; 0291 } 0292 0293 inline int ratingForVariant(const QVariant &variant) 0294 { 0295 if (variant.isValid()) { 0296 return variant.toInt(); 0297 } else { 0298 return 0; 0299 } 0300 } 0301 0302 void SemanticInfoContextManagerItem::slotSelectionChanged() 0303 { 0304 update(); 0305 } 0306 0307 void SemanticInfoContextManagerItem::update() 0308 { 0309 const KFileItemList itemList = contextManager()->selectedFileItemList(); 0310 0311 bool first = true; 0312 int rating = 0; 0313 QString description; 0314 SortedDirModel *dirModel = contextManager()->dirModel(); 0315 0316 // This hash stores for how many items the tag is present 0317 // If you have 3 items, and only 2 have the "Holiday" tag, 0318 // then tagHash["Holiday"] will be 2 at the end of the loop. 0319 using TagHash = QHash<QString, int>; 0320 TagHash tagHash; 0321 0322 for (const KFileItem &item : itemList) { 0323 QModelIndex index = dirModel->indexForItem(item); 0324 0325 QVariant value = dirModel->data(index, SemanticInfoDirModel::RatingRole); 0326 if (first) { 0327 rating = ratingForVariant(value); 0328 } else if (rating != ratingForVariant(value)) { 0329 // Ratings aren't the same, reset 0330 rating = 0; 0331 } 0332 0333 QString indexDescription = index.data(SemanticInfoDirModel::DescriptionRole).toString(); 0334 if (first) { 0335 description = indexDescription; 0336 } else if (description != indexDescription) { 0337 description.clear(); 0338 } 0339 0340 // Fill tagHash, incrementing the tag count if it's already there 0341 const TagSet tagSet = TagSet::fromVariant(index.data(SemanticInfoDirModel::TagsRole)); 0342 for (const QString &tag : tagSet) { 0343 TagHash::Iterator it = tagHash.find(tag); 0344 if (it == tagHash.end()) { 0345 tagHash[tag] = 1; 0346 } else { 0347 ++it.value(); 0348 } 0349 } 0350 0351 first = false; 0352 } 0353 { 0354 SignalBlocker blocker(d->mRatingWidget); 0355 d->mRatingWidget->setRating(rating); 0356 } 0357 d->mDescriptionTextEdit->setText(description); 0358 0359 // Init tagInfo from tagHash 0360 d->mTagInfo.clear(); 0361 int itemCount = itemList.count(); 0362 TagHash::ConstIterator it = tagHash.constBegin(), end = tagHash.constEnd(); 0363 for (; it != end; ++it) { 0364 QString tag = it.key(); 0365 int count = it.value(); 0366 d->mTagInfo[tag] = count == itemCount; 0367 } 0368 0369 bool enabled = !contextManager()->selectedFileItemList().isEmpty(); 0370 for (QAction *action : qAsConst(d->mActions)) { 0371 action->setEnabled(enabled); 0372 } 0373 d->updateTags(); 0374 if (d->mSemanticInfoDialog) { 0375 d->updateSemanticInfoDialog(); 0376 } 0377 } 0378 0379 void SemanticInfoContextManagerItem::slotRatingChanged(int rating) 0380 { 0381 const KFileItemList itemList = contextManager()->selectedFileItemList(); 0382 0383 // Show rating indicator in view mode, and only if sidebar is not visible 0384 if (d->mViewMainPage->isVisible() && !d->mRatingWidget->isVisible()) { 0385 if (!d->mRatingIndicator.data()) { 0386 d->mRatingIndicator = new RatingIndicator; 0387 d->mViewMainPage->showMessageWidget(d->mRatingIndicator, Qt::AlignBottom | Qt::AlignHCenter); 0388 } 0389 d->mRatingIndicator->setRating(rating); 0390 } 0391 0392 SortedDirModel *dirModel = contextManager()->dirModel(); 0393 for (const KFileItem &item : itemList) { 0394 QModelIndex index = dirModel->indexForItem(item); 0395 dirModel->setData(index, rating, SemanticInfoDirModel::RatingRole); 0396 } 0397 } 0398 0399 void SemanticInfoContextManagerItem::storeDescription() 0400 { 0401 if (!d->mDescriptionTextEdit->document()->isModified()) { 0402 return; 0403 } 0404 d->mDescriptionTextEdit->document()->setModified(false); 0405 QString description = d->mDescriptionTextEdit->toPlainText(); 0406 const KFileItemList itemList = contextManager()->selectedFileItemList(); 0407 0408 SortedDirModel *dirModel = contextManager()->dirModel(); 0409 for (const KFileItem &item : itemList) { 0410 QModelIndex index = dirModel->indexForItem(item); 0411 dirModel->setData(index, description, SemanticInfoDirModel::DescriptionRole); 0412 } 0413 } 0414 0415 void SemanticInfoContextManagerItem::assignTag(const SemanticInfoTag &tag) 0416 { 0417 const KFileItemList itemList = contextManager()->selectedFileItemList(); 0418 0419 SortedDirModel *dirModel = contextManager()->dirModel(); 0420 for (const KFileItem &item : itemList) { 0421 QModelIndex index = dirModel->indexForItem(item); 0422 TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole)); 0423 if (!tags.contains(tag)) { 0424 tags << tag; 0425 dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole); 0426 } 0427 } 0428 } 0429 0430 void SemanticInfoContextManagerItem::removeTag(const SemanticInfoTag &tag) 0431 { 0432 const KFileItemList itemList = contextManager()->selectedFileItemList(); 0433 0434 SortedDirModel *dirModel = contextManager()->dirModel(); 0435 for (const KFileItem &item : itemList) { 0436 QModelIndex index = dirModel->indexForItem(item); 0437 TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole)); 0438 if (tags.contains(tag)) { 0439 tags.remove(tag); 0440 dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole); 0441 } 0442 } 0443 } 0444 0445 void SemanticInfoContextManagerItem::showSemanticInfoDialog() 0446 { 0447 if (!d->mSemanticInfoDialog) { 0448 d->mSemanticInfoDialog = new SemanticInfoDialog(d->mGroup); 0449 d->mSemanticInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true); 0450 0451 connect(d->mSemanticInfoDialog->mPreviousButton, 0452 &QAbstractButton::clicked, 0453 d->mActionCollection->action(QStringLiteral("go_previous")), 0454 &QAction::trigger); 0455 connect(d->mSemanticInfoDialog->mNextButton, &QAbstractButton::clicked, d->mActionCollection->action(QStringLiteral("go_next")), &QAction::trigger); 0456 connect(d->mSemanticInfoDialog->mButtonBox, &QDialogButtonBox::rejected, d->mSemanticInfoDialog.data(), &QWidget::close); 0457 0458 AbstractSemanticInfoBackEnd *backEnd = contextManager()->dirModel()->semanticInfoBackEnd(); 0459 d->mSemanticInfoDialog->mTagWidget->setSemanticInfoBackEnd(backEnd); 0460 connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagAssigned, this, &SemanticInfoContextManagerItem::assignTag); 0461 connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagRemoved, this, &SemanticInfoContextManagerItem::removeTag); 0462 } 0463 d->updateSemanticInfoDialog(); 0464 d->mSemanticInfoDialog->show(); 0465 } 0466 0467 bool SemanticInfoContextManagerItem::eventFilter(QObject *, QEvent *event) 0468 { 0469 if (event->type() == QEvent::FocusOut) { 0470 storeDescription(); 0471 } 0472 return false; 0473 } 0474 0475 } // namespace 0476 0477 #include "moc_semanticinfocontextmanageritem.cpp"