1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org> |
4 | SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org> |
5 | SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org> |
6 | SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> |
7 | SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org> |
8 | SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org> |
9 | SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org> |
10 | SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org> |
11 | SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org> |
12 | SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org> |
13 | |
14 | SPDX-License-Identifier: LGPL-2.0-only |
15 | */ |
16 | |
17 | #include "krecentfilesaction.h" |
18 | #include "krecentfilesaction_p.h" |
19 | |
20 | #include <QActionGroup> |
21 | #include <QDir> |
22 | #include <QGuiApplication> |
23 | #include <QMenu> |
24 | #include <QMimeDatabase> |
25 | #include <QMimeType> |
26 | #include <QScreen> |
27 | |
28 | #ifdef QT_DBUS_LIB |
29 | #include <QDBusConnectionInterface> |
30 | #include <QDBusInterface> |
31 | #include <QDBusMessage> |
32 | #endif |
33 | |
34 | #include <KConfig> |
35 | #include <KConfigGroup> |
36 | #include <KLocalizedString> |
37 | #include <KShell> |
38 | |
39 | #include <set> |
40 | |
41 | KRecentFilesAction::KRecentFilesAction(QObject *parent) |
42 | : KSelectAction(parent) |
43 | , d_ptr(new KRecentFilesActionPrivate(this)) |
44 | { |
45 | Q_D(KRecentFilesAction); |
46 | d->init(); |
47 | } |
48 | |
49 | KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent) |
50 | : KSelectAction(parent) |
51 | , d_ptr(new KRecentFilesActionPrivate(this)) |
52 | { |
53 | Q_D(KRecentFilesAction); |
54 | d->init(); |
55 | |
56 | // Want to keep the ampersands |
57 | setText(text); |
58 | } |
59 | |
60 | KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent) |
61 | : KSelectAction(parent) |
62 | , d_ptr(new KRecentFilesActionPrivate(this)) |
63 | { |
64 | Q_D(KRecentFilesAction); |
65 | d->init(); |
66 | |
67 | setIcon(icon); |
68 | // Want to keep the ampersands |
69 | setText(text); |
70 | } |
71 | |
72 | void KRecentFilesActionPrivate::init() |
73 | { |
74 | Q_Q(KRecentFilesAction); |
75 | delete q->menu(); |
76 | q->setMenu(new QMenu()); |
77 | q->setToolBarMode(KSelectAction::MenuMode); |
78 | m_noEntriesAction = q->menu()->addAction(i18n("No Entries" )); |
79 | m_noEntriesAction->setObjectName(QStringLiteral("no_entries" )); |
80 | m_noEntriesAction->setEnabled(false); |
81 | clearSeparator = q->menu()->addSeparator(); |
82 | clearSeparator->setVisible(false); |
83 | clearSeparator->setObjectName(QStringLiteral("separator" )); |
84 | clearAction = q->menu()->addAction(icon: QIcon::fromTheme(QStringLiteral("edit-clear-history" )), i18n("Clear List" ), args: q, args: &KRecentFilesAction::clear); |
85 | clearAction->setObjectName(QStringLiteral("clear_action" )); |
86 | clearAction->setVisible(false); |
87 | q->setEnabled(false); |
88 | q->connect(sender: q, signal: &KSelectAction::actionTriggered, context: q, slot: [this](QAction *action) { |
89 | urlSelected(action); |
90 | }); |
91 | |
92 | q->connect(sender: q->menu(), signal: &QMenu::aboutToShow, context: q, slot: [q] { |
93 | std::vector<RecentActionInfo> &recentActions = q->d_ptr->m_recentActions; |
94 | // Set icons lazily based on the mimetype |
95 | for (auto action : recentActions) { |
96 | if (action.action->icon().isNull()) { |
97 | if (!action.mimeType.isValid()) { |
98 | action.mimeType = QMimeDatabase().mimeTypeForFile(fileName: action.url.path(), mode: QMimeDatabase::MatchExtension); |
99 | } |
100 | |
101 | if (!action.mimeType.isDefault()) { |
102 | action.action->setIcon(QIcon::fromTheme(name: action.mimeType.iconName())); |
103 | } |
104 | } |
105 | } |
106 | }); |
107 | } |
108 | |
109 | KRecentFilesAction::~KRecentFilesAction() = default; |
110 | |
111 | void KRecentFilesActionPrivate::urlSelected(QAction *action) |
112 | { |
113 | Q_Q(KRecentFilesAction); |
114 | |
115 | auto it = findByAction(action); |
116 | |
117 | Q_ASSERT(it != m_recentActions.cend()); // Should never happen |
118 | |
119 | const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules |
120 | Q_EMIT q->urlSelected(url); |
121 | } |
122 | |
123 | // TODO: remove this helper function, it will crash if you use it in a loop |
124 | void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it) |
125 | { |
126 | Q_Q(KRecentFilesAction); |
127 | delete q->KSelectAction::removeAction(action: it->action); |
128 | m_recentActions.erase(position: it); |
129 | } |
130 | |
131 | int KRecentFilesAction::maxItems() const |
132 | { |
133 | Q_D(const KRecentFilesAction); |
134 | return d->m_maxItems; |
135 | } |
136 | |
137 | void KRecentFilesAction::setMaxItems(int maxItems) |
138 | { |
139 | Q_D(KRecentFilesAction); |
140 | // set new maxItems |
141 | d->m_maxItems = std::max(a: maxItems, b: 0); |
142 | |
143 | // Remove all excess items, oldest (i.e. first added) first |
144 | const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems; |
145 | if (difference > 0) { |
146 | auto beginIt = d->m_recentActions.begin(); |
147 | auto endIt = d->m_recentActions.begin() + difference; |
148 | for (auto it = beginIt; it < endIt; ++it) { |
149 | // Remove the action from the menus, action groups ...etc |
150 | delete KSelectAction::removeAction(action: it->action); |
151 | } |
152 | d->m_recentActions.erase(first: beginIt, last: endIt); |
153 | } |
154 | } |
155 | |
156 | static QString titleWithSensibleWidth(const QString &nameValue, const QString &value) |
157 | { |
158 | // Calculate 3/4 of screen geometry, we do not want |
159 | // action titles to be bigger than that |
160 | // Since we do not know in which screen we are going to show |
161 | // we choose the min of all the screens |
162 | int maxWidthForTitles = INT_MAX; |
163 | const auto screens = QGuiApplication::screens(); |
164 | for (QScreen *screen : screens) { |
165 | maxWidthForTitles = qMin(a: maxWidthForTitles, b: screen->availableGeometry().width() * 3 / 4); |
166 | } |
167 | const QFontMetrics fontMetrics = QFontMetrics(QFont()); |
168 | |
169 | QString title = nameValue + QLatin1String(" [" ) + value + QLatin1Char(']'); |
170 | const int nameWidth = fontMetrics.boundingRect(text: title).width(); |
171 | if (nameWidth > maxWidthForTitles) { |
172 | // If it does not fit, try to cut only the whole path, though if the |
173 | // name is too long (more than 3/4 of the whole text) we cut it a bit too |
174 | const int nameValueMaxWidth = maxWidthForTitles * 3 / 4; |
175 | QString cutNameValue; |
176 | QString cutValue; |
177 | if (nameWidth > nameValueMaxWidth) { |
178 | cutNameValue = fontMetrics.elidedText(text: nameValue, mode: Qt::ElideMiddle, width: nameValueMaxWidth); |
179 | cutValue = fontMetrics.elidedText(text: value, mode: Qt::ElideMiddle, width: maxWidthForTitles - nameValueMaxWidth); |
180 | } else { |
181 | cutNameValue = nameValue; |
182 | cutValue = fontMetrics.elidedText(text: value, mode: Qt::ElideMiddle, width: maxWidthForTitles - nameWidth); |
183 | } |
184 | title = cutNameValue + QLatin1String(" [" ) + cutValue + QLatin1Char(']'); |
185 | } |
186 | return title; |
187 | } |
188 | |
189 | void KRecentFilesAction::addUrl(const QUrl &url, const QString &name) |
190 | { |
191 | Q_D(KRecentFilesAction); |
192 | |
193 | // ensure we never add items if we want none |
194 | if (d->m_maxItems == 0) { |
195 | return; |
196 | } |
197 | |
198 | if (url.isLocalFile() && url.toLocalFile().startsWith(s: QDir::tempPath())) { |
199 | return; |
200 | } |
201 | |
202 | // Remove url if it already exists in the list |
203 | removeUrl(url); |
204 | |
205 | // Remove oldest item if already maxItems in list |
206 | Q_ASSERT(d->m_maxItems > 0); |
207 | if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) { |
208 | d->removeAction(it: d->m_recentActions.begin()); |
209 | } |
210 | |
211 | const QString pathOrUrl(url.toDisplayString(options: QUrl::PreferLocalFile)); |
212 | const QString tmpName = !name.isEmpty() ? name : url.fileName(); |
213 | #ifdef Q_OS_WIN |
214 | const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl; |
215 | #else |
216 | const QString file = pathOrUrl; |
217 | #endif |
218 | |
219 | d->m_noEntriesAction->setVisible(false); |
220 | d->clearSeparator->setVisible(true); |
221 | d->clearAction->setVisible(true); |
222 | setEnabled(true); |
223 | // add file to list |
224 | const QString title = titleWithSensibleWidth(nameValue: tmpName, value: KShell::tildeCollapse(path: file)); |
225 | |
226 | #ifdef QT_DBUS_LIB |
227 | static bool isKdeSession = qgetenv(varName: "XDG_CURRENT_DESKTOP" ) == "KDE" ; |
228 | if (isKdeSession) { |
229 | const QDBusConnection bus = QDBusConnection::sessionBus(); |
230 | if (bus.isConnected() && bus.interface()->isServiceRegistered(QStringLiteral("org.kde.ActivityManager" ))) { |
231 | const static QString activityService = QStringLiteral("org.kde.ActivityManager" ); |
232 | const static QString activityResources = QStringLiteral("/ActivityManager/Resources" ); |
233 | const static QString activityResouceInferface = QStringLiteral("org.kde.ActivityManager.Resources" ); |
234 | const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(fileName: url.path(), mode: QMimeDatabase::MatchExtension); |
235 | |
236 | const auto urlString = url.toString(options: QUrl::PreferLocalFile); |
237 | QDBusMessage message = |
238 | QDBusMessage::createMethodCall(destination: activityService, path: activityResources, interface: activityResouceInferface, QStringLiteral("RegisterResourceEvent" )); |
239 | message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */}); |
240 | bus.asyncCall(message); |
241 | |
242 | message = QDBusMessage::createMethodCall(destination: activityService, path: activityResources, interface: activityResouceInferface, QStringLiteral("RegisterResourceMimetype" )); |
243 | message.setArguments({urlString, mimeType.name()}); |
244 | bus.asyncCall(message); |
245 | |
246 | message = QDBusMessage::createMethodCall(destination: activityService, path: activityResources, interface: activityResouceInferface, QStringLiteral("RegisterResourceTitle" )); |
247 | message.setArguments({urlString, url.fileName()}); |
248 | bus.asyncCall(message); |
249 | } |
250 | } |
251 | #endif |
252 | |
253 | QAction *action = new QAction(title, selectableActionGroup()); |
254 | addAction(action, url, name: tmpName, mimeType: QMimeType()); |
255 | } |
256 | |
257 | void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType) |
258 | { |
259 | Q_D(KRecentFilesAction); |
260 | menu()->insertAction(before: menu()->actions().value(i: 0), action); |
261 | d->m_recentActions.push_back(x: {.action: action, .url: url, .shortName: name, .mimeType: mimeType}); |
262 | } |
263 | |
264 | QAction *KRecentFilesAction::removeAction(QAction *action) |
265 | { |
266 | Q_D(KRecentFilesAction); |
267 | auto it = d->findByAction(action); |
268 | Q_ASSERT(it != d->m_recentActions.cend()); |
269 | d->m_recentActions.erase(position: it); |
270 | return KSelectAction::removeAction(action); |
271 | } |
272 | |
273 | void KRecentFilesAction::removeUrl(const QUrl &url) |
274 | { |
275 | Q_D(KRecentFilesAction); |
276 | |
277 | auto it = d->findByUrl(url); |
278 | |
279 | if (it != d->m_recentActions.cend()) { |
280 | d->removeAction(it); |
281 | }; |
282 | } |
283 | |
284 | QList<QUrl> KRecentFilesAction::urls() const |
285 | { |
286 | Q_D(const KRecentFilesAction); |
287 | |
288 | QList<QUrl> list; |
289 | list.reserve(asize: d->m_recentActions.size()); |
290 | |
291 | using Info = KRecentFilesActionPrivate::RecentActionInfo; |
292 | // Reverse order to match how the actions appear in the menu |
293 | std::transform(first: d->m_recentActions.crbegin(), last: d->m_recentActions.crend(), result: std::back_inserter(x&: list), unary_op: [](const Info &info) { |
294 | return info.url; |
295 | }); |
296 | |
297 | return list; |
298 | } |
299 | |
300 | void KRecentFilesAction::clear() |
301 | { |
302 | clearEntries(); |
303 | Q_EMIT recentListCleared(); |
304 | } |
305 | |
306 | void KRecentFilesAction::clearEntries() |
307 | { |
308 | Q_D(KRecentFilesAction); |
309 | KSelectAction::clear(); |
310 | d->m_recentActions.clear(); |
311 | d->m_noEntriesAction->setVisible(true); |
312 | d->clearSeparator->setVisible(false); |
313 | d->clearAction->setVisible(false); |
314 | setEnabled(false); |
315 | } |
316 | |
317 | void KRecentFilesAction::loadEntries(const KConfigGroup &_config) |
318 | { |
319 | Q_D(KRecentFilesAction); |
320 | clearEntries(); |
321 | |
322 | QString key; |
323 | QString value; |
324 | QString nameKey; |
325 | QString nameValue; |
326 | QString title; |
327 | QUrl url; |
328 | |
329 | KConfigGroup cg = _config; |
330 | // "<default>" means the group was constructed with an empty name |
331 | if (cg.name() == QLatin1String("<default>" )) { |
332 | cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles" )); |
333 | } |
334 | |
335 | std::set<QUrl> seenUrls; |
336 | |
337 | bool thereAreEntries = false; |
338 | // read file list |
339 | for (int i = 1; i <= d->m_maxItems; i++) { |
340 | key = QStringLiteral("File%1" ).arg(a: i); |
341 | value = cg.readPathEntry(pKey: key, aDefault: QString()); |
342 | if (value.isEmpty()) { |
343 | continue; |
344 | } |
345 | url = QUrl::fromUserInput(userInput: value); |
346 | |
347 | auto [it, isNewUrl] = seenUrls.insert(x: url); |
348 | // Don't restore if this url has already been restored (e.g. broken config) |
349 | if (!isNewUrl) { |
350 | continue; |
351 | } |
352 | |
353 | #ifdef Q_OS_WIN |
354 | // convert to backslashes |
355 | if (url.isLocalFile()) { |
356 | value = QDir::toNativeSeparators(value); |
357 | } |
358 | #endif |
359 | |
360 | nameKey = QStringLiteral("Name%1" ).arg(a: i); |
361 | nameValue = cg.readPathEntry(pKey: nameKey, aDefault: url.fileName()); |
362 | title = titleWithSensibleWidth(nameValue, value: KShell::tildeCollapse(path: value)); |
363 | if (!value.isNull()) { |
364 | thereAreEntries = true; |
365 | addAction(action: new QAction(title, selectableActionGroup()), url, name: nameValue); |
366 | } |
367 | } |
368 | if (thereAreEntries) { |
369 | d->m_noEntriesAction->setVisible(false); |
370 | d->clearSeparator->setVisible(true); |
371 | d->clearAction->setVisible(true); |
372 | setEnabled(true); |
373 | } |
374 | } |
375 | |
376 | void KRecentFilesAction::saveEntries(const KConfigGroup &_cg) |
377 | { |
378 | Q_D(KRecentFilesAction); |
379 | |
380 | KConfigGroup cg = _cg; |
381 | // "<default>" means the group was constructed with an empty name |
382 | if (cg.name() == QLatin1String("<default>" )) { |
383 | cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles" )); |
384 | } |
385 | |
386 | cg.deleteGroup(); |
387 | |
388 | // write file list |
389 | int i = 1; |
390 | for (const auto &[action, url, shortName, _] : d->m_recentActions) { |
391 | cg.writePathEntry(QStringLiteral("File%1" ).arg(a: i), path: url.toDisplayString(options: QUrl::PreferLocalFile)); |
392 | cg.writePathEntry(QStringLiteral("Name%1" ).arg(a: i), path: shortName); |
393 | |
394 | ++i; |
395 | } |
396 | } |
397 | |
398 | #include "moc_krecentfilesaction.cpp" |
399 | |