File indexing completed on 2024-10-06 06:39:36
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org> 0004 SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org> 0005 SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org> 0006 SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> 0007 SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org> 0008 SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org> 0009 SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org> 0010 SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org> 0011 SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org> 0012 SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org> 0013 0014 SPDX-License-Identifier: LGPL-2.0-only 0015 */ 0016 0017 #include "krecentfilesaction.h" 0018 #include "krecentfilesaction_p.h" 0019 0020 #include <QActionGroup> 0021 #include <QDir> 0022 #include <QGuiApplication> 0023 #include <QMenu> 0024 #include <QMimeDatabase> 0025 #include <QMimeType> 0026 #include <QScreen> 0027 0028 #ifdef QT_DBUS_LIB 0029 #include <QDBusConnectionInterface> 0030 #include <QDBusInterface> 0031 #include <QDBusMessage> 0032 #endif 0033 0034 #include <KConfig> 0035 #include <KConfigGroup> 0036 #include <KLocalizedString> 0037 #include <KShell> 0038 0039 #include <set> 0040 0041 KRecentFilesAction::KRecentFilesAction(QObject *parent) 0042 : KSelectAction(parent) 0043 , d_ptr(new KRecentFilesActionPrivate(this)) 0044 { 0045 Q_D(KRecentFilesAction); 0046 d->init(); 0047 } 0048 0049 KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent) 0050 : KSelectAction(parent) 0051 , d_ptr(new KRecentFilesActionPrivate(this)) 0052 { 0053 Q_D(KRecentFilesAction); 0054 d->init(); 0055 0056 // Want to keep the ampersands 0057 setText(text); 0058 } 0059 0060 KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent) 0061 : KSelectAction(parent) 0062 , d_ptr(new KRecentFilesActionPrivate(this)) 0063 { 0064 Q_D(KRecentFilesAction); 0065 d->init(); 0066 0067 setIcon(icon); 0068 // Want to keep the ampersands 0069 setText(text); 0070 } 0071 0072 void KRecentFilesActionPrivate::init() 0073 { 0074 Q_Q(KRecentFilesAction); 0075 delete q->menu(); 0076 q->setMenu(new QMenu()); 0077 q->setToolBarMode(KSelectAction::MenuMode); 0078 m_noEntriesAction = q->menu()->addAction(i18n("No Entries")); 0079 m_noEntriesAction->setObjectName(QStringLiteral("no_entries")); 0080 m_noEntriesAction->setEnabled(false); 0081 clearSeparator = q->menu()->addSeparator(); 0082 clearSeparator->setVisible(false); 0083 clearSeparator->setObjectName(QStringLiteral("separator")); 0084 clearAction = q->menu()->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear); 0085 clearAction->setObjectName(QStringLiteral("clear_action")); 0086 clearAction->setVisible(false); 0087 q->setEnabled(false); 0088 q->connect(q, &KSelectAction::actionTriggered, q, [this](QAction *action) { 0089 urlSelected(action); 0090 }); 0091 } 0092 0093 KRecentFilesAction::~KRecentFilesAction() = default; 0094 0095 void KRecentFilesActionPrivate::urlSelected(QAction *action) 0096 { 0097 Q_Q(KRecentFilesAction); 0098 0099 auto it = findByAction(action); 0100 0101 Q_ASSERT(it != m_recentActions.cend()); // Should never happen 0102 0103 const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules 0104 Q_EMIT q->urlSelected(url); 0105 } 0106 0107 // TODO: remove this helper function, it will crash if you use it in a loop 0108 void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it) 0109 { 0110 Q_Q(KRecentFilesAction); 0111 delete q->KSelectAction::removeAction(it->action); 0112 m_recentActions.erase(it); 0113 } 0114 0115 int KRecentFilesAction::maxItems() const 0116 { 0117 Q_D(const KRecentFilesAction); 0118 return d->m_maxItems; 0119 } 0120 0121 void KRecentFilesAction::setMaxItems(int maxItems) 0122 { 0123 Q_D(KRecentFilesAction); 0124 // set new maxItems 0125 d->m_maxItems = std::max(maxItems, 0); 0126 0127 // Remove all excess items, oldest (i.e. first added) first 0128 const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems; 0129 if (difference > 0) { 0130 auto beginIt = d->m_recentActions.begin(); 0131 auto endIt = d->m_recentActions.begin() + difference; 0132 for (auto it = beginIt; it < endIt; ++it) { 0133 // Remove the action from the menus, action groups ...etc 0134 delete KSelectAction::removeAction(it->action); 0135 } 0136 d->m_recentActions.erase(beginIt, endIt); 0137 } 0138 } 0139 0140 static QString titleWithSensibleWidth(const QString &nameValue, const QString &value) 0141 { 0142 // Calculate 3/4 of screen geometry, we do not want 0143 // action titles to be bigger than that 0144 // Since we do not know in which screen we are going to show 0145 // we choose the min of all the screens 0146 int maxWidthForTitles = INT_MAX; 0147 const auto screens = QGuiApplication::screens(); 0148 for (QScreen *screen : screens) { 0149 maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4); 0150 } 0151 const QFontMetrics fontMetrics = QFontMetrics(QFont()); 0152 0153 QString title = nameValue + QLatin1String(" [") + value + QLatin1Char(']'); 0154 const int nameWidth = fontMetrics.boundingRect(title).width(); 0155 if (nameWidth > maxWidthForTitles) { 0156 // If it does not fit, try to cut only the whole path, though if the 0157 // name is too long (more than 3/4 of the whole text) we cut it a bit too 0158 const int nameValueMaxWidth = maxWidthForTitles * 3 / 4; 0159 QString cutNameValue; 0160 QString cutValue; 0161 if (nameWidth > nameValueMaxWidth) { 0162 cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth); 0163 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth); 0164 } else { 0165 cutNameValue = nameValue; 0166 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth); 0167 } 0168 title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']'); 0169 } 0170 return title; 0171 } 0172 0173 void KRecentFilesAction::addUrl(const QUrl &url, const QString &name) 0174 { 0175 Q_D(KRecentFilesAction); 0176 0177 // ensure we never add items if we want none 0178 if (d->m_maxItems == 0) { 0179 return; 0180 } 0181 0182 if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) { 0183 return; 0184 } 0185 0186 // Remove url if it already exists in the list 0187 removeUrl(url); 0188 0189 // Remove oldest item if already maxItems in list 0190 Q_ASSERT(d->m_maxItems > 0); 0191 if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) { 0192 d->removeAction(d->m_recentActions.begin()); 0193 } 0194 0195 const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile)); 0196 const QString tmpName = !name.isEmpty() ? name : url.fileName(); 0197 #ifdef Q_OS_WIN 0198 const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl; 0199 #else 0200 const QString file = pathOrUrl; 0201 #endif 0202 0203 d->m_noEntriesAction->setVisible(false); 0204 d->clearSeparator->setVisible(true); 0205 d->clearAction->setVisible(true); 0206 setEnabled(true); 0207 // add file to list 0208 const QString title = titleWithSensibleWidth(tmpName, KShell::tildeCollapse(file)); 0209 0210 const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension); 0211 0212 #ifdef QT_DBUS_LIB 0213 static bool isKdeSession = qgetenv("XDG_CURRENT_DESKTOP") == "KDE"; 0214 if (isKdeSession) { 0215 const QDBusConnection bus = QDBusConnection::sessionBus(); 0216 if (bus.isConnected() && bus.interface()->isServiceRegistered(QStringLiteral("org.kde.ActivityManager"))) { 0217 const static QString activityService = QStringLiteral("org.kde.ActivityManager"); 0218 const static QString activityResources = QStringLiteral("/ActivityManager/Resources"); 0219 const static QString activityResouceInferface = QStringLiteral("org.kde.ActivityManager.Resources"); 0220 0221 const auto urlString = url.toString(QUrl::PreferLocalFile); 0222 QDBusMessage message = 0223 QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceEvent")); 0224 message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */}); 0225 bus.asyncCall(message); 0226 0227 message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceMimetype")); 0228 message.setArguments({urlString, mimeType.name()}); 0229 bus.asyncCall(message); 0230 0231 message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceTitle")); 0232 message.setArguments({urlString, url.fileName()}); 0233 bus.asyncCall(message); 0234 } 0235 } 0236 #endif 0237 0238 QAction *action = new QAction(title, selectableActionGroup()); 0239 addAction(action, url, tmpName, mimeType); 0240 } 0241 0242 void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &_mimeType) 0243 { 0244 Q_D(KRecentFilesAction); 0245 0246 auto mimeType = _mimeType; 0247 if (!mimeType.isValid()) { 0248 mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension); 0249 } 0250 0251 if (!mimeType.isDefault()) { 0252 action->setIcon(QIcon::fromTheme(mimeType.iconName())); 0253 } 0254 0255 menu()->insertAction(menu()->actions().value(0), action); 0256 d->m_recentActions.push_back({action, url, name}); 0257 } 0258 0259 QAction *KRecentFilesAction::removeAction(QAction *action) 0260 { 0261 Q_D(KRecentFilesAction); 0262 auto it = d->findByAction(action); 0263 Q_ASSERT(it != d->m_recentActions.cend()); 0264 d->m_recentActions.erase(it); 0265 return KSelectAction::removeAction(action); 0266 } 0267 0268 void KRecentFilesAction::removeUrl(const QUrl &url) 0269 { 0270 Q_D(KRecentFilesAction); 0271 0272 auto it = d->findByUrl(url); 0273 0274 if (it != d->m_recentActions.cend()) { 0275 d->removeAction(it); 0276 }; 0277 } 0278 0279 QList<QUrl> KRecentFilesAction::urls() const 0280 { 0281 Q_D(const KRecentFilesAction); 0282 0283 QList<QUrl> list; 0284 list.reserve(d->m_recentActions.size()); 0285 0286 using Info = KRecentFilesActionPrivate::RecentActionInfo; 0287 // Reverse order to match how the actions appear in the menu 0288 std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) { 0289 return info.url; 0290 }); 0291 0292 return list; 0293 } 0294 0295 void KRecentFilesAction::clear() 0296 { 0297 clearEntries(); 0298 Q_EMIT recentListCleared(); 0299 } 0300 0301 void KRecentFilesAction::clearEntries() 0302 { 0303 Q_D(KRecentFilesAction); 0304 KSelectAction::clear(); 0305 d->m_recentActions.clear(); 0306 d->m_noEntriesAction->setVisible(true); 0307 d->clearSeparator->setVisible(false); 0308 d->clearAction->setVisible(false); 0309 setEnabled(false); 0310 } 0311 0312 void KRecentFilesAction::loadEntries(const KConfigGroup &_config) 0313 { 0314 Q_D(KRecentFilesAction); 0315 clearEntries(); 0316 0317 QString key; 0318 QString value; 0319 QString nameKey; 0320 QString nameValue; 0321 QString title; 0322 QUrl url; 0323 0324 KConfigGroup cg = _config; 0325 // "<default>" means the group was constructed with an empty name 0326 if (cg.name() == QLatin1String("<default>")) { 0327 cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles")); 0328 } 0329 0330 std::set<QUrl> seenUrls; 0331 0332 bool thereAreEntries = false; 0333 // read file list 0334 for (int i = 1; i <= d->m_maxItems; i++) { 0335 key = QStringLiteral("File%1").arg(i); 0336 value = cg.readPathEntry(key, QString()); 0337 if (value.isEmpty()) { 0338 continue; 0339 } 0340 url = QUrl::fromUserInput(value); 0341 0342 auto [it, isNewUrl] = seenUrls.insert(url); 0343 // Don't restore if this url has already been restored (e.g. broken config) 0344 if (!isNewUrl) { 0345 continue; 0346 } 0347 0348 #ifdef Q_OS_WIN 0349 // convert to backslashes 0350 if (url.isLocalFile()) { 0351 value = QDir::toNativeSeparators(value); 0352 } 0353 #endif 0354 0355 nameKey = QStringLiteral("Name%1").arg(i); 0356 nameValue = cg.readPathEntry(nameKey, url.fileName()); 0357 title = titleWithSensibleWidth(nameValue, KShell::tildeCollapse(value)); 0358 if (!value.isNull()) { 0359 thereAreEntries = true; 0360 addAction(new QAction(title, selectableActionGroup()), url, nameValue); 0361 } 0362 } 0363 if (thereAreEntries) { 0364 d->m_noEntriesAction->setVisible(false); 0365 d->clearSeparator->setVisible(true); 0366 d->clearAction->setVisible(true); 0367 setEnabled(true); 0368 } 0369 } 0370 0371 void KRecentFilesAction::saveEntries(const KConfigGroup &_cg) 0372 { 0373 Q_D(KRecentFilesAction); 0374 0375 KConfigGroup cg = _cg; 0376 // "<default>" means the group was constructed with an empty name 0377 if (cg.name() == QLatin1String("<default>")) { 0378 cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles")); 0379 } 0380 0381 cg.deleteGroup(); 0382 0383 // write file list 0384 int i = 1; 0385 for (const auto &[action, url, shortName] : d->m_recentActions) { 0386 cg.writePathEntry(QStringLiteral("File%1").arg(i), url.toDisplayString(QUrl::PreferLocalFile)); 0387 cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName); 0388 0389 ++i; 0390 } 0391 } 0392 0393 #include "moc_krecentfilesaction.cpp"