File indexing completed on 2023-09-24 07:59:36

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