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 | std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::(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 | |
99 | void KRecentFilesMenuPrivate::() const |
100 | { |
101 | q->rebuildMenu(); |
102 | Q_EMIT q->recentFilesChanged(); |
103 | } |
104 | |
105 | 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 | |
122 | KRecentFilesMenu::(QWidget *parent) |
123 | : KRecentFilesMenu(tr(s: "Recent Files" ), parent) |
124 | { |
125 | } |
126 | |
127 | KRecentFilesMenu::() |
128 | { |
129 | writeToFile(); |
130 | qDeleteAll(c: d->m_entries); |
131 | delete d->m_clearAction; |
132 | delete d->m_noEntriesAction; |
133 | } |
134 | |
135 | void KRecentFilesMenu::() |
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 | |
159 | void KRecentFilesMenu::(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 | |
185 | void KRecentFilesMenu::(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 | |
199 | void KRecentFilesMenu::() |
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 | |
219 | void KRecentFilesMenu::() |
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 | |
238 | QString KRecentFilesMenu::() const |
239 | { |
240 | return d->m_group; |
241 | } |
242 | |
243 | void KRecentFilesMenu::(const QString &group) |
244 | { |
245 | d->m_group = group; |
246 | readFromFile(); |
247 | } |
248 | |
249 | int KRecentFilesMenu::() const |
250 | { |
251 | return d->m_maximumItems; |
252 | } |
253 | |
254 | void KRecentFilesMenu::(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 | |
267 | QList<QUrl> KRecentFilesMenu::() 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 | |
278 | void KRecentFilesMenu::() |
279 | { |
280 | qDeleteAll(c: d->m_entries); |
281 | d->m_entries.clear(); |
282 | |
283 | d->recentFilesChanged(); |
284 | } |
285 | |
286 | #include "moc_krecentfilesmenu.cpp" |
287 | |