KWidgetsAddons

krecentfilesmenu.cpp
1// This file is part of the KDE libraries
2// SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5#include "krecentfilesmenu.h"
6
7#include <QGuiApplication>
8#include <QIcon>
9#include <QScreen>
10#include <QSettings>
11#include <QStandardPaths>
12
13class RecentFilesEntry
14{
15public:
16 QUrl url;
17 QString displayName;
18 QAction *action = nullptr;
19
20 QString titleWithSensibleWidth(QWidget *widget) const
21 {
22 const QString urlString = url.toDisplayString(QUrl::PreferLocalFile);
23 // Calculate 3/4 of screen geometry, we do not want
24 // action titles to be bigger than that
25 // Since we do not know in which screen we are going to show
26 // we choose the min of all the screens
27 int maxWidthForTitles = INT_MAX;
28 const auto screens = QGuiApplication::screens();
29 for (QScreen *screen : screens) {
30 maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
31 }
32
33 const QFontMetrics fontMetrics = widget->fontMetrics();
34
35 QString title = displayName + QLatin1String(" [") + urlString + QLatin1Char(']');
36 const int nameWidth = fontMetrics.boundingRect(title).width();
37 if (nameWidth > maxWidthForTitles) {
38 // If it does not fit, try to cut only the whole path, though if the
39 // name is too long (more than 3/4 of the whole text) we cut it a bit too
40 const int displayNameMaxWidth = maxWidthForTitles * 3 / 4;
41 QString cutNameValue;
42 QString cutValue;
43 if (nameWidth > displayNameMaxWidth) {
44 cutNameValue = fontMetrics.elidedText(displayName, Qt::ElideMiddle, displayNameMaxWidth);
45 cutValue = fontMetrics.elidedText(urlString, Qt::ElideMiddle, maxWidthForTitles - displayNameMaxWidth);
46 } else {
47 cutNameValue = displayName;
48 cutValue = fontMetrics.elidedText(urlString, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
49 }
50 title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
51 }
52 return title;
53 }
54
55 explicit RecentFilesEntry(const QUrl &_url, const QString &_displayName, KRecentFilesMenu *menu)
56 : url(_url)
57 , displayName(_displayName)
58 {
59 action = new QAction(titleWithSensibleWidth(menu));
60 QObject::connect(action, &QAction::triggered, action, [this, menu]() {
61 Q_EMIT menu->urlTriggered(url);
62 });
63 }
64
65 ~RecentFilesEntry()
66 {
67 delete action;
68 }
69};
70
71class KRecentFilesMenuPrivate
72{
73public:
74 explicit KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr);
75
76 std::vector<RecentFilesEntry *>::iterator findEntry(const QUrl &url);
77 void recentFilesChanged() const;
78
79 KRecentFilesMenu *const q;
80 QString m_group = QStringLiteral("RecentFiles");
81 std::vector<RecentFilesEntry *> m_entries;
82 QSettings *m_settings;
83 size_t m_maximumItems = 10;
84 QAction *m_noEntriesAction;
85 QAction *m_clearAction;
86};
87
88KRecentFilesMenuPrivate::KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr)
89 : q(q_ptr)
90{
91}
92
93std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::findEntry(const QUrl &url)
94{
95 return std::find_if(m_entries.begin(), m_entries.end(), [url](RecentFilesEntry *entry) {
96 return entry->url == url;
97 });
98}
99
100void KRecentFilesMenuPrivate::recentFilesChanged() const
101{
102 q->rebuildMenu();
103 Q_EMIT q->recentFilesChanged();
104}
105
106KRecentFilesMenu::KRecentFilesMenu(const QString &title, QWidget *parent)
107 : QMenu(title, parent)
108 , d(new KRecentFilesMenuPrivate(this))
109{
110 setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent")));
111 const QString fileName =
113 d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this);
114
115 d->m_noEntriesAction = new QAction(tr("No Entries"));
116 d->m_noEntriesAction->setDisabled(true);
117
118 d->m_clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), tr("Clear List"));
119
120 readFromFile();
121}
122
123KRecentFilesMenu::KRecentFilesMenu(QWidget *parent)
124 : KRecentFilesMenu(tr("Recent Files"), parent)
125{
126}
127
128KRecentFilesMenu::~KRecentFilesMenu()
129{
130 writeToFile();
131 qDeleteAll(d->m_entries);
132 delete d->m_clearAction;
133 delete d->m_noEntriesAction;
134}
135
136void KRecentFilesMenu::readFromFile()
137{
138 qDeleteAll(d->m_entries);
139 d->m_entries.clear();
140
141 d->m_settings->beginGroup(d->m_group);
142 const int size = d->m_settings->beginReadArray(QStringLiteral("files"));
143
144 d->m_entries.reserve(size);
145
146 for (int i = 0; i < size; ++i) {
147 d->m_settings->setArrayIndex(i);
148
149 const QUrl url = d->m_settings->value(QStringLiteral("url")).toUrl();
150 RecentFilesEntry *entry = new RecentFilesEntry(url, d->m_settings->value(QStringLiteral("displayName")).toString(), this);
151 d->m_entries.push_back(entry);
152 }
153
154 d->m_settings->endArray();
155 d->m_settings->endGroup();
156
157 d->recentFilesChanged();
158}
159
160void KRecentFilesMenu::addUrl(const QUrl &url, const QString &name)
161{
162 if (d->m_entries.size() == d->m_maximumItems) {
163 delete d->m_entries.back();
164 d->m_entries.pop_back();
165 }
166
167 // If it's already there remove the old one and reinsert so it appears as new
168 auto it = d->findEntry(url);
169 if (it != d->m_entries.cend()) {
170 delete *it;
171 d->m_entries.erase(it);
172 }
173
174 QString displayName = name;
175
176 if (displayName.isEmpty()) {
177 displayName = url.fileName();
178 }
179
180 RecentFilesEntry *entry = new RecentFilesEntry(url, displayName, this);
181 d->m_entries.insert(d->m_entries.begin(), entry);
182
183 d->recentFilesChanged();
184}
185
187{
188 auto it = d->findEntry(url);
189
190 if (it == d->m_entries.end()) {
191 return;
192 }
193
194 delete *it;
195 d->m_entries.erase(it);
196
197 d->recentFilesChanged();
198}
199
200void KRecentFilesMenu::rebuildMenu()
201{
202 clear();
203
204 if (d->m_entries.empty()) {
205 addAction(d->m_noEntriesAction);
206 return;
207 }
208
209 for (const RecentFilesEntry *entry : d->m_entries) {
210 addAction(entry->action);
211 }
212
213 addSeparator();
214 addAction(d->m_clearAction);
215
216 // reconnect d->m_clearAction, since it was disconnected in clear()
218}
219
220void KRecentFilesMenu::writeToFile()
221{
222 d->m_settings->remove(QString());
223 d->m_settings->beginGroup(d->m_group);
224 d->m_settings->beginWriteArray(QStringLiteral("files"));
225
226 int index = 0;
227 for (const RecentFilesEntry *entry : d->m_entries) {
228 d->m_settings->setArrayIndex(index);
229 d->m_settings->setValue(QStringLiteral("url"), entry->url);
230 d->m_settings->setValue(QStringLiteral("displayName"), entry->displayName);
231 ++index;
232 }
233
234 d->m_settings->endArray();
235 d->m_settings->endGroup();
236 d->m_settings->sync();
237}
238
240{
241 return d->m_group;
242}
243
245{
246 d->m_group = group;
247 readFromFile();
248}
249
251{
252 return d->m_maximumItems;
253}
254
256{
257 d->m_maximumItems = maximumItems;
258
259 // Truncate if there are more entries than the new maximum
260 if (d->m_entries.size() > maximumItems) {
261 qDeleteAll(d->m_entries.begin() + maximumItems, d->m_entries.end());
262 d->m_entries.erase(d->m_entries.begin() + maximumItems, d->m_entries.end());
263
264 d->recentFilesChanged();
265 }
266}
267
269{
270 QList<QUrl> urls;
271 urls.reserve(d->m_entries.size());
272 std::transform(d->m_entries.cbegin(), d->m_entries.cend(), std::back_inserter(urls), [](const RecentFilesEntry *entry) {
273 return entry->url;
274 });
275
276 return urls;
277}
278
280{
281 qDeleteAll(d->m_entries);
282 d->m_entries.clear();
283
284 d->recentFilesChanged();
285}
286
287#include "moc_krecentfilesmenu.cpp"
A menu that offers a set of recent files.
void removeUrl(const QUrl &url)
Remove a URL from the recent files list.
QList< QUrl > recentFiles() const
List of URLs of recent files.
void urlTriggered(const QUrl &url)
emitted when the user clicks on a file action.
void setGroup(const QString &group)
Specify a group for storing the URLs.
void setMaximumItems(size_t maximumItems)
Set the maximum URL count.
int maximumItems() const
The maximum number of files this menu can hold.
void clearRecentFiles()
Clear recent files list.
void addUrl(const QUrl &url, const QString &name=QString())
Add URL to recent files list.
QString group() const
The group the URLs are saved to/read from.
char * toString(const EngineQuery &query)
void triggered(bool checked)
QRect boundingRect(QChar ch) const const
QString elidedText(const QString &text, Qt::TextElideMode mode, int width, int flags) const const
QList< QScreen * > screens()
QIcon fromTheme(const QString &name)
void reserve(qsizetype size)
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSeparator()
void clear()
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
int width() const const
QString writableLocation(StandardLocation type)
QString arg(Args &&... args) const const
bool isEmpty() const const
ElideMiddle
PreferLocalFile
QString fileName(ComponentFormattingOptions options) const const
QFontMetrics fontMetrics() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 12:02:04 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.