File indexing completed on 2024-04-21 14:54:20

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 #include <KConfig>
0029 #include <KConfigGroup>
0030 #include <KLocalizedString>
0031 
0032 #include <set>
0033 
0034 KRecentFilesAction::KRecentFilesAction(QObject *parent)
0035     : KSelectAction(parent)
0036     , d_ptr(new KRecentFilesActionPrivate(this))
0037 {
0038     Q_D(KRecentFilesAction);
0039     d->init();
0040 }
0041 
0042 KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent)
0043     : KSelectAction(parent)
0044     , d_ptr(new KRecentFilesActionPrivate(this))
0045 {
0046     Q_D(KRecentFilesAction);
0047     d->init();
0048 
0049     // Want to keep the ampersands
0050     setText(text);
0051 }
0052 
0053 KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent)
0054     : KSelectAction(parent)
0055     , d_ptr(new KRecentFilesActionPrivate(this))
0056 {
0057     Q_D(KRecentFilesAction);
0058     d->init();
0059 
0060     setIcon(icon);
0061     // Want to keep the ampersands
0062     setText(text);
0063 }
0064 
0065 void KRecentFilesActionPrivate::init()
0066 {
0067     Q_Q(KRecentFilesAction);
0068     delete q->menu();
0069     q->setMenu(new QMenu());
0070     q->setToolBarMode(KSelectAction::MenuMode);
0071     m_noEntriesAction = q->menu()->addAction(i18n("No Entries"));
0072     m_noEntriesAction->setObjectName(QStringLiteral("no_entries"));
0073     m_noEntriesAction->setEnabled(false);
0074     clearSeparator = q->menu()->addSeparator();
0075     clearSeparator->setVisible(false);
0076     clearSeparator->setObjectName(QStringLiteral("separator"));
0077     clearAction = q->menu()->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear);
0078     clearAction->setObjectName(QStringLiteral("clear_action"));
0079     clearAction->setVisible(false);
0080     q->setEnabled(false);
0081     q->connect(q, qOverload<QAction *>(&KSelectAction::triggered), q, [this](QAction *action) {
0082         urlSelected(action);
0083     });
0084 }
0085 
0086 KRecentFilesAction::~KRecentFilesAction() = default;
0087 
0088 void KRecentFilesActionPrivate::urlSelected(QAction *action)
0089 {
0090     Q_Q(KRecentFilesAction);
0091 
0092     auto it = findByAction(action);
0093 
0094     Q_ASSERT(it != m_recentActions.cend()); // Should never happen
0095 
0096     const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules
0097     Q_EMIT q->urlSelected(url);
0098 }
0099 
0100 // TODO: remove this helper function, it will crash if you use it in a loop
0101 void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it)
0102 {
0103     Q_Q(KRecentFilesAction);
0104     delete q->KSelectAction::removeAction(it->action);
0105     m_recentActions.erase(it);
0106 }
0107 
0108 int KRecentFilesAction::maxItems() const
0109 {
0110     Q_D(const KRecentFilesAction);
0111     return d->m_maxItems;
0112 }
0113 
0114 void KRecentFilesAction::setMaxItems(int maxItems)
0115 {
0116     Q_D(KRecentFilesAction);
0117     // set new maxItems
0118     d->m_maxItems = std::max(maxItems, 0);
0119 
0120     // Remove all excess items, oldest (i.e. first added) first
0121     const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems;
0122     if (difference > 0) {
0123         auto beginIt = d->m_recentActions.begin();
0124         auto endIt = d->m_recentActions.begin() + difference;
0125         for (auto it = beginIt; it < endIt; ++it) {
0126             // Remove the action from the menus, action groups ...etc
0127             delete KSelectAction::removeAction(it->action);
0128         }
0129         d->m_recentActions.erase(beginIt, endIt);
0130     }
0131 }
0132 
0133 static QString titleWithSensibleWidth(const QString &nameValue, const QString &value)
0134 {
0135     // Calculate 3/4 of screen geometry, we do not want
0136     // action titles to be bigger than that
0137     // Since we do not know in which screen we are going to show
0138     // we choose the min of all the screens
0139     int maxWidthForTitles = INT_MAX;
0140     const auto screens = QGuiApplication::screens();
0141     for (QScreen *screen : screens) {
0142         maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
0143     }
0144     const QFontMetrics fontMetrics = QFontMetrics(QFont());
0145 
0146     QString title = nameValue + QLatin1String(" [") + value + QLatin1Char(']');
0147     const int nameWidth = fontMetrics.boundingRect(title).width();
0148     if (nameWidth > maxWidthForTitles) {
0149         // If it does not fit, try to cut only the whole path, though if the
0150         // name is too long (more than 3/4 of the whole text) we cut it a bit too
0151         const int nameValueMaxWidth = maxWidthForTitles * 3 / 4;
0152         QString cutNameValue;
0153         QString cutValue;
0154         if (nameWidth > nameValueMaxWidth) {
0155             cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth);
0156             cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth);
0157         } else {
0158             cutNameValue = nameValue;
0159             cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
0160         }
0161         title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
0162     }
0163     return title;
0164 }
0165 
0166 void KRecentFilesAction::addUrl(const QUrl &url, const QString &name)
0167 {
0168     Q_D(KRecentFilesAction);
0169 
0170     // ensure we never add items if we want none
0171     if (d->m_maxItems == 0) {
0172         return;
0173     }
0174 
0175     if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) {
0176         return;
0177     }
0178 
0179     // Remove url if it already exists in the list
0180     removeUrl(url);
0181 
0182     // Remove oldest item if already maxItems in list
0183     Q_ASSERT(d->m_maxItems > 0);
0184     if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) {
0185         d->removeAction(d->m_recentActions.begin());
0186     }
0187 
0188     const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile));
0189     const QString tmpName = !name.isEmpty() ? name : url.fileName();
0190 #ifdef Q_OS_WIN
0191     const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl;
0192 #else
0193     const QString file = pathOrUrl;
0194 #endif
0195 
0196     d->m_noEntriesAction->setVisible(false);
0197     d->clearSeparator->setVisible(true);
0198     d->clearAction->setVisible(true);
0199     setEnabled(true);
0200     // add file to list
0201     const QString title = titleWithSensibleWidth(tmpName, file);
0202 
0203     QAction *action = new QAction(title, selectableActionGroup());
0204     addAction(action, url, tmpName);
0205 }
0206 
0207 void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name)
0208 {
0209     Q_D(KRecentFilesAction);
0210 
0211     const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension);
0212     if (!mimeType.isDefault()) {
0213         action->setIcon(QIcon::fromTheme(mimeType.iconName()));
0214     }
0215     menu()->insertAction(menu()->actions().value(0), action);
0216     d->m_recentActions.push_back({action, url, name});
0217 }
0218 
0219 QAction *KRecentFilesAction::removeAction(QAction *action)
0220 {
0221     Q_D(KRecentFilesAction);
0222     auto it = d->findByAction(action);
0223     Q_ASSERT(it != d->m_recentActions.cend());
0224     d->m_recentActions.erase(it);
0225     return KSelectAction::removeAction(action);
0226 }
0227 
0228 void KRecentFilesAction::removeUrl(const QUrl &url)
0229 {
0230     Q_D(KRecentFilesAction);
0231 
0232     auto it = d->findByUrl(url);
0233 
0234     if (it != d->m_recentActions.cend()) {
0235         d->removeAction(it);
0236     };
0237 }
0238 
0239 QList<QUrl> KRecentFilesAction::urls() const
0240 {
0241     Q_D(const KRecentFilesAction);
0242 
0243     QList<QUrl> list;
0244     list.reserve(d->m_recentActions.size());
0245 
0246     using Info = KRecentFilesActionPrivate::RecentActionInfo;
0247     // Reverse order to match how the actions appear in the menu
0248     std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) {
0249         return info.url;
0250     });
0251 
0252     return list;
0253 }
0254 
0255 void KRecentFilesAction::clear()
0256 {
0257     clearEntries();
0258     Q_EMIT recentListCleared();
0259 }
0260 
0261 void KRecentFilesAction::clearEntries()
0262 {
0263     Q_D(KRecentFilesAction);
0264     KSelectAction::clear();
0265     d->m_recentActions.clear();
0266     d->m_noEntriesAction->setVisible(true);
0267     d->clearSeparator->setVisible(false);
0268     d->clearAction->setVisible(false);
0269     setEnabled(false);
0270 }
0271 
0272 void KRecentFilesAction::loadEntries(const KConfigGroup &_config)
0273 {
0274     Q_D(KRecentFilesAction);
0275     clearEntries();
0276 
0277     QString key;
0278     QString value;
0279     QString nameKey;
0280     QString nameValue;
0281     QString title;
0282     QUrl url;
0283 
0284     KConfigGroup cg = _config;
0285     // "<default>" means the group was constructed with an empty name
0286     if (cg.name() == QLatin1String("<default>")) {
0287         cg = KConfigGroup(cg.config(), "RecentFiles");
0288     }
0289 
0290     std::set<QUrl> seenUrls;
0291 
0292     bool thereAreEntries = false;
0293     // read file list
0294     for (int i = 1; i <= d->m_maxItems; i++) {
0295         key = QStringLiteral("File%1").arg(i);
0296         value = cg.readPathEntry(key, QString());
0297         if (value.isEmpty()) {
0298             continue;
0299         }
0300         url = QUrl::fromUserInput(value);
0301 
0302         auto [it, isNewUrl] = seenUrls.insert(url);
0303         // Don't restore if this url has already been restored (e.g. broken config)
0304         if (!isNewUrl) {
0305             continue;
0306         }
0307 
0308 #ifdef Q_OS_WIN
0309         // convert to backslashes
0310         if (url.isLocalFile()) {
0311             value = QDir::toNativeSeparators(value);
0312         }
0313 #endif
0314 
0315         nameKey = QStringLiteral("Name%1").arg(i);
0316         nameValue = cg.readPathEntry(nameKey, url.fileName());
0317         title = titleWithSensibleWidth(nameValue, value);
0318         if (!value.isNull()) {
0319             thereAreEntries = true;
0320             addAction(new QAction(title, selectableActionGroup()), url, nameValue);
0321         }
0322     }
0323     if (thereAreEntries) {
0324         d->m_noEntriesAction->setVisible(false);
0325         d->clearSeparator->setVisible(true);
0326         d->clearAction->setVisible(true);
0327         setEnabled(true);
0328     }
0329 }
0330 
0331 void KRecentFilesAction::saveEntries(const KConfigGroup &_cg)
0332 {
0333     Q_D(KRecentFilesAction);
0334 
0335     KConfigGroup cg = _cg;
0336     // "<default>" means the group was constructed with an empty name
0337     if (cg.name() == QLatin1String("<default>")) {
0338         cg = KConfigGroup(cg.config(), "RecentFiles");
0339     }
0340 
0341     cg.deleteGroup();
0342 
0343     // write file list
0344     int i = 1;
0345     for (const auto &[action, url, shortName] : d->m_recentActions) {
0346         cg.writePathEntry(QStringLiteral("File%1").arg(i), url.toDisplayString(QUrl::PreferLocalFile));
0347         cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName);
0348 
0349         ++i;
0350     }
0351 }
0352 
0353 #include "moc_krecentfilesaction.cpp"