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 const int width = screen->availableGeometry().width();
31 if (width <= 0) {
32 // Ignore screens with zero width
33 // QGuiApplication::screens() sometimes returns these,
34 // such as on X11 when an xorg.conf ServerLayout refers
35 // to a Screen that is not currently connected.
36 continue;
37 }
38 maxWidthForTitles = qMin(a: maxWidthForTitles, b: width * 3 / 4);
39 }
40
41 const QFontMetrics fontMetrics = widget->fontMetrics();
42
43 QString title = displayName + QLatin1String(" [") + urlString + QLatin1Char(']');
44 const int nameWidth = fontMetrics.boundingRect(text: title).width();
45 if (nameWidth > maxWidthForTitles) {
46 // If it does not fit, try to cut only the whole path, though if the
47 // name is too long (more than 3/4 of the whole text) we cut it a bit too
48 const int displayNameMaxWidth = maxWidthForTitles * 3 / 4;
49 QString cutNameValue;
50 QString cutValue;
51 if (nameWidth > displayNameMaxWidth) {
52 cutNameValue = fontMetrics.elidedText(text: displayName, mode: Qt::ElideMiddle, width: displayNameMaxWidth);
53 cutValue = fontMetrics.elidedText(text: urlString, mode: Qt::ElideMiddle, width: maxWidthForTitles - displayNameMaxWidth);
54 } else {
55 cutNameValue = displayName;
56 cutValue = fontMetrics.elidedText(text: urlString, mode: Qt::ElideMiddle, width: maxWidthForTitles - nameWidth);
57 }
58 title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
59 }
60 return title;
61 }
62
63 explicit RecentFilesEntry(const QUrl &_url, const QString &_displayName, KRecentFilesMenu *menu)
64 : url(_url)
65 , displayName(_displayName)
66 {
67 action = new QAction(titleWithSensibleWidth(widget: menu));
68 QObject::connect(sender: action, signal: &QAction::triggered, context: action, slot: [this, menu]() {
69 Q_EMIT menu->urlTriggered(url);
70 });
71 }
72
73 ~RecentFilesEntry()
74 {
75 delete action;
76 }
77};
78
79class KRecentFilesMenuPrivate
80{
81public:
82 explicit KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr);
83
84 std::vector<RecentFilesEntry *>::iterator findEntry(const QUrl &url);
85 void recentFilesChanged() const;
86
87 KRecentFilesMenu *const q;
88 QString m_group = QStringLiteral("RecentFiles");
89 std::vector<RecentFilesEntry *> m_entries;
90 QSettings *m_settings;
91 size_t m_maximumItems = 10;
92 QAction *m_noEntriesAction;
93 QAction *m_clearAction;
94};
95
96KRecentFilesMenuPrivate::KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr)
97 : q(q_ptr)
98{
99}
100
101std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::findEntry(const QUrl &url)
102{
103 return std::find_if(first: m_entries.begin(), last: m_entries.end(), pred: [url](RecentFilesEntry *entry) {
104 return entry->url == url;
105 });
106}
107
108void KRecentFilesMenuPrivate::recentFilesChanged() const
109{
110 q->rebuildMenu();
111 Q_EMIT q->recentFilesChanged();
112}
113
114KRecentFilesMenu::KRecentFilesMenu(const QString &title, QWidget *parent)
115 : QMenu(title, parent)
116 , d(new KRecentFilesMenuPrivate(this))
117{
118 setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent")));
119 const QString fileName =
120 QStringLiteral("%1/%2_recentfiles").arg(args: QStandardPaths::writableLocation(type: QStandardPaths::AppDataLocation), args: QCoreApplication::applicationName());
121 d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this);
122
123 d->m_noEntriesAction = new QAction(tr(s: "No Entries"));
124 d->m_noEntriesAction->setDisabled(true);
125
126 d->m_clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), tr(s: "Clear List"));
127
128 readFromFile();
129}
130
131KRecentFilesMenu::KRecentFilesMenu(QWidget *parent)
132 : KRecentFilesMenu(tr(s: "Recent Files"), parent)
133{
134}
135
136KRecentFilesMenu::~KRecentFilesMenu()
137{
138 writeToFile();
139 qDeleteAll(c: d->m_entries);
140 delete d->m_clearAction;
141 delete d->m_noEntriesAction;
142}
143
144void KRecentFilesMenu::readFromFile()
145{
146 qDeleteAll(c: d->m_entries);
147 d->m_entries.clear();
148
149 d->m_settings->beginGroup(prefix: d->m_group);
150 const int size = d->m_settings->beginReadArray(QStringLiteral("files"));
151
152 d->m_entries.reserve(n: size);
153
154 for (int i = 0; i < size; ++i) {
155 d->m_settings->setArrayIndex(i);
156
157 const QUrl url = d->m_settings->value(QStringLiteral("url")).toUrl();
158 RecentFilesEntry *entry = new RecentFilesEntry(url, d->m_settings->value(QStringLiteral("displayName")).toString(), this);
159 d->m_entries.push_back(x: entry);
160 }
161
162 d->m_settings->endArray();
163 d->m_settings->endGroup();
164
165 d->recentFilesChanged();
166}
167
168void KRecentFilesMenu::addUrl(const QUrl &url, const QString &name)
169{
170 if (d->m_entries.size() == d->m_maximumItems) {
171 delete d->m_entries.back();
172 d->m_entries.pop_back();
173 }
174
175 // If it's already there remove the old one and reinsert so it appears as new
176 auto it = d->findEntry(url);
177 if (it != d->m_entries.cend()) {
178 delete *it;
179 d->m_entries.erase(position: it);
180 }
181
182 QString displayName = name;
183
184 if (displayName.isEmpty()) {
185 displayName = url.fileName();
186 }
187
188 RecentFilesEntry *entry = new RecentFilesEntry(url, displayName, this);
189 d->m_entries.insert(position: d->m_entries.begin(), x: entry);
190
191 d->recentFilesChanged();
192}
193
194void KRecentFilesMenu::removeUrl(const QUrl &url)
195{
196 auto it = d->findEntry(url);
197
198 if (it == d->m_entries.end()) {
199 return;
200 }
201
202 delete *it;
203 d->m_entries.erase(position: it);
204
205 d->recentFilesChanged();
206}
207
208void KRecentFilesMenu::rebuildMenu()
209{
210 clear();
211
212 if (d->m_entries.empty()) {
213 addAction(action: d->m_noEntriesAction);
214 return;
215 }
216
217 for (const RecentFilesEntry *entry : d->m_entries) {
218 addAction(action: entry->action);
219 }
220
221 addSeparator();
222 addAction(action: d->m_clearAction);
223
224 // reconnect d->m_clearAction, since it was disconnected in clear()
225 connect(sender: d->m_clearAction, signal: &QAction::triggered, context: this, slot: &KRecentFilesMenu::clearRecentFiles);
226}
227
228void KRecentFilesMenu::writeToFile()
229{
230 d->m_settings->remove(key: QString());
231 d->m_settings->beginGroup(prefix: d->m_group);
232 d->m_settings->beginWriteArray(QStringLiteral("files"));
233
234 int index = 0;
235 for (const RecentFilesEntry *entry : d->m_entries) {
236 d->m_settings->setArrayIndex(index);
237 d->m_settings->setValue(QStringLiteral("url"), value: entry->url);
238 d->m_settings->setValue(QStringLiteral("displayName"), value: entry->displayName);
239 ++index;
240 }
241
242 d->m_settings->endArray();
243 d->m_settings->endGroup();
244 d->m_settings->sync();
245}
246
247QString KRecentFilesMenu::group() const
248{
249 return d->m_group;
250}
251
252void KRecentFilesMenu::setGroup(const QString &group)
253{
254 d->m_group = group;
255 readFromFile();
256}
257
258int KRecentFilesMenu::maximumItems() const
259{
260 return d->m_maximumItems;
261}
262
263void KRecentFilesMenu::setMaximumItems(size_t maximumItems)
264{
265 d->m_maximumItems = maximumItems;
266
267 // Truncate if there are more entries than the new maximum
268 if (d->m_entries.size() > maximumItems) {
269 qDeleteAll(begin: d->m_entries.begin() + maximumItems, end: d->m_entries.end());
270 d->m_entries.erase(first: d->m_entries.begin() + maximumItems, last: d->m_entries.end());
271
272 d->recentFilesChanged();
273 }
274}
275
276QList<QUrl> KRecentFilesMenu::recentFiles() const
277{
278 QList<QUrl> urls;
279 urls.reserve(asize: d->m_entries.size());
280 std::transform(first: d->m_entries.cbegin(), last: d->m_entries.cend(), result: std::back_inserter(x&: urls), unary_op: [](const RecentFilesEntry *entry) {
281 return entry->url;
282 });
283
284 return urls;
285}
286
287void KRecentFilesMenu::clearRecentFiles()
288{
289 qDeleteAll(c: d->m_entries);
290 d->m_entries.clear();
291
292 d->recentFilesChanged();
293}
294
295#include "moc_krecentfilesmenu.cpp"
296

source code of kwidgetsaddons/src/krecentfilesmenu.cpp