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 | |
13 | class RecentFilesEntry |
14 | { |
15 | public: |
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 (const QUrl &_url, const QString &_displayName, KRecentFilesMenu *) |
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 | |
71 | class |
72 | { |
73 | public: |
74 | explicit KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr); |
75 | |
76 | std::vector<RecentFilesEntry *>::iterator findEntry(const QUrl &url); |
77 | void recentFilesChanged() const; |
78 | |
79 | KRecentFilesMenu *const ; |
80 | QString = QStringLiteral("RecentFiles" ); |
81 | std::vector<RecentFilesEntry *> ; |
82 | QSettings *; |
83 | size_t = 10; |
84 | QAction *; |
85 | QAction *; |
86 | }; |
87 | |
88 | KRecentFilesMenuPrivate::(KRecentFilesMenu *q_ptr) |
89 | : q(q_ptr) |
90 | { |
91 | } |
92 | |
93 | std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::(const QUrl &url) |
94 | { |
95 | return std::find_if(first: m_entries.begin(), last: m_entries.end(), pred: [url](RecentFilesEntry *entry) { |
96 | return entry->url == url; |
97 | }); |
98 | } |
99 | |
100 | void KRecentFilesMenuPrivate::() const |
101 | { |
102 | q->rebuildMenu(); |
103 | Q_EMIT q->recentFilesChanged(); |
104 | } |
105 | |
106 | 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 = |
112 | QStringLiteral("%1/%2_recentfiles" ).arg(args: QStandardPaths::writableLocation(type: QStandardPaths::AppDataLocation), args: QCoreApplication::applicationName()); |
113 | d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this); |
114 | |
115 | d->m_noEntriesAction = new QAction(tr(s: "No Entries" )); |
116 | d->m_noEntriesAction->setDisabled(true); |
117 | |
118 | d->m_clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history" )), tr(s: "Clear List" )); |
119 | |
120 | readFromFile(); |
121 | } |
122 | |
123 | KRecentFilesMenu::(QWidget *parent) |
124 | : KRecentFilesMenu(tr(s: "Recent Files" ), parent) |
125 | { |
126 | } |
127 | |
128 | KRecentFilesMenu::() |
129 | { |
130 | writeToFile(); |
131 | qDeleteAll(c: d->m_entries); |
132 | delete d->m_clearAction; |
133 | delete d->m_noEntriesAction; |
134 | } |
135 | |
136 | void KRecentFilesMenu::() |
137 | { |
138 | qDeleteAll(c: d->m_entries); |
139 | d->m_entries.clear(); |
140 | |
141 | d->m_settings->beginGroup(prefix: d->m_group); |
142 | const int size = d->m_settings->beginReadArray(QStringLiteral("files" )); |
143 | |
144 | d->m_entries.reserve(n: 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(x: entry); |
152 | } |
153 | |
154 | d->m_settings->endArray(); |
155 | d->m_settings->endGroup(); |
156 | |
157 | d->recentFilesChanged(); |
158 | } |
159 | |
160 | void KRecentFilesMenu::(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(position: 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(position: d->m_entries.begin(), x: entry); |
182 | |
183 | d->recentFilesChanged(); |
184 | } |
185 | |
186 | void KRecentFilesMenu::(const QUrl &url) |
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(position: it); |
196 | |
197 | d->recentFilesChanged(); |
198 | } |
199 | |
200 | void KRecentFilesMenu::() |
201 | { |
202 | clear(); |
203 | |
204 | if (d->m_entries.empty()) { |
205 | addAction(action: d->m_noEntriesAction); |
206 | return; |
207 | } |
208 | |
209 | for (const RecentFilesEntry *entry : d->m_entries) { |
210 | addAction(action: entry->action); |
211 | } |
212 | |
213 | addSeparator(); |
214 | addAction(action: d->m_clearAction); |
215 | |
216 | // reconnect d->m_clearAction, since it was disconnected in clear() |
217 | connect(sender: d->m_clearAction, signal: &QAction::triggered, context: this, slot: &KRecentFilesMenu::clearRecentFiles); |
218 | } |
219 | |
220 | void KRecentFilesMenu::() |
221 | { |
222 | d->m_settings->remove(key: QString()); |
223 | d->m_settings->beginGroup(prefix: 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" ), value: entry->url); |
230 | d->m_settings->setValue(QStringLiteral("displayName" ), value: entry->displayName); |
231 | ++index; |
232 | } |
233 | |
234 | d->m_settings->endArray(); |
235 | d->m_settings->endGroup(); |
236 | d->m_settings->sync(); |
237 | } |
238 | |
239 | QString KRecentFilesMenu::() const |
240 | { |
241 | return d->m_group; |
242 | } |
243 | |
244 | void KRecentFilesMenu::(const QString &group) |
245 | { |
246 | d->m_group = group; |
247 | readFromFile(); |
248 | } |
249 | |
250 | int KRecentFilesMenu::() const |
251 | { |
252 | return d->m_maximumItems; |
253 | } |
254 | |
255 | void KRecentFilesMenu::(size_t maximumItems) |
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(begin: d->m_entries.begin() + maximumItems, end: d->m_entries.end()); |
262 | d->m_entries.erase(first: d->m_entries.begin() + maximumItems, last: d->m_entries.end()); |
263 | |
264 | d->recentFilesChanged(); |
265 | } |
266 | } |
267 | |
268 | QList<QUrl> KRecentFilesMenu::() const |
269 | { |
270 | QList<QUrl> urls; |
271 | urls.reserve(asize: d->m_entries.size()); |
272 | std::transform(first: d->m_entries.cbegin(), last: d->m_entries.cend(), result: std::back_inserter(x&: urls), unary_op: [](const RecentFilesEntry *entry) { |
273 | return entry->url; |
274 | }); |
275 | |
276 | return urls; |
277 | } |
278 | |
279 | void KRecentFilesMenu::() |
280 | { |
281 | qDeleteAll(c: d->m_entries); |
282 | d->m_entries.clear(); |
283 | |
284 | d->recentFilesChanged(); |
285 | } |
286 | |
287 | #include "moc_krecentfilesmenu.cpp" |
288 | |