File indexing completed on 2024-04-28 03:59:12

0001 // This file is part of the KDE libraries
0002 // SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
0003 // SPDX-License-Identifier: LGPL-2.1-or-later
0004 
0005 #include "krecentfilesmenu.h"
0006 
0007 #include <QGuiApplication>
0008 #include <QIcon>
0009 #include <QScreen>
0010 #include <QSettings>
0011 #include <QStandardPaths>
0012 
0013 class RecentFilesEntry
0014 {
0015 public:
0016     QUrl url;
0017     QString displayName;
0018     QAction *action = nullptr;
0019 
0020     QString titleWithSensibleWidth(QWidget *widget) const
0021     {
0022         const QString urlString = url.toDisplayString(QUrl::PreferLocalFile);
0023         // Calculate 3/4 of screen geometry, we do not want
0024         // action titles to be bigger than that
0025         // Since we do not know in which screen we are going to show
0026         // we choose the min of all the screens
0027         int maxWidthForTitles = INT_MAX;
0028         const auto screens = QGuiApplication::screens();
0029         for (QScreen *screen : screens) {
0030             maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
0031         }
0032 
0033         const QFontMetrics fontMetrics = widget->fontMetrics();
0034 
0035         QString title = displayName + QLatin1String(" [") + urlString + QLatin1Char(']');
0036         const int nameWidth = fontMetrics.boundingRect(title).width();
0037         if (nameWidth > maxWidthForTitles) {
0038             // If it does not fit, try to cut only the whole path, though if the
0039             // name is too long (more than 3/4 of the whole text) we cut it a bit too
0040             const int displayNameMaxWidth = maxWidthForTitles * 3 / 4;
0041             QString cutNameValue;
0042             QString cutValue;
0043             if (nameWidth > displayNameMaxWidth) {
0044                 cutNameValue = fontMetrics.elidedText(displayName, Qt::ElideMiddle, displayNameMaxWidth);
0045                 cutValue = fontMetrics.elidedText(urlString, Qt::ElideMiddle, maxWidthForTitles - displayNameMaxWidth);
0046             } else {
0047                 cutNameValue = displayName;
0048                 cutValue = fontMetrics.elidedText(urlString, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
0049             }
0050             title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
0051         }
0052         return title;
0053     }
0054 
0055     explicit RecentFilesEntry(const QUrl &_url, const QString &_displayName, KRecentFilesMenu *menu)
0056         : url(_url)
0057         , displayName(_displayName)
0058     {
0059         action = new QAction(titleWithSensibleWidth(menu));
0060         QObject::connect(action, &QAction::triggered, action, [this, menu]() {
0061             Q_EMIT menu->urlTriggered(url);
0062         });
0063     }
0064 
0065     ~RecentFilesEntry()
0066     {
0067         delete action;
0068     }
0069 };
0070 
0071 class KRecentFilesMenuPrivate
0072 {
0073 public:
0074     explicit KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr);
0075 
0076     std::vector<RecentFilesEntry *>::iterator findEntry(const QUrl &url);
0077     void recentFilesChanged() const;
0078 
0079     KRecentFilesMenu *const q;
0080     QString m_group = QStringLiteral("RecentFiles");
0081     std::vector<RecentFilesEntry *> m_entries;
0082     QSettings *m_settings;
0083     size_t m_maximumItems = 10;
0084     QAction *m_noEntriesAction;
0085     QAction *m_clearAction;
0086 };
0087 
0088 KRecentFilesMenuPrivate::KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr)
0089     : q(q_ptr)
0090 {}
0091 
0092 std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::findEntry(const QUrl &url)
0093 {
0094     return std::find_if(m_entries.begin(), m_entries.end(), [url](RecentFilesEntry *entry) {
0095         return entry->url == url;
0096     });
0097 }
0098 
0099 void KRecentFilesMenuPrivate::recentFilesChanged() const
0100 {
0101     q->rebuildMenu();
0102     Q_EMIT q->recentFilesChanged();
0103 }
0104 
0105 KRecentFilesMenu::KRecentFilesMenu(const QString &title, QWidget *parent)
0106     : QMenu(title, parent)
0107     , d(new KRecentFilesMenuPrivate(this))
0108 {
0109     setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent")));
0110     const QString fileName =
0111         QStringLiteral("%1/%2_recentfiles").arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), QCoreApplication::applicationName());
0112     d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this);
0113 
0114     d->m_noEntriesAction = new QAction(tr("No Entries"));
0115     d->m_noEntriesAction->setDisabled(true);
0116 
0117     d->m_clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), tr("Clear List"));
0118 
0119     readFromFile();
0120 }
0121 
0122 KRecentFilesMenu::KRecentFilesMenu(QWidget *parent)
0123     : KRecentFilesMenu(tr("Recent Files"), parent)
0124 {
0125 }
0126 
0127 KRecentFilesMenu::~KRecentFilesMenu()
0128 {
0129     writeToFile();
0130     qDeleteAll(d->m_entries);
0131     delete d->m_clearAction;
0132     delete d->m_noEntriesAction;
0133 }
0134 
0135 void KRecentFilesMenu::readFromFile()
0136 {
0137     qDeleteAll(d->m_entries);
0138     d->m_entries.clear();
0139 
0140     d->m_settings->beginGroup(d->m_group);
0141     const int size = d->m_settings->beginReadArray(QStringLiteral("files"));
0142 
0143     d->m_entries.reserve(size);
0144 
0145     for (int i = 0; i < size; ++i) {
0146         d->m_settings->setArrayIndex(i);
0147 
0148         const QUrl url = d->m_settings->value(QStringLiteral("url")).toUrl();
0149         RecentFilesEntry *entry = new RecentFilesEntry(url, d->m_settings->value(QStringLiteral("displayName")).toString(), this);
0150         d->m_entries.push_back(entry);
0151     }
0152 
0153     d->m_settings->endArray();
0154     d->m_settings->endGroup();
0155 
0156     d->recentFilesChanged();
0157 }
0158 
0159 void KRecentFilesMenu::addUrl(const QUrl &url, const QString &name)
0160 {
0161     if (d->m_entries.size() == d->m_maximumItems) {
0162         delete d->m_entries.back();
0163         d->m_entries.pop_back();
0164     }
0165 
0166     // If it's already there remove the old one and reinsert so it appears as new
0167     auto it = d->findEntry(url);
0168     if (it != d->m_entries.cend()) {
0169         delete *it;
0170         d->m_entries.erase(it);
0171     }
0172 
0173     QString displayName = name;
0174 
0175     if (displayName.isEmpty()) {
0176         displayName = url.fileName();
0177     }
0178 
0179     RecentFilesEntry *entry = new RecentFilesEntry(url, displayName, this);
0180     d->m_entries.insert(d->m_entries.begin(), entry);
0181 
0182     d->recentFilesChanged();
0183 }
0184 
0185 void KRecentFilesMenu::removeUrl(const QUrl &url)
0186 {
0187     auto it = d->findEntry(url);
0188 
0189     if (it == d->m_entries.end()) {
0190         return;
0191     }
0192 
0193     delete *it;
0194     d->m_entries.erase(it);
0195 
0196     d->recentFilesChanged();
0197 }
0198 
0199 void KRecentFilesMenu::rebuildMenu()
0200 {
0201     clear();
0202 
0203     if (d->m_entries.empty()) {
0204         addAction(d->m_noEntriesAction);
0205         return;
0206     }
0207 
0208     for (const RecentFilesEntry *entry : d->m_entries) {
0209         addAction(entry->action);
0210     }
0211 
0212     addSeparator();
0213     addAction(d->m_clearAction);
0214 
0215     // reconnect d->m_clearAction, since it was disconnected in clear()
0216     connect(d->m_clearAction, &QAction::triggered, this, &KRecentFilesMenu::clearRecentFiles);
0217 }
0218 
0219 void KRecentFilesMenu::writeToFile()
0220 {
0221     d->m_settings->remove(QString());
0222     d->m_settings->beginGroup(d->m_group);
0223     d->m_settings->beginWriteArray(QStringLiteral("files"));
0224 
0225     int index = 0;
0226     for (const RecentFilesEntry *entry : d->m_entries) {
0227         d->m_settings->setArrayIndex(index);
0228         d->m_settings->setValue(QStringLiteral("url"), entry->url);
0229         d->m_settings->setValue(QStringLiteral("displayName"), entry->displayName);
0230         ++index;
0231     }
0232 
0233     d->m_settings->endArray();
0234     d->m_settings->endGroup();
0235     d->m_settings->sync();
0236 }
0237 
0238 QString KRecentFilesMenu::group() const
0239 {
0240     return d->m_group;
0241 }
0242 
0243 void KRecentFilesMenu::setGroup(const QString &group)
0244 {
0245     d->m_group = group;
0246     readFromFile();
0247 }
0248 
0249 int KRecentFilesMenu::maximumItems() const
0250 {
0251     return d->m_maximumItems;
0252 }
0253 
0254 void KRecentFilesMenu::setMaximumItems(size_t maximumItems)
0255 {
0256     d->m_maximumItems = maximumItems;
0257 
0258     // Truncate if there are more entries than the new maximum
0259     if (d->m_entries.size() > maximumItems) {
0260         qDeleteAll(d->m_entries.begin() + maximumItems, d->m_entries.end());
0261         d->m_entries.erase(d->m_entries.begin() + maximumItems, d->m_entries.end());
0262 
0263         d->recentFilesChanged();
0264     }
0265 }
0266 
0267 QList<QUrl> KRecentFilesMenu::recentFiles() const
0268 {
0269     QList<QUrl> urls;
0270     urls.reserve(d->m_entries.size());
0271     std::transform(d->m_entries.cbegin(), d->m_entries.cend(), std::back_inserter(urls), [](const RecentFilesEntry *entry) {
0272         return entry->url;
0273     });
0274 
0275     return urls;
0276 }
0277 
0278 void KRecentFilesMenu::clearRecentFiles()
0279 {
0280     qDeleteAll(d->m_entries);
0281     d->m_entries.clear();
0282 
0283     d->recentFilesChanged();
0284 }
0285 
0286 #include "moc_krecentfilesmenu.cpp"