File indexing completed on 2023-09-24 11:39:11

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