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{
191 addUrl(url, name, QString());
192}
193
194void KRecentFilesAction::addUrl(const QUrl &url, const QString &name, const QString &mimeTypeStr)
195{
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(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(d->m_recentActions.begin());
214 }
215
216 const QString pathOrUrl(url.toDisplayString(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(tmpName, KShell::tildeCollapse(file));
230
231#if HAVE_QTDBUS
232 static bool isKdeSession = qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
233 if (isKdeSession) {
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 QMimeType mimeType;
243 if (!mimeTypeStr.isEmpty()) {
244 mimeType = QMimeDatabase().mimeTypeForName(mimeTypeStr);
245 } else {
247 }
248
249 const auto urlString = url.toString(QUrl::PreferLocalFile);
250 QDBusMessage message =
251 QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceEvent"));
252 message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */});
253 bus.asyncCall(message);
254
255 message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceMimetype"));
256 message.setArguments({urlString, mimeType.name()});
257 bus.asyncCall(message);
258
259 message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceTitle"));
260 message.setArguments({urlString, url.fileName()});
261 bus.asyncCall(message);
262 }
263 }
264#endif
265
267 addAction(action, url, tmpName, QMimeType());
268}
269
270void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType)
271{
273 menu()->insertAction(menu()->actions().value(0), action);
274 d->m_recentActions.push_back({action, url, name, mimeType});
275}
276
278{
280 auto it = d->findByAction(action);
281 Q_ASSERT(it != d->m_recentActions.cend());
282 d->m_recentActions.erase(it);
284}
285
287{
289
290 auto it = d->findByUrl(url);
291
292 if (it != d->m_recentActions.cend()) {
293 d->removeAction(it);
294 };
295}
296
298{
299 Q_D(const KRecentFilesAction);
300
301 QList<QUrl> list;
302 list.reserve(d->m_recentActions.size());
303
304 using Info = KRecentFilesActionPrivate::RecentActionInfo;
305 // Reverse order to match how the actions appear in the menu
306 std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) {
307 return info.url;
308 });
309
310 return list;
311}
312
314{
315 clearEntries();
317}
318
319void KRecentFilesAction::clearEntries()
320{
323 d->m_recentActions.clear();
324 d->m_noEntriesAction->setVisible(true);
325 d->clearSeparator->setVisible(false);
326 d->clearAction->setVisible(false);
327 setEnabled(false);
328}
329
331{
333 clearEntries();
334
335 QString key;
336 QString value;
337 QString nameKey;
338 QString nameValue;
339 QString title;
340 QUrl url;
341
342 KConfigGroup cg = _config;
343 // "<default>" means the group was constructed with an empty name
344 if (cg.name() == QLatin1String("<default>")) {
345 cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
346 }
347
348 std::set<QUrl> seenUrls;
349
350 bool thereAreEntries = false;
351 // read file list
352 for (int i = 1; i <= d->m_maxItems; i++) {
353 key = QStringLiteral("File%1").arg(i);
354 value = cg.readPathEntry(key, QString());
355 if (value.isEmpty()) {
356 continue;
357 }
358 url = QUrl::fromUserInput(value);
359
360 auto [it, isNewUrl] = seenUrls.insert(url);
361 // Don't restore if this url has already been restored (e.g. broken config)
362 if (!isNewUrl) {
363 continue;
364 }
365
366#ifdef Q_OS_WIN
367 // convert to backslashes
368 if (url.isLocalFile()) {
369 value = QDir::toNativeSeparators(value);
370 }
371#endif
372
373 nameKey = QStringLiteral("Name%1").arg(i);
374 nameValue = cg.readPathEntry(nameKey, url.fileName());
375 title = titleWithSensibleWidth(nameValue, KShell::tildeCollapse(value));
376 if (!value.isNull()) {
377 thereAreEntries = true;
378 addAction(new QAction(title, selectableActionGroup()), url, nameValue);
379 }
380 }
381 if (thereAreEntries) {
382 d->m_noEntriesAction->setVisible(false);
383 d->clearSeparator->setVisible(true);
384 d->clearAction->setVisible(true);
385 setEnabled(true);
386 }
387}
388
390{
392
393 KConfigGroup cg = _cg;
394 // "<default>" means the group was constructed with an empty name
395 if (cg.name() == QLatin1String("<default>")) {
396 cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
397 }
398
399 cg.deleteGroup();
400
401 // write file list
402 int i = 1;
403 for (const auto &[action, url, shortName, _] : d->m_recentActions) {
404 cg.writePathEntry(QStringLiteral("File%1").arg(i), url.toDisplayString(QUrl::PreferLocalFile));
405 cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName);
406
407 ++i;
408 }
409}
410
411#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
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
Q_EMITQ_EMIT
int width() const const
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) 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 Nov 29 2024 11:50:09 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.