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
41KRecentFilesAction::KRecentFilesAction(QObject *parent)
42 : KSelectAction(parent)
43 , d_ptr(new KRecentFilesActionPrivate(this))
44{
45 Q_D(KRecentFilesAction);
46 d->init();
47}
48
49KRecentFilesAction::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
60KRecentFilesAction::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
72void 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
109KRecentFilesAction::~KRecentFilesAction() = default;
110
111void 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
124void 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
131int KRecentFilesAction::maxItems() const
132{
133 Q_D(const KRecentFilesAction);
134 return d->m_maxItems;
135}
136
137void 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
156static 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
189void 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
257void 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
264QAction *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
273void 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
284QList<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
300void KRecentFilesAction::clear()
301{
302 clearEntries();
303 Q_EMIT recentListCleared();
304}
305
306void 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
317void 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
376void 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

source code of kconfigwidgets/src/krecentfilesaction.cpp