Index: src/CMakeLists.txt =================================================================== --- src/CMakeLists.txt +++ src/CMakeLists.txt @@ -38,6 +38,7 @@ kfontrequester.cpp kpassworddialog.cpp kruler.cpp + krecentfilesmenu.cpp kselector.cpp kxyselector.cpp kseparator.cpp @@ -155,6 +156,7 @@ KFontRequester KPasswordDialog KRuler + KRecentFilesMenu KSelector,KGradientSelector KTitleWidget KXYSelector Index: src/krecentfilesmenu.h =================================================================== --- /dev/null +++ src/krecentfilesmenu.h @@ -0,0 +1,106 @@ +/* Copyright 2020 Nicolas Fella + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef KRECENTFILESMENU_H +#define KRECENTFILESMENU_H + +#include + +#include +#include + +class KRecentFilesMenuPrivate; + +/** + * A menu that offers a set of recent files. + * + * @since 5.74 + * + */ +class KWIDGETSADDONS_EXPORT KRecentFilesMenu : public QMenu +{ + Q_OBJECT +public: + explicit KRecentFilesMenu(const QString &title, QWidget *parent = nullptr); + explicit KRecentFilesMenu(QWidget *parent = nullptr); + ~KRecentFilesMenu() override; + + /** + * The group the URLs are saved to/read from. + * Unless a group is specified by setGroup "RecentFiles" is used. + */ + QString group() const; + + /** + * Specify a group for storing the URLs. This allows e.g. storing multiple + * types of recent files. + * + * By default the group "RecentFiles" is used. + * + * @param group the name of the group. + */ + void setGroup(const QString &group); + + /** + * Add URL to recent files list. This will enable this action. + * + * @param url The URL of the file + * @param name The user visible pretty name that appears before the URL + */ + void addUrl(const QUrl &url, const QString &name = QString()); + + /** + * Remove a URL from the recent files list. + * + * @param url The URL of the file + */ + void removeUrl(const QUrl &url); + + /** + * The maximum number of files this menu can hold. + * + * When the maximum url count is reached and a new URL is added the + * oldest will be replaced. + * + * By default maximum 10 URLs are shown. + */ + int maximumItems() const; + + /** + * Set the maximum URL count. + * + * See \ref maximumItems + */ + void setMaximumItems(size_t maximumItems); + +Q_SIGNALS: + /** + * emitted when the user clicks on a file action. + * Usually this should result in the specified URL being opened. + * + * @param url The url associated with the triggered action. + */ + void urlTriggered(const QUrl &url); + +private: + void readFromFile(); + void writeToFile(); + void rebuildMenu(); + + std::unique_ptr const d; +}; + +#endif Index: src/krecentfilesmenu.cpp =================================================================== --- /dev/null +++ src/krecentfilesmenu.cpp @@ -0,0 +1,268 @@ +/* Copyright 2020 Nicolas Fella + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "krecentfilesmenu.h" + +#include +#include +#include +#include +#include +#include + +class RecentFilesEntry +{ +public: + QUrl url; + QString displayName; + QAction *action = nullptr; + + QString titleWithSensibleWidth(QWidget *widget) const + { + const QString urlString = url.toDisplayString(QUrl::PreferLocalFile); + // Calculate 3/4 of screen geometry, we do not want + // action titles to be bigger than that + // Since we do not know in which screen we are going to show + // we choose the min of all the screens + int maxWidthForTitles = INT_MAX; + const auto screens = QGuiApplication::screens(); + for (QScreen *screen : screens) { + maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4); + } + + const QFontMetrics fontMetrics = widget->fontMetrics(); + + QString title = displayName + QLatin1String(" [") + urlString + QLatin1Char(']'); + const int nameWidth = fontMetrics.boundingRect(title).width(); + if (nameWidth > maxWidthForTitles) { + // If it does not fit, try to cut only the whole path, though if the + // name is too long (more than 3/4 of the whole text) we cut it a bit too + const int displayNameMaxWidth = maxWidthForTitles * 3 / 4; + QString cutNameValue, cutValue; + if (nameWidth > displayNameMaxWidth) { + cutNameValue = fontMetrics.elidedText(displayName, Qt::ElideMiddle, displayNameMaxWidth); + cutValue = fontMetrics.elidedText(urlString, Qt::ElideMiddle, maxWidthForTitles - displayNameMaxWidth); + } else { + cutNameValue = displayName; + cutValue = fontMetrics.elidedText(urlString, Qt::ElideMiddle, maxWidthForTitles - nameWidth); + } + title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']'); + } + return title; + } + + explicit RecentFilesEntry(const QUrl &_url, const QString &_displayName, KRecentFilesMenu *menu) + : url(_url) + , displayName(_displayName) + { + action = new QAction(titleWithSensibleWidth(menu)); + QObject::connect(action, &QAction::triggered, action, [this, menu]() { + Q_EMIT menu->urlTriggered(url); + }); + } + + ~RecentFilesEntry() + { + delete action; + } +}; + +class KRecentFilesMenuPrivate { +public: + QString m_group = QStringLiteral("RecentFiles"); + std::vector m_entries; + QSettings *m_settings; + size_t m_maximumItems = 10; + QAction *m_noEntriesAction; + QAction *m_clearAction; + + std::vector::iterator findEntry(const QUrl &url); +}; + +KRecentFilesMenu::KRecentFilesMenu(const QString& title, QWidget *parent) + : QMenu(title, parent) + , d(new KRecentFilesMenuPrivate) +{ + setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent"))); + const QString fileName = QStringLiteral("%1/%2_recentfiles").arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), QCoreApplication::applicationName()); + d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this); + + d->m_noEntriesAction = new QAction(tr("No Entries")); + d->m_noEntriesAction->setDisabled(true); + + d->m_clearAction = new QAction(tr("Clear List")); + + readFromFile(); + rebuildMenu(); +} + +KRecentFilesMenu::KRecentFilesMenu(QWidget *parent) + : KRecentFilesMenu(tr("Recent Files"), parent) +{ +} + +KRecentFilesMenu::~KRecentFilesMenu() +{ + writeToFile(); + qDeleteAll(d->m_entries); + delete d->m_clearAction; + delete d->m_noEntriesAction; +} + +void KRecentFilesMenu::readFromFile() +{ + qDeleteAll(d->m_entries); + d->m_entries.clear(); + + d->m_settings->beginGroup(d->m_group); + const int size = d->m_settings->beginReadArray(QStringLiteral("files")); + + d->m_entries.reserve(size); + + for (int i = 0; i < size; ++i) { + d->m_settings->setArrayIndex(i); + + const QUrl url = d->m_settings->value(QStringLiteral("url")).toUrl(); + + // Don't restore if file doesn't exist anymore + if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) { + continue; + } + + RecentFilesEntry *entry = new RecentFilesEntry(url, d->m_settings->value(QStringLiteral("displayName")).toString(), this); + d->m_entries.push_back(entry); + } + + d->m_settings->endArray(); + d->m_settings->endGroup(); +} + +void KRecentFilesMenu::addUrl(const QUrl &url, const QString &name) +{ + if (d->m_entries.size() == d->m_maximumItems) { + delete d->m_entries.back(); + d->m_entries.pop_back(); + } + + // If it's already there remove the old one and reinsert so it appears as new + auto it = d->findEntry(url); + if (it != d->m_entries.cend()) { + delete *it; + d->m_entries.erase(it); + } + + QString displayName = name; + + if (displayName.isEmpty()) { + displayName = url.fileName(); + } + + RecentFilesEntry *entry = new RecentFilesEntry(url, displayName, this); + d->m_entries.insert(d->m_entries.begin(), entry); + rebuildMenu(); +} + +void KRecentFilesMenu::removeUrl(const QUrl &url) +{ + auto it = d->findEntry(url); + + if (it == d->m_entries.end()) { + return; + } + + delete *it; + d->m_entries.erase(it); + rebuildMenu(); +} + + +void KRecentFilesMenu::rebuildMenu() +{ + clear(); + + if (d->m_entries.empty()) { + addAction(d->m_noEntriesAction); + return; + } + + for (const RecentFilesEntry *entry : d->m_entries) { + addAction(entry->action); + } + + addSeparator(); + addAction(d->m_clearAction); + + connect(d->m_clearAction, &QAction::triggered, this, [this] { + qDeleteAll(d->m_entries); + d->m_entries.clear(); + rebuildMenu(); + }); +} + +void KRecentFilesMenu::writeToFile() +{ + d->m_settings->remove(QString()); + d->m_settings->beginGroup(d->m_group); + d->m_settings->beginWriteArray(QStringLiteral("files")); + + int index = 0; + for (const RecentFilesEntry *entry : d->m_entries) { + d->m_settings->setArrayIndex(index); + d->m_settings->setValue(QStringLiteral("url"), entry->url); + d->m_settings->setValue(QStringLiteral("displayName"), entry->displayName); + ++index; + } + + d->m_settings->endArray(); + d->m_settings->endGroup(); + d->m_settings->sync(); +} + +std::vector::iterator KRecentFilesMenuPrivate::findEntry(const QUrl &url) +{ + return std::find_if(m_entries.begin(), m_entries.end(), [url] (RecentFilesEntry *entry) { + return entry->url == url; + }); +} + +QString KRecentFilesMenu::group() const +{ + return d->m_group; +} + +void KRecentFilesMenu::setGroup(const QString &group) +{ + d->m_group = group; + readFromFile(); + rebuildMenu(); +} + +int KRecentFilesMenu::maximumItems() const +{ + return d->m_maximumItems; +} + +void KRecentFilesMenu::setMaximumItems(size_t maximumItems) +{ + d->m_maximumItems = maximumItems; + + // Truncate if there are more entries than the new maximum + if (d->m_entries.size() > maximumItems) { + qDeleteAll(d->m_entries.begin() + maximumItems, d->m_entries.end()); + d->m_entries.erase(d->m_entries.begin() + maximumItems, d->m_entries.end()); + rebuildMenu(); + } +}