File indexing completed on 2024-12-22 04:14:00

0001 /* This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
0003     SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org>
0004     SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org>
0005     SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
0006     SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org>
0007     SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org>
0008     SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org>
0009     SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
0010     SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org>
0011     SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
0012     SPDX-FileCopyrightText: 2022 Alvin Wong <alvin@alvinhc.com>
0013 
0014     SPDX-License-Identifier: LGPL-2.0-only
0015 */
0016 
0017 #include "krecentfilesaction.h"
0018 #include "krecentfilesaction_p.h"
0019 
0020 #include <QFile>
0021 #include <QGuiApplication>
0022 #include <QDir>
0023 #include <QMenu>
0024 #include <QComboBox>
0025 #include <QScreen>
0026 #include <QProxyStyle>
0027 #include <QStandardItemModel>
0028 #include <QStyleFactory>
0029 
0030 #include <kconfig.h>
0031 #include <kconfiggroup.h>
0032 #include <klocalizedstring.h>
0033 
0034 #include "KisRecentFilesManager.h"
0035 
0036 class KRecentFilesIconProxyStyle : public QProxyStyle
0037 {
0038 public:
0039     KRecentFilesIconProxyStyle(QStyle *style = nullptr)
0040         : QProxyStyle(style)
0041     {
0042     }
0043 
0044     int pixelMetric(PixelMetric metric, const QStyleOption *option = nullptr, const QWidget *widget = nullptr) const override
0045     {
0046         if (metric == QStyle::PM_SmallIconSize) {
0047             return 48;
0048         }
0049         return QProxyStyle::pixelMetric(metric, option, widget);
0050     }
0051 };
0052 
0053 KRecentFilesAction::KRecentFilesAction(QObject *parent)
0054     : KSelectAction(parent),
0055       d_ptr(new KRecentFilesActionPrivate(this))
0056 {
0057     Q_D(KRecentFilesAction);
0058     d->init();
0059 }
0060 
0061 KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent)
0062     : KSelectAction(parent),
0063       d_ptr(new KRecentFilesActionPrivate(this))
0064 {
0065     Q_D(KRecentFilesAction);
0066     d->init();
0067 
0068     // Want to keep the ampersands
0069     setText(text);
0070 }
0071 
0072 KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent)
0073     : KSelectAction(parent),
0074       d_ptr(new KRecentFilesActionPrivate(this))
0075 {
0076     Q_D(KRecentFilesAction);
0077     d->init();
0078 
0079     setIcon(icon);
0080     // Want to keep the ampersands
0081     setText(text);
0082 }
0083 
0084 void KRecentFilesActionPrivate::init()
0085 {
0086     Q_Q(KRecentFilesAction);
0087     delete q->menu();
0088     q->setMenu(new QMenu());
0089     q->setToolBarMode(KSelectAction::MenuMode);
0090     m_noEntriesAction = q->menu()->addAction(i18n("No Entries"));
0091     m_noEntriesAction->setObjectName(QLatin1String("no_entries"));
0092     m_noEntriesAction->setEnabled(false);
0093     clearSeparator = q->menu()->addSeparator();
0094     clearSeparator->setVisible(false);
0095     clearSeparator->setObjectName(QLatin1String("separator"));
0096     clearAction = q->menu()->addAction(i18n("Clear List"), q, SLOT(clearActionTriggered()));
0097     clearAction->setObjectName(QLatin1String("clear_action"));
0098     clearAction->setVisible(false);
0099     q->setEnabled(false);
0100     q->connect(q, SIGNAL(triggered(QAction*)), SLOT(_k_urlSelected(QAction*)));
0101 
0102     QString baseStyleName = q->menu()->style()->objectName();
0103     if (baseStyleName != QLatin1String("windows")) {
0104         // Force Fusion theme because other themes like Breeze doesn't
0105         // work well with QProxyStyle, may result in small icons.
0106         baseStyleName = QStringLiteral("fusion");
0107     }
0108     QStyle *baseStyle = QStyleFactory::create(baseStyleName);
0109     QStyle *newStyle = new KRecentFilesIconProxyStyle(baseStyle);
0110     newStyle->setParent(q->menu());
0111     q->menu()->setStyle(newStyle);
0112 
0113     q->connect(q->menu(),
0114             SIGNAL(aboutToShow()),
0115             SLOT(menuAboutToShow()));
0116 
0117     q->connect(KisRecentFilesManager::instance(),
0118             SIGNAL(fileAdded(const QUrl &)),
0119             SLOT(fileAdded(const QUrl &)));
0120     q->connect(KisRecentFilesManager::instance(),
0121             SIGNAL(fileRemoved(const QUrl &)),
0122             SLOT(fileRemoved(const QUrl &)));
0123     q->connect(KisRecentFilesManager::instance(),
0124             SIGNAL(listRenewed()),
0125             SLOT(listRenewed()));
0126 
0127     // We have to manually trigger the initial load because
0128     // KisRecentFilesManager is initialized earlier than this.
0129     q->rebuildEntries();
0130 }
0131 
0132 KRecentFilesAction::~KRecentFilesAction()
0133 {
0134     delete d_ptr;
0135 }
0136 
0137 void KRecentFilesActionPrivate::_k_urlSelected(QAction *action)
0138 {
0139     Q_Q(KRecentFilesAction);
0140     emit q->urlSelected(m_urls[action]);
0141 }
0142 
0143 void KRecentFilesActionPrivate::updateIcon(const QStandardItem *item)
0144 {
0145     if (!item) {
0146         return;
0147     }
0148     const QUrl url = item->data().toUrl();
0149     if (!url.isValid()) {
0150         return;
0151     }
0152     QAction *action = m_urls.key(url);
0153     if (!action) {
0154         return;
0155     }
0156     const QIcon icon = item->icon();
0157     if (icon.isNull()) {
0158         return;
0159     }
0160     action->setIcon(icon);
0161     action->setIconVisibleInMenu(true);
0162 }
0163 
0164 static QString titleWithSensibleWidth(const QString &nameValue, const QString &value)
0165 {
0166     // Calculate 3/4 of screen geometry, we do not want
0167     // action titles to be bigger than that
0168     // Since we do not know in which screen we are going to show
0169     // we choose the min of all the screens
0170     int maxWidthForTitles = INT_MAX;
0171     Q_FOREACH(const QScreen *screen, QGuiApplication::screens()) {
0172         maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
0173     }
0174     const QFontMetrics fontMetrics = QFontMetrics(QFont());
0175 
0176     QString title = nameValue + " [" + value + ']';
0177     if (fontMetrics.boundingRect(title).width() > maxWidthForTitles) {
0178         // If it does not fit, try to cut only the whole path, though if the
0179         // name is too long (more than 3/4 of the whole text) we cut it a bit too
0180         const int nameValueMaxWidth = maxWidthForTitles * 3 / 4;
0181         const int nameWidth = fontMetrics.boundingRect(nameValue).width();
0182         QString cutNameValue, cutValue;
0183         if (nameWidth > nameValueMaxWidth) {
0184             cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth);
0185             cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth);
0186         } else {
0187             cutNameValue = nameValue;
0188             cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
0189         }
0190         title = cutNameValue + " [" + cutValue + ']';
0191     }
0192     return title;
0193 }
0194 
0195 void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &/*name*/)
0196 {
0197     Q_D(KRecentFilesAction);
0198 
0199     menu()->insertAction(menu()->actions().value(0), action);
0200     d->m_urls.insert(action, url);
0201 }
0202 
0203 QAction *KRecentFilesAction::removeAction(QAction *action)
0204 {
0205     Q_D(KRecentFilesAction);
0206     KSelectAction::removeAction(action);
0207 
0208     d->m_urls.remove(action);
0209 
0210     return action;
0211 }
0212 
0213 void KRecentFilesAction::setRecentFilesModel(const QStandardItemModel *model)
0214 {
0215     Q_D(KRecentFilesAction);
0216 
0217     if (d->m_recentFilesModel) {
0218         disconnect(d->m_recentFilesModel, nullptr, this, nullptr);
0219     }
0220 
0221     d->m_recentFilesModel = model;
0222     // Do not connect the signals or populate the icons now, because we want
0223     // them to be lazy-loaded only when the menu is opened for the first time.
0224     d->m_fileIconsPopulated = false;
0225 }
0226 
0227 void KRecentFilesAction::modelItemChanged(QStandardItem *item)
0228 {
0229     Q_D(KRecentFilesAction);
0230     d->updateIcon(item);
0231 }
0232 
0233 void KRecentFilesAction::modelRowsInserted(const QModelIndex &/*parent*/, int first, int last)
0234 {
0235     Q_D(KRecentFilesAction);
0236     for (int i = first; i <= last; i++) {
0237         d->updateIcon(d->m_recentFilesModel->item(i));
0238     }
0239 }
0240 
0241 void KRecentFilesAction::menuAboutToShow()
0242 {
0243     Q_D(KRecentFilesAction);
0244     if (!d->m_fileIconsPopulated) {
0245         d->m_fileIconsPopulated = true;
0246         connect(d->m_recentFilesModel, SIGNAL(itemChanged(QStandardItem *)),
0247                 SLOT(modelItemChanged(QStandardItem *)));
0248         connect(d->m_recentFilesModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
0249                 SLOT(modelRowsInserted(const QModelIndex &, int, int)));
0250         // Populate the file icons only on first showing the menu, so lazy
0251         // loading actually works.
0252         const int count = d->m_recentFilesModel->rowCount();
0253         for (int i = 0; i < count; i++) {
0254             d->updateIcon(d->m_recentFilesModel->item(i));
0255         }
0256     }
0257 }
0258 
0259 void KRecentFilesAction::clearActionTriggered()
0260 {
0261     KisRecentFilesManager::instance()->clear();
0262 }
0263 
0264 void KRecentFilesAction::clearEntries()
0265 {
0266     Q_D(KRecentFilesAction);
0267     KSelectAction::clear();
0268     d->m_urls.clear();
0269     d->m_noEntriesAction->setVisible(true);
0270     d->clearSeparator->setVisible(false);
0271     d->clearAction->setVisible(false);
0272     setEnabled(false);
0273 }
0274 
0275 void KRecentFilesAction::rebuildEntries()
0276 {
0277     Q_D(KRecentFilesAction);
0278 
0279     clearEntries();
0280 
0281     QVector<KisRecentFilesEntry> items = KisRecentFilesManager::instance()->recentFiles();
0282     if (items.count() > d->m_visibleItemsCount) {
0283         items = items.mid(items.count() - d->m_visibleItemsCount);
0284     }
0285     bool thereAreEntries = false;
0286     Q_FOREACH(const auto &item, items) {
0287         QString value;
0288         if (item.m_url.isLocalFile()) {
0289             value = item.m_url.toLocalFile();
0290 #ifdef Q_OS_WIN
0291             // Convert forward slashes to backslashes
0292             value = QDir::toNativeSeparators(value);
0293 #endif
0294         } else {
0295             value = item.m_url.toDisplayString();
0296         }
0297         const QString nameValue = item.m_displayName;
0298         const QString title = titleWithSensibleWidth(nameValue, value);
0299         if (!value.isNull()) {
0300             thereAreEntries = true;
0301             addAction(new QAction(title, selectableActionGroup()), item.m_url, nameValue);
0302         }
0303     }
0304     if (thereAreEntries) {
0305         d->m_noEntriesAction->setVisible(false);
0306         d->clearSeparator->setVisible(true);
0307         d->clearAction->setVisible(true);
0308         setEnabled(true);
0309     }
0310 }
0311 
0312 void KRecentFilesAction::fileAdded(const QUrl &url)
0313 {
0314     Q_D(KRecentFilesAction);
0315     const QString name; // Dummy
0316 
0317     if (d->m_visibleItemsCount <= 0) {
0318         return;
0319     }
0320 
0321     // remove oldest item if already maxitems in list
0322     if (selectableActionGroup()->actions().count() >= d->m_visibleItemsCount) {
0323         // remove oldest added item
0324         delete removeAction(selectableActionGroup()->actions().first());
0325     }
0326 
0327     const QString tmpName = name.isEmpty() ? url.fileName() : name;
0328     const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile));
0329 
0330 #ifdef Q_OS_WIN
0331     const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl;
0332 #else
0333     const QString file = pathOrUrl;
0334 #endif
0335 
0336     d->m_noEntriesAction->setVisible(false);
0337     d->clearSeparator->setVisible(true);
0338     d->clearAction->setVisible(true);
0339     setEnabled(true);
0340     // add file to list
0341     const QString title = titleWithSensibleWidth(tmpName, file);
0342     QAction *action = new QAction(title, selectableActionGroup());
0343     addAction(action, url, tmpName);
0344 }
0345 
0346 void KRecentFilesAction::fileRemoved(const QUrl &url)
0347 {
0348     Q_D(KRecentFilesAction);
0349     for (QMap<QAction *, QUrl>::ConstIterator it = d->m_urls.constBegin(); it != d->m_urls.constEnd(); ++it) {
0350         if (it.value() == url) {
0351             delete removeAction(it.key());
0352             return;
0353         }
0354     }
0355 }
0356 
0357 void KRecentFilesAction::listRenewed()
0358 {
0359     rebuildEntries();
0360 }
0361 
0362 #include "moc_krecentfilesaction.cpp"