| 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 | |