File indexing completed on 2024-04-14 03:51:15

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
0004     SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org>
0005     SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org>
0006     SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
0007     SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org>
0008     SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org>
0009     SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org>
0010     SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
0011     SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org>
0012     SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
0013 
0014     SPDX-License-Identifier: LGPL-2.0-only
0015 */
0016 
0017 #include "krecentfilesaction.h"
0018 #include "krecentfilesaction_p.h"
0019 
0020 #include <QActionGroup>
0021 #include <QDir>
0022 #include <QGuiApplication>
0023 #include <QMenu>
0024 #include <QMimeDatabase>
0025 #include <QMimeType>
0026 #include <QScreen>
0027 
0028 #ifdef QT_DBUS_LIB
0029 #include <QDBusConnectionInterface>
0030 #include <QDBusInterface>
0031 #include <QDBusMessage>
0032 #endif
0033 
0034 #include <KConfig>
0035 #include <KConfigGroup>
0036 #include <KLocalizedString>
0037 #include <KShell>
0038 
0039 #include <set>
0040 
0041 KRecentFilesAction::KRecentFilesAction(QObject *parent)
0042     : KSelectAction(parent)
0043     , d_ptr(new KRecentFilesActionPrivate(this))
0044 {
0045     Q_D(KRecentFilesAction);
0046     d->init();
0047 }
0048 
0049 KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent)
0050     : KSelectAction(parent)
0051     , d_ptr(new KRecentFilesActionPrivate(this))
0052 {
0053     Q_D(KRecentFilesAction);
0054     d->init();
0055 
0056     // Want to keep the ampersands
0057     setText(text);
0058 }
0059 
0060 KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent)
0061     : KSelectAction(parent)
0062     , d_ptr(new KRecentFilesActionPrivate(this))
0063 {
0064     Q_D(KRecentFilesAction);
0065     d->init();
0066 
0067     setIcon(icon);
0068     // Want to keep the ampersands
0069     setText(text);
0070 }
0071 
0072 void KRecentFilesActionPrivate::init()
0073 {
0074     Q_Q(KRecentFilesAction);
0075     delete q->menu();
0076     q->setMenu(new QMenu());
0077     q->setToolBarMode(KSelectAction::MenuMode);
0078     m_noEntriesAction = q->menu()->addAction(i18n("No Entries"));
0079     m_noEntriesAction->setObjectName(QStringLiteral("no_entries"));
0080     m_noEntriesAction->setEnabled(false);
0081     clearSeparator = q->menu()->addSeparator();
0082     clearSeparator->setVisible(false);
0083     clearSeparator->setObjectName(QStringLiteral("separator"));
0084     clearAction = q->menu()->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear);
0085     clearAction->setObjectName(QStringLiteral("clear_action"));
0086     clearAction->setVisible(false);
0087     q->setEnabled(false);
0088     q->connect(q, &KSelectAction::actionTriggered, q, [this](QAction *action) {
0089         urlSelected(action);
0090     });
0091 }
0092 
0093 KRecentFilesAction::~KRecentFilesAction() = default;
0094 
0095 void KRecentFilesActionPrivate::urlSelected(QAction *action)
0096 {
0097     Q_Q(KRecentFilesAction);
0098 
0099     auto it = findByAction(action);
0100 
0101     Q_ASSERT(it != m_recentActions.cend()); // Should never happen
0102 
0103     const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules
0104     Q_EMIT q->urlSelected(url);
0105 }
0106 
0107 // TODO: remove this helper function, it will crash if you use it in a loop
0108 void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it)
0109 {
0110     Q_Q(KRecentFilesAction);
0111     delete q->KSelectAction::removeAction(it->action);
0112     m_recentActions.erase(it);
0113 }
0114 
0115 int KRecentFilesAction::maxItems() const
0116 {
0117     Q_D(const KRecentFilesAction);
0118     return d->m_maxItems;
0119 }
0120 
0121 void KRecentFilesAction::setMaxItems(int maxItems)
0122 {
0123     Q_D(KRecentFilesAction);
0124     // set new maxItems
0125     d->m_maxItems = std::max(maxItems, 0);
0126 
0127     // Remove all excess items, oldest (i.e. first added) first
0128     const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems;
0129     if (difference > 0) {
0130         auto beginIt = d->m_recentActions.begin();
0131         auto endIt = d->m_recentActions.begin() + difference;
0132         for (auto it = beginIt; it < endIt; ++it) {
0133             // Remove the action from the menus, action groups ...etc
0134             delete KSelectAction::removeAction(it->action);
0135         }
0136         d->m_recentActions.erase(beginIt, endIt);
0137     }
0138 }
0139 
0140 static QString titleWithSensibleWidth(const QString &nameValue, const QString &value)
0141 {
0142     // Calculate 3/4 of screen geometry, we do not want
0143     // action titles to be bigger than that
0144     // Since we do not know in which screen we are going to show
0145     // we choose the min of all the screens
0146     int maxWidthForTitles = INT_MAX;
0147     const auto screens = QGuiApplication::screens();
0148     for (QScreen *screen : screens) {
0149         maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
0150     }
0151     const QFontMetrics fontMetrics = QFontMetrics(QFont());
0152 
0153     QString title = nameValue + QLatin1String(" [") + value + QLatin1Char(']');
0154     const int nameWidth = fontMetrics.boundingRect(title).width();
0155     if (nameWidth > maxWidthForTitles) {
0156         // If it does not fit, try to cut only the whole path, though if the
0157         // name is too long (more than 3/4 of the whole text) we cut it a bit too
0158         const int nameValueMaxWidth = maxWidthForTitles * 3 / 4;
0159         QString cutNameValue;
0160         QString cutValue;
0161         if (nameWidth > nameValueMaxWidth) {
0162             cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth);
0163             cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth);
0164         } else {
0165             cutNameValue = nameValue;
0166             cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
0167         }
0168         title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
0169     }
0170     return title;
0171 }
0172 
0173 void KRecentFilesAction::addUrl(const QUrl &url, const QString &name)
0174 {
0175     Q_D(KRecentFilesAction);
0176 
0177     // ensure we never add items if we want none
0178     if (d->m_maxItems == 0) {
0179         return;
0180     }
0181 
0182     if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) {
0183         return;
0184     }
0185 
0186     // Remove url if it already exists in the list
0187     removeUrl(url);
0188 
0189     // Remove oldest item if already maxItems in list
0190     Q_ASSERT(d->m_maxItems > 0);
0191     if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) {
0192         d->removeAction(d->m_recentActions.begin());
0193     }
0194 
0195     const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile));
0196     const QString tmpName = !name.isEmpty() ? name : url.fileName();
0197 #ifdef Q_OS_WIN
0198     const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl;
0199 #else
0200     const QString file = pathOrUrl;
0201 #endif
0202 
0203     d->m_noEntriesAction->setVisible(false);
0204     d->clearSeparator->setVisible(true);
0205     d->clearAction->setVisible(true);
0206     setEnabled(true);
0207     // add file to list
0208     const QString title = titleWithSensibleWidth(tmpName, KShell::tildeCollapse(file));
0209 
0210     const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension);
0211 
0212 #ifdef QT_DBUS_LIB
0213     static bool isKdeSession = qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
0214     if (isKdeSession) {
0215         const QDBusConnection bus = QDBusConnection::sessionBus();
0216         if (bus.isConnected() && bus.interface()->isServiceRegistered(QStringLiteral("org.kde.ActivityManager"))) {
0217             const static QString activityService = QStringLiteral("org.kde.ActivityManager");
0218             const static QString activityResources = QStringLiteral("/ActivityManager/Resources");
0219             const static QString activityResouceInferface = QStringLiteral("org.kde.ActivityManager.Resources");
0220 
0221             const auto urlString = url.toString(QUrl::PreferLocalFile);
0222             QDBusMessage message =
0223                 QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceEvent"));
0224             message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */});
0225             bus.asyncCall(message);
0226 
0227             message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceMimetype"));
0228             message.setArguments({urlString, mimeType.name()});
0229             bus.asyncCall(message);
0230 
0231             message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceTitle"));
0232             message.setArguments({urlString, url.fileName()});
0233             bus.asyncCall(message);
0234         }
0235     }
0236 #endif
0237 
0238     QAction *action = new QAction(title, selectableActionGroup());
0239     addAction(action, url, tmpName, mimeType);
0240 }
0241 
0242 void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &_mimeType)
0243 {
0244     Q_D(KRecentFilesAction);
0245 
0246     auto mimeType = _mimeType;
0247     if (!mimeType.isValid()) {
0248         mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension);
0249     }
0250 
0251     if (!mimeType.isDefault()) {
0252         action->setIcon(QIcon::fromTheme(mimeType.iconName()));
0253     }
0254 
0255     menu()->insertAction(menu()->actions().value(0), action);
0256     d->m_recentActions.push_back({action, url, name});
0257 }
0258 
0259 QAction *KRecentFilesAction::removeAction(QAction *action)
0260 {
0261     Q_D(KRecentFilesAction);
0262     auto it = d->findByAction(action);
0263     Q_ASSERT(it != d->m_recentActions.cend());
0264     d->m_recentActions.erase(it);
0265     return KSelectAction::removeAction(action);
0266 }
0267 
0268 void KRecentFilesAction::removeUrl(const QUrl &url)
0269 {
0270     Q_D(KRecentFilesAction);
0271 
0272     auto it = d->findByUrl(url);
0273 
0274     if (it != d->m_recentActions.cend()) {
0275         d->removeAction(it);
0276     };
0277 }
0278 
0279 QList<QUrl> KRecentFilesAction::urls() const
0280 {
0281     Q_D(const KRecentFilesAction);
0282 
0283     QList<QUrl> list;
0284     list.reserve(d->m_recentActions.size());
0285 
0286     using Info = KRecentFilesActionPrivate::RecentActionInfo;
0287     // Reverse order to match how the actions appear in the menu
0288     std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) {
0289         return info.url;
0290     });
0291 
0292     return list;
0293 }
0294 
0295 void KRecentFilesAction::clear()
0296 {
0297     clearEntries();
0298     Q_EMIT recentListCleared();
0299 }
0300 
0301 void KRecentFilesAction::clearEntries()
0302 {
0303     Q_D(KRecentFilesAction);
0304     KSelectAction::clear();
0305     d->m_recentActions.clear();
0306     d->m_noEntriesAction->setVisible(true);
0307     d->clearSeparator->setVisible(false);
0308     d->clearAction->setVisible(false);
0309     setEnabled(false);
0310 }
0311 
0312 void KRecentFilesAction::loadEntries(const KConfigGroup &_config)
0313 {
0314     Q_D(KRecentFilesAction);
0315     clearEntries();
0316 
0317     QString key;
0318     QString value;
0319     QString nameKey;
0320     QString nameValue;
0321     QString title;
0322     QUrl url;
0323 
0324     KConfigGroup cg = _config;
0325     // "<default>" means the group was constructed with an empty name
0326     if (cg.name() == QLatin1String("<default>")) {
0327         cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
0328     }
0329 
0330     std::set<QUrl> seenUrls;
0331 
0332     bool thereAreEntries = false;
0333     // read file list
0334     for (int i = 1; i <= d->m_maxItems; i++) {
0335         key = QStringLiteral("File%1").arg(i);
0336         value = cg.readPathEntry(key, QString());
0337         if (value.isEmpty()) {
0338             continue;
0339         }
0340         url = QUrl::fromUserInput(value);
0341 
0342         auto [it, isNewUrl] = seenUrls.insert(url);
0343         // Don't restore if this url has already been restored (e.g. broken config)
0344         if (!isNewUrl) {
0345             continue;
0346         }
0347 
0348 #ifdef Q_OS_WIN
0349         // convert to backslashes
0350         if (url.isLocalFile()) {
0351             value = QDir::toNativeSeparators(value);
0352         }
0353 #endif
0354 
0355         nameKey = QStringLiteral("Name%1").arg(i);
0356         nameValue = cg.readPathEntry(nameKey, url.fileName());
0357         title = titleWithSensibleWidth(nameValue, KShell::tildeCollapse(value));
0358         if (!value.isNull()) {
0359             thereAreEntries = true;
0360             addAction(new QAction(title, selectableActionGroup()), url, nameValue);
0361         }
0362     }
0363     if (thereAreEntries) {
0364         d->m_noEntriesAction->setVisible(false);
0365         d->clearSeparator->setVisible(true);
0366         d->clearAction->setVisible(true);
0367         setEnabled(true);
0368     }
0369 }
0370 
0371 void KRecentFilesAction::saveEntries(const KConfigGroup &_cg)
0372 {
0373     Q_D(KRecentFilesAction);
0374 
0375     KConfigGroup cg = _cg;
0376     // "<default>" means the group was constructed with an empty name
0377     if (cg.name() == QLatin1String("<default>")) {
0378         cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
0379     }
0380 
0381     cg.deleteGroup();
0382 
0383     // write file list
0384     int i = 1;
0385     for (const auto &[action, url, shortName] : d->m_recentActions) {
0386         cg.writePathEntry(QStringLiteral("File%1").arg(i), url.toDisplayString(QUrl::PreferLocalFile));
0387         cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName);
0388 
0389         ++i;
0390     }
0391 }
0392 
0393 #include "moc_krecentfilesaction.cpp"