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(options: 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(a: maxWidthForTitles, b: 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(text: 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(text: displayName, mode: Qt::ElideMiddle, width: displayNameMaxWidth);
45 cutValue = fontMetrics.elidedText(text: urlString, mode: Qt::ElideMiddle, width: maxWidthForTitles - displayNameMaxWidth);
46 } else {
47 cutNameValue = displayName;
48 cutValue = fontMetrics.elidedText(text: urlString, mode: Qt::ElideMiddle, width: 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(widget: menu));
60 QObject::connect(sender: action, signal: &QAction::triggered, context: action, slot: [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
92std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::findEntry(const QUrl &url)
93{
94 return std::find_if(first: m_entries.begin(), last: m_entries.end(), pred: [url](RecentFilesEntry *entry) {
95 return entry->url == url;
96 });
97}
98
99void KRecentFilesMenuPrivate::recentFilesChanged() const
100{
101 q->rebuildMenu();
102 Q_EMIT q->recentFilesChanged();
103}
104
105KRecentFilesMenu::KRecentFilesMenu(const QString &title, QWidget *parent)
106 : QMenu(title, parent)
107 , d(new KRecentFilesMenuPrivate(this))
108{
109 setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent")));
110 const QString fileName =
111 QStringLiteral("%1/%2_recentfiles").arg(args: QStandardPaths::writableLocation(type: QStandardPaths::AppDataLocation), args: QCoreApplication::applicationName());
112 d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this);
113
114 d->m_noEntriesAction = new QAction(tr(s: "No Entries"));
115 d->m_noEntriesAction->setDisabled(true);
116
117 d->m_clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), tr(s: "Clear List"));
118
119 readFromFile();
120}
121
122KRecentFilesMenu::KRecentFilesMenu(QWidget *parent)
123 : KRecentFilesMenu(tr(s: "Recent Files"), parent)
124{
125}
126
127KRecentFilesMenu::~KRecentFilesMenu()
128{
129 writeToFile();
130 qDeleteAll(c: d->m_entries);
131 delete d->m_clearAction;
132 delete d->m_noEntriesAction;
133}
134
135void KRecentFilesMenu::readFromFile()
136{
137 qDeleteAll(c: d->m_entries);
138 d->m_entries.clear();
139
140 d->m_settings->beginGroup(prefix: d->m_group);
141 const int size = d->m_settings->beginReadArray(QStringLiteral("files"));
142
143 d->m_entries.reserve(n: size);
144
145 for (int i = 0; i < size; ++i) {
146 d->m_settings->setArrayIndex(i);
147
148 const QUrl url = d->m_settings->value(QStringLiteral("url")).toUrl();
149 RecentFilesEntry *entry = new RecentFilesEntry(url, d->m_settings->value(QStringLiteral("displayName")).toString(), this);
150 d->m_entries.push_back(x: entry);
151 }
152
153 d->m_settings->endArray();
154 d->m_settings->endGroup();
155
156 d->recentFilesChanged();
157}
158
159void KRecentFilesMenu::addUrl(const QUrl &url, const QString &name)
160{
161 if (d->m_entries.size() == d->m_maximumItems) {
162 delete d->m_entries.back();
163 d->m_entries.pop_back();
164 }
165
166 // If it's already there remove the old one and reinsert so it appears as new
167 auto it = d->findEntry(url);
168 if (it != d->m_entries.cend()) {
169 delete *it;
170 d->m_entries.erase(position: it);
171 }
172
173 QString displayName = name;
174
175 if (displayName.isEmpty()) {
176 displayName = url.fileName();
177 }
178
179 RecentFilesEntry *entry = new RecentFilesEntry(url, displayName, this);
180 d->m_entries.insert(position: d->m_entries.begin(), x: entry);
181
182 d->recentFilesChanged();
183}
184
185void KRecentFilesMenu::removeUrl(const QUrl &url)
186{
187 auto it = d->findEntry(url);
188
189 if (it == d->m_entries.end()) {
190 return;
191 }
192
193 delete *it;
194 d->m_entries.erase(position: it);
195
196 d->recentFilesChanged();
197}
198
199void KRecentFilesMenu::rebuildMenu()
200{
201 clear();
202
203 if (d->m_entries.empty()) {
204 addAction(action: d->m_noEntriesAction);
205 return;
206 }
207
208 for (const RecentFilesEntry *entry : d->m_entries) {
209 addAction(action: entry->action);
210 }
211
212 addSeparator();
213 addAction(action: d->m_clearAction);
214
215 // reconnect d->m_clearAction, since it was disconnected in clear()
216 connect(sender: d->m_clearAction, signal: &QAction::triggered, context: this, slot: &KRecentFilesMenu::clearRecentFiles);
217}
218
219void KRecentFilesMenu::writeToFile()
220{
221 d->m_settings->remove(key: QString());
222 d->m_settings->beginGroup(prefix: d->m_group);
223 d->m_settings->beginWriteArray(QStringLiteral("files"));
224
225 int index = 0;
226 for (const RecentFilesEntry *entry : d->m_entries) {
227 d->m_settings->setArrayIndex(index);
228 d->m_settings->setValue(QStringLiteral("url"), value: entry->url);
229 d->m_settings->setValue(QStringLiteral("displayName"), value: entry->displayName);
230 ++index;
231 }
232
233 d->m_settings->endArray();
234 d->m_settings->endGroup();
235 d->m_settings->sync();
236}
237
238QString KRecentFilesMenu::group() const
239{
240 return d->m_group;
241}
242
243void KRecentFilesMenu::setGroup(const QString &group)
244{
245 d->m_group = group;
246 readFromFile();
247}
248
249int KRecentFilesMenu::maximumItems() const
250{
251 return d->m_maximumItems;
252}
253
254void KRecentFilesMenu::setMaximumItems(size_t maximumItems)
255{
256 d->m_maximumItems = maximumItems;
257
258 // Truncate if there are more entries than the new maximum
259 if (d->m_entries.size() > maximumItems) {
260 qDeleteAll(begin: d->m_entries.begin() + maximumItems, end: d->m_entries.end());
261 d->m_entries.erase(first: d->m_entries.begin() + maximumItems, last: d->m_entries.end());
262
263 d->recentFilesChanged();
264 }
265}
266
267QList<QUrl> KRecentFilesMenu::recentFiles() const
268{
269 QList<QUrl> urls;
270 urls.reserve(asize: d->m_entries.size());
271 std::transform(first: d->m_entries.cbegin(), last: d->m_entries.cend(), result: std::back_inserter(x&: urls), unary_op: [](const RecentFilesEntry *entry) {
272 return entry->url;
273 });
274
275 return urls;
276}
277
278void KRecentFilesMenu::clearRecentFiles()
279{
280 qDeleteAll(c: d->m_entries);
281 d->m_entries.clear();
282
283 d->recentFilesChanged();
284}
285
286#include "moc_krecentfilesmenu.cpp"
287

source code of kwidgetsaddons/src/krecentfilesmenu.cpp