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 | #if HAVE_QTDBUS |
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 | addUrl(url, name, mimeType: QString()); |
192 | } |
193 | |
194 | void KRecentFilesAction::addUrl(const QUrl &url, const QString &name, const QString &mimeTypeStr) |
195 | { |
196 | Q_D(KRecentFilesAction); |
197 | |
198 | // ensure we never add items if we want none |
199 | if (d->m_maxItems == 0) { |
200 | return; |
201 | } |
202 | |
203 | if (url.isLocalFile() && url.toLocalFile().startsWith(s: QDir::tempPath())) { |
204 | return; |
205 | } |
206 | |
207 | // Remove url if it already exists in the list |
208 | removeUrl(url); |
209 | |
210 | // Remove oldest item if already maxItems in list |
211 | Q_ASSERT(d->m_maxItems > 0); |
212 | if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) { |
213 | d->removeAction(it: d->m_recentActions.begin()); |
214 | } |
215 | |
216 | const QString pathOrUrl(url.toDisplayString(options: QUrl::PreferLocalFile)); |
217 | const QString tmpName = !name.isEmpty() ? name : url.fileName(); |
218 | #ifdef Q_OS_WIN |
219 | const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl; |
220 | #else |
221 | const QString file = pathOrUrl; |
222 | #endif |
223 | |
224 | d->m_noEntriesAction->setVisible(false); |
225 | d->clearSeparator->setVisible(true); |
226 | d->clearAction->setVisible(true); |
227 | setEnabled(true); |
228 | // add file to list |
229 | const QString title = titleWithSensibleWidth(nameValue: tmpName, value: KShell::tildeCollapse(path: file)); |
230 | |
231 | #if HAVE_QTDBUS |
232 | static bool isKdeSession = qgetenv(varName: "XDG_CURRENT_DESKTOP" ) == "KDE" ; |
233 | if (isKdeSession) { |
234 | const QDBusConnection bus = QDBusConnection::sessionBus(); |
235 | if (bus.isConnected() |
236 | && bus.interface()->isServiceRegistered(QStringLiteral("org.kde.ActivityManager" )) |
237 | // skip files in hidden directories |
238 | && !url.path().contains(QStringLiteral("/." ))) { |
239 | const static QString activityService = QStringLiteral("org.kde.ActivityManager" ); |
240 | const static QString activityResources = QStringLiteral("/ActivityManager/Resources" ); |
241 | const static QString activityResouceInferface = QStringLiteral("org.kde.ActivityManager.Resources" ); |
242 | |
243 | QString mimeTypeName = mimeTypeStr; |
244 | if (mimeTypeName.isEmpty()) { |
245 | mimeTypeName = QMimeDatabase().mimeTypeForFile(fileName: url.path(), mode: QMimeDatabase::MatchExtension).name(); |
246 | } |
247 | |
248 | const auto urlString = url.toString(options: QUrl::PreferLocalFile); |
249 | QDBusMessage message = |
250 | QDBusMessage::createMethodCall(destination: activityService, path: activityResources, interface: activityResouceInferface, QStringLiteral("RegisterResourceEvent" )); |
251 | message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */}); |
252 | bus.asyncCall(message); |
253 | |
254 | message = QDBusMessage::createMethodCall(destination: activityService, path: activityResources, interface: activityResouceInferface, QStringLiteral("RegisterResourceMimetype" )); |
255 | message.setArguments({urlString, mimeTypeName}); |
256 | bus.asyncCall(message); |
257 | |
258 | message = QDBusMessage::createMethodCall(destination: activityService, path: activityResources, interface: activityResouceInferface, QStringLiteral("RegisterResourceTitle" )); |
259 | message.setArguments({urlString, url.fileName()}); |
260 | bus.asyncCall(message); |
261 | } |
262 | } |
263 | #endif |
264 | |
265 | QAction *action = new QAction(title, selectableActionGroup()); |
266 | addAction(action, url, name: tmpName, mimeType: QMimeType()); |
267 | } |
268 | |
269 | void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType) |
270 | { |
271 | Q_D(KRecentFilesAction); |
272 | menu()->insertAction(before: menu()->actions().value(i: 0), action); |
273 | d->m_recentActions.push_back(x: {.action: action, .url: url, .shortName: name, .mimeType: mimeType}); |
274 | } |
275 | |
276 | QAction *KRecentFilesAction::removeAction(QAction *action) |
277 | { |
278 | Q_D(KRecentFilesAction); |
279 | auto it = d->findByAction(action); |
280 | Q_ASSERT(it != d->m_recentActions.cend()); |
281 | d->m_recentActions.erase(position: it); |
282 | return KSelectAction::removeAction(action); |
283 | } |
284 | |
285 | void KRecentFilesAction::removeUrl(const QUrl &url) |
286 | { |
287 | Q_D(KRecentFilesAction); |
288 | |
289 | auto it = d->findByUrl(url); |
290 | |
291 | if (it != d->m_recentActions.cend()) { |
292 | d->removeAction(it); |
293 | }; |
294 | } |
295 | |
296 | QList<QUrl> KRecentFilesAction::urls() const |
297 | { |
298 | Q_D(const KRecentFilesAction); |
299 | |
300 | QList<QUrl> list; |
301 | list.reserve(asize: d->m_recentActions.size()); |
302 | |
303 | using Info = KRecentFilesActionPrivate::RecentActionInfo; |
304 | // Reverse order to match how the actions appear in the menu |
305 | std::transform(first: d->m_recentActions.crbegin(), last: d->m_recentActions.crend(), result: std::back_inserter(x&: list), unary_op: [](const Info &info) { |
306 | return info.url; |
307 | }); |
308 | |
309 | return list; |
310 | } |
311 | |
312 | void KRecentFilesAction::clear() |
313 | { |
314 | clearEntries(); |
315 | Q_EMIT recentListCleared(); |
316 | } |
317 | |
318 | void KRecentFilesAction::clearEntries() |
319 | { |
320 | Q_D(KRecentFilesAction); |
321 | KSelectAction::clear(); |
322 | d->m_recentActions.clear(); |
323 | d->m_noEntriesAction->setVisible(true); |
324 | d->clearSeparator->setVisible(false); |
325 | d->clearAction->setVisible(false); |
326 | setEnabled(false); |
327 | } |
328 | |
329 | void KRecentFilesAction::loadEntries(const KConfigGroup &_config) |
330 | { |
331 | Q_D(KRecentFilesAction); |
332 | clearEntries(); |
333 | |
334 | QString key; |
335 | QString value; |
336 | QString nameKey; |
337 | QString nameValue; |
338 | QString title; |
339 | QUrl url; |
340 | |
341 | KConfigGroup cg = _config; |
342 | // "<default>" means the group was constructed with an empty name |
343 | if (cg.name() == QLatin1String("<default>" )) { |
344 | cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles" )); |
345 | } |
346 | |
347 | std::set<QUrl> seenUrls; |
348 | |
349 | bool thereAreEntries = false; |
350 | // read file list |
351 | for (int i = 1; i <= d->m_maxItems; i++) { |
352 | key = QStringLiteral("File%1" ).arg(a: i); |
353 | value = cg.readPathEntry(pKey: key, aDefault: QString()); |
354 | if (value.isEmpty()) { |
355 | continue; |
356 | } |
357 | url = QUrl::fromUserInput(userInput: value); |
358 | |
359 | auto [it, isNewUrl] = seenUrls.insert(x: url); |
360 | // Don't restore if this url has already been restored (e.g. broken config) |
361 | if (!isNewUrl) { |
362 | continue; |
363 | } |
364 | |
365 | #ifdef Q_OS_WIN |
366 | // convert to backslashes |
367 | if (url.isLocalFile()) { |
368 | value = QDir::toNativeSeparators(value); |
369 | } |
370 | #endif |
371 | |
372 | nameKey = QStringLiteral("Name%1" ).arg(a: i); |
373 | nameValue = cg.readPathEntry(pKey: nameKey, aDefault: url.fileName()); |
374 | title = titleWithSensibleWidth(nameValue, value: KShell::tildeCollapse(path: value)); |
375 | if (!value.isNull()) { |
376 | thereAreEntries = true; |
377 | addAction(action: new QAction(title, selectableActionGroup()), url, name: nameValue); |
378 | } |
379 | } |
380 | if (thereAreEntries) { |
381 | d->m_noEntriesAction->setVisible(false); |
382 | d->clearSeparator->setVisible(true); |
383 | d->clearAction->setVisible(true); |
384 | setEnabled(true); |
385 | } |
386 | } |
387 | |
388 | void KRecentFilesAction::saveEntries(const KConfigGroup &_cg) |
389 | { |
390 | Q_D(KRecentFilesAction); |
391 | |
392 | KConfigGroup cg = _cg; |
393 | // "<default>" means the group was constructed with an empty name |
394 | if (cg.name() == QLatin1String("<default>" )) { |
395 | cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles" )); |
396 | } |
397 | |
398 | cg.deleteGroup(); |
399 | |
400 | // write file list |
401 | int i = 1; |
402 | for (const auto &[action, url, shortName, _] : d->m_recentActions) { |
403 | cg.writePathEntry(QStringLiteral("File%1" ).arg(a: i), path: url.toDisplayString(options: QUrl::PreferLocalFile)); |
404 | cg.writePathEntry(QStringLiteral("Name%1" ).arg(a: i), path: shortName); |
405 | |
406 | ++i; |
407 | } |
408 | } |
409 | |
410 | #include "moc_krecentfilesaction.cpp" |
411 | |