diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,7 @@ kfontrequester.cpp kpassworddialog.cpp kruler.cpp + krecentfilesmenu.cpp kselector.cpp kxyselector.cpp kseparator.cpp @@ -151,6 +152,7 @@ KFontRequester KPasswordDialog KRuler + KRecentFilesMenu KSelector,KGradientSelector KTitleWidget KXYSelector diff --git a/src/krecentfilesmenu.h b/src/krecentfilesmenu.h new file mode 100644 --- /dev/null +++ b/src/krecentfilesmenu.h @@ -0,0 +1,109 @@ +/* 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 + +#include + +class KRecentFilesMenuPrivate; + +/** + * A menu that offers a set of recent files. + * + * Replaces KRecentFilesAction from KConfigWidgets. + * + * @since 5.67 + * + */ +class KWIDGETSADDONS_EXPORT KRecentFilesMenu : public QMenu +{ + Q_OBJECT +public: + KRecentFilesMenu(const QString &title, QWidget *parent = nullptr); + KRecentFilesMenu(QWidget *parent = nullptr); + virtual ~KRecentFilesMenu(); + + /** + * 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 an 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 manimum url count is reached and a new URL is added the + * oldest will be replaced. + */ + int maximumItems() const; + + /** + * Set the maximum URL count. + * + * See \ref maximumItems + */ + void setMaximumItems(int 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 init(); + void readFromFile(); + void rebuildMenu(); + std::list>::const_iterator findUrl(const QUrl url); + + std::unique_ptr const d; +}; + +#endif diff --git a/src/krecentfilesmenu.cpp b/src/krecentfilesmenu.cpp new file mode 100644 --- /dev/null +++ b/src/krecentfilesmenu.cpp @@ -0,0 +1,216 @@ +/* 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 KRecentFilesMenuPrivate { +public: + QString m_group = QStringLiteral("RecentFiles"); + std::list> m_urls; + QSettings *m_settings; + size_t m_maximumItems = 10; +}; + +KRecentFilesMenu::KRecentFilesMenu(const QString& title, QWidget* parent) + : QMenu(title, parent) + , d(new KRecentFilesMenuPrivate) +{ + init(); +} + +KRecentFilesMenu::KRecentFilesMenu(QWidget* parent) + : QMenu(tr("Recent Files"), parent) + , d(new KRecentFilesMenuPrivate) +{ + init(); +} + +KRecentFilesMenu::~KRecentFilesMenu() = default; + +void KRecentFilesMenu::init() +{ + setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent"))); + const QString fileName = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/") + QCoreApplication::applicationName() + QLatin1String("staterc"); + d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this); + readFromFile(); + rebuildMenu(); +} + +void KRecentFilesMenu::readFromFile() +{ + d->m_settings->beginGroup(d->m_group); + int size = d->m_settings->beginReadArray(QStringLiteral("files")); + + 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; + } + + d->m_urls.push_back(qMakePair(url, d->m_settings->value(QStringLiteral("displayName")).toString())); + } + + d->m_settings->endArray(); + d->m_settings->endGroup(); +} + +void KRecentFilesMenu::addUrl(const QUrl url, const QString& name) +{ + if (d->m_urls.size() == d->m_maximumItems) { + d->m_urls.pop_back(); + } + + // If it's already there remove the old one and reinsert so it appears as new + auto it = findUrl(url); + if (it != d->m_urls.cend()) { + d->m_urls.erase(it); + } + + QString displayName = name; + + if (displayName.isEmpty()) { + displayName = url.fileName(); + } + + d->m_urls.push_front(qMakePair(url, displayName)); + rebuildMenu(); +} + +void KRecentFilesMenu::removeUrl(const QUrl& url) +{ + auto it = findUrl(url); + + if (it == d->m_urls.end()) { + return; + } + + d->m_urls.erase(it); + rebuildMenu(); +} + +static QString titleWithSensibleWidth(const QString &nameValue, const QString &value) +{ + // 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 = QFontMetrics(QFont()); + + QString title = nameValue + QLatin1String(" [") + value + 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 nameValueMaxWidth = maxWidthForTitles * 3 / 4; + QString cutNameValue, cutValue; + if (nameWidth > nameValueMaxWidth) { + cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth); + cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth); + } else { + cutNameValue = nameValue; + cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth); + } + title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']'); + } + return title; +} + +void KRecentFilesMenu::rebuildMenu() +{ + clear(); + d->m_settings->remove(QString()); + + if (d->m_urls.empty()) { + QAction *noEntryAction = addAction(tr("No Entries")); + noEntryAction->setDisabled(true); + return; + } + + d->m_settings->beginGroup(d->m_group); + d->m_settings->beginWriteArray(QStringLiteral("files")); + + int index = 0; + for (const QPair &urlPair : d->m_urls) { + const QAction *action = addAction(titleWithSensibleWidth(urlPair.second, urlPair.first.toDisplayString(QUrl::PreferLocalFile))); + const QUrl &url = urlPair.first; + connect(action, &QAction::triggered, this, [this, &url]() { + Q_EMIT urlTriggered(url); + }); + + d->m_settings->setArrayIndex(index); + d->m_settings->setValue(QStringLiteral("url"), url); + d->m_settings->setValue(QStringLiteral("displayName"), urlPair.second); + + ++index; + } + + d->m_settings->endArray(); + d->m_settings->endGroup(); + d->m_settings->sync(); + + addSeparator(); + + QAction *clearAction = addAction(tr("Clear List")); + connect(clearAction, &QAction::triggered, this, [this]{ + d->m_urls.clear(); + rebuildMenu(); + }); +} + +std::list >::const_iterator KRecentFilesMenu::findUrl(const QUrl url) +{ + return std::find_if(d->m_urls.cbegin(), d->m_urls.cend(), [url] (const QPair &pair) { + return pair.first == 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(int maximumItems) +{ + d->m_maximumItems = maximumItems; +}