KConfigWidgets

krecentfilesaction.cpp
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
42 : KSelectAction(parent)
43 , d_ptr(new KRecentFilesActionPrivate(this))
44{
46 d->init();
47}
48
50 : KSelectAction(parent)
51 , d_ptr(new KRecentFilesActionPrivate(this))
52{
54 d->init();
55
56 // Want to keep the ampersands
58}
59
61 : KSelectAction(parent)
62 , d_ptr(new KRecentFilesActionPrivate(this))
63{
65 d->init();
66
68 // Want to keep the ampersands
70}
71
72void KRecentFilesActionPrivate::init()
73{
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(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear);
85 clearAction->setObjectName(QStringLiteral("clear_action"));
86 clearAction->setVisible(false);
87 q->setEnabled(false);
88 q->connect(q, &KSelectAction::actionTriggered, q, [this](QAction *action) {
89 urlSelected(action);
90 });
91
92 q->connect(q->menu(), &QMenu::aboutToShow, q, [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(action.url.path(), QMimeDatabase::MatchExtension);
99 }
100
101 if (!action.mimeType.isDefault()) {
102 action.action->setIcon(QIcon::fromTheme(action.mimeType.iconName()));
103 }
104 }
105 }
106 });
107}
108
110
111void KRecentFilesActionPrivate::urlSelected(QAction *action)
112{
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{
127 delete q->KSelectAction::removeAction(it->action);
128 m_recentActions.erase(it);
129}
130
131int KRecentFilesAction::maxItems() const
132{
133 Q_D(const KRecentFilesAction);
134 return d->m_maxItems;
135}
136
138{
140 // set new maxItems
141 d->m_maxItems = std::max(maxItems, 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(it->action);
151 }
152 d->m_recentActions.erase(beginIt, 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(maxWidthForTitles, 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(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(nameValue, Qt::ElideMiddle, nameValueMaxWidth);
179 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth);
180 } else {
181 cutNameValue = nameValue;
182 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, 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{
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(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(d->m_recentActions.begin());
209 }
210
211 const QString pathOrUrl(url.toDisplayString(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(tmpName, KShell::tildeCollapse(file));
225
226#if HAVE_QTDBUS
227 static bool isKdeSession = qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
228 if (isKdeSession) {
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");
235
236 const auto urlString = url.toString(QUrl::PreferLocalFile);
237 QDBusMessage message =
238 QDBusMessage::createMethodCall(activityService, activityResources, 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(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceMimetype"));
243 message.setArguments({urlString, mimeType.name()});
244 bus.asyncCall(message);
245
246 message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceTitle"));
247 message.setArguments({urlString, url.fileName()});
248 bus.asyncCall(message);
249 }
250 }
251#endif
252
254 addAction(action, url, tmpName, QMimeType());
255}
256
257void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType)
258{
260 menu()->insertAction(menu()->actions().value(0), action);
261 d->m_recentActions.push_back({action, url, name, mimeType});
262}
263
265{
267 auto it = d->findByAction(action);
268 Q_ASSERT(it != d->m_recentActions.cend());
269 d->m_recentActions.erase(it);
271}
272
274{
276
277 auto it = d->findByUrl(url);
278
279 if (it != d->m_recentActions.cend()) {
280 d->removeAction(it);
281 };
282}
283
285{
286 Q_D(const KRecentFilesAction);
287
288 QList<QUrl> list;
289 list.reserve(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(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) {
294 return info.url;
295 });
296
297 return list;
298}
299
301{
302 clearEntries();
304}
305
306void KRecentFilesAction::clearEntries()
307{
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
318{
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(i);
341 value = cg.readPathEntry(key, QString());
342 if (value.isEmpty()) {
343 continue;
344 }
345 url = QUrl::fromUserInput(value);
346
347 auto [it, isNewUrl] = seenUrls.insert(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(i);
361 nameValue = cg.readPathEntry(nameKey, url.fileName());
362 title = titleWithSensibleWidth(nameValue, KShell::tildeCollapse(value));
363 if (!value.isNull()) {
364 thereAreEntries = true;
365 addAction(new QAction(title, selectableActionGroup()), url, 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
377{
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(i), url.toDisplayString(QUrl::PreferLocalFile));
392 cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName);
393
394 ++i;
395 }
396}
397
398#include "moc_krecentfilesaction.cpp"
QString name() const
void deleteGroup(const QString &group, WriteConfigFlags flags=Normal)
void writePathEntry(const char *Key, const QString &path, WriteConfigFlags pFlags=Normal)
QString readPathEntry(const char *key, const QString &aDefault) const
KConfig * config()
Recent files action.
void addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType=QMimeType())
Adds action to the list of URLs, with url and title name.
KRecentFilesAction(QObject *parent)
Constructs an action with the specified parent.
void removeUrl(const QUrl &url)
Remove an URL from the recent files list.
QAction * removeAction(QAction *action) override
Reimplemented for internal reasons.
QList< QUrl > urls() const
Retrieve a list of all URLs in the recent files list.
void recentListCleared()
This signal gets emitted when the user clear list.
void addUrl(const QUrl &url, const QString &name=QString())
Add URL to the recent files list.
void setMaxItems(int maxItems)
Sets the maximum of items in the recent files list.
~KRecentFilesAction() override
Destructor.
void loadEntries(const KConfigGroup &config)
Loads the recent files entries from a given KConfigGroup object.
void saveEntries(const KConfigGroup &config)
Saves the current recent files entries to a given KConfigGroup object.
virtual void clear()
Clears the recent files list.
virtual QAction * removeAction(QAction *action)
QList< QAction * > actions() const
QActionGroup * selectableActionGroup() const
QAction * action(const QString &text, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
void actionTriggered(QAction *action)
QString i18n(const char *text, const TYPE &arg...)
KCOREADDONS_EXPORT QString tildeCollapse(const QString &path)
QAction(QObject *parent)
void setEnabled(bool)
void setIcon(const QIcon &icon)
QMenu * menu() const const
void setText(const QString &text)
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QDBusConnectionInterface * interface() const const
bool isConnected() const const
QDBusConnection sessionBus()
QDBusReply< bool > isServiceRegistered(const QString &serviceName) const const
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
void setArguments(const QList< QVariant > &arguments)
QString tempPath()
QString toNativeSeparators(const QString &pathName)
QRect boundingRect(QChar ch) const const
QString elidedText(const QString &text, Qt::TextElideMode mode, int width, int flags) const const
QList< QScreen * > screens()
QIcon fromTheme(const QString &name)
void reserve(qsizetype size)
void aboutToShow()
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
Q_EMITQ_EMIT
int width() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
bool isNull() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
ElideMiddle
PreferLocalFile
QString fileName(ComponentFormattingOptions options) const const
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
QString toDisplayString(FormattingOptions options) const const
QString toLocalFile() const const
QString toString(FormattingOptions options) const const
QString url(FormattingOptions options) const const
void insertAction(QAction *before, QAction *action)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 12 2024 11:50:16 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.