File indexing completed on 2024-10-06 09:34:56
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 #include <KConfig> 0029 #include <KConfigGroup> 0030 #include <KLocalizedString> 0031 0032 #include <set> 0033 0034 KRecentFilesAction::KRecentFilesAction(QObject *parent) 0035 : KSelectAction(parent) 0036 , d_ptr(new KRecentFilesActionPrivate(this)) 0037 { 0038 Q_D(KRecentFilesAction); 0039 d->init(); 0040 } 0041 0042 KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent) 0043 : KSelectAction(parent) 0044 , d_ptr(new KRecentFilesActionPrivate(this)) 0045 { 0046 Q_D(KRecentFilesAction); 0047 d->init(); 0048 0049 // Want to keep the ampersands 0050 setText(text); 0051 } 0052 0053 KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent) 0054 : KSelectAction(parent) 0055 , d_ptr(new KRecentFilesActionPrivate(this)) 0056 { 0057 Q_D(KRecentFilesAction); 0058 d->init(); 0059 0060 setIcon(icon); 0061 // Want to keep the ampersands 0062 setText(text); 0063 } 0064 0065 void KRecentFilesActionPrivate::init() 0066 { 0067 Q_Q(KRecentFilesAction); 0068 delete q->menu(); 0069 q->setMenu(new QMenu()); 0070 q->setToolBarMode(KSelectAction::MenuMode); 0071 m_noEntriesAction = q->menu()->addAction(i18n("No Entries")); 0072 m_noEntriesAction->setObjectName(QStringLiteral("no_entries")); 0073 m_noEntriesAction->setEnabled(false); 0074 clearSeparator = q->menu()->addSeparator(); 0075 clearSeparator->setVisible(false); 0076 clearSeparator->setObjectName(QStringLiteral("separator")); 0077 clearAction = q->menu()->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear); 0078 clearAction->setObjectName(QStringLiteral("clear_action")); 0079 clearAction->setVisible(false); 0080 q->setEnabled(false); 0081 q->connect(q, qOverload<QAction *>(&KSelectAction::triggered), q, [this](QAction *action) { 0082 urlSelected(action); 0083 }); 0084 } 0085 0086 KRecentFilesAction::~KRecentFilesAction() = default; 0087 0088 void KRecentFilesActionPrivate::urlSelected(QAction *action) 0089 { 0090 Q_Q(KRecentFilesAction); 0091 0092 auto it = findByAction(action); 0093 0094 Q_ASSERT(it != m_recentActions.cend()); // Should never happen 0095 0096 const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules 0097 Q_EMIT q->urlSelected(url); 0098 } 0099 0100 // TODO: remove this helper function, it will crash if you use it in a loop 0101 void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it) 0102 { 0103 Q_Q(KRecentFilesAction); 0104 delete q->KSelectAction::removeAction(it->action); 0105 m_recentActions.erase(it); 0106 } 0107 0108 int KRecentFilesAction::maxItems() const 0109 { 0110 Q_D(const KRecentFilesAction); 0111 return d->m_maxItems; 0112 } 0113 0114 void KRecentFilesAction::setMaxItems(int maxItems) 0115 { 0116 Q_D(KRecentFilesAction); 0117 // set new maxItems 0118 d->m_maxItems = std::max(maxItems, 0); 0119 0120 // Remove all excess items, oldest (i.e. first added) first 0121 const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems; 0122 if (difference > 0) { 0123 auto beginIt = d->m_recentActions.begin(); 0124 auto endIt = d->m_recentActions.begin() + difference; 0125 for (auto it = beginIt; it < endIt; ++it) { 0126 // Remove the action from the menus, action groups ...etc 0127 delete KSelectAction::removeAction(it->action); 0128 } 0129 d->m_recentActions.erase(beginIt, endIt); 0130 } 0131 } 0132 0133 static QString titleWithSensibleWidth(const QString &nameValue, const QString &value) 0134 { 0135 // Calculate 3/4 of screen geometry, we do not want 0136 // action titles to be bigger than that 0137 // Since we do not know in which screen we are going to show 0138 // we choose the min of all the screens 0139 int maxWidthForTitles = INT_MAX; 0140 const auto screens = QGuiApplication::screens(); 0141 for (QScreen *screen : screens) { 0142 maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4); 0143 } 0144 const QFontMetrics fontMetrics = QFontMetrics(QFont()); 0145 0146 QString title = nameValue + QLatin1String(" [") + value + QLatin1Char(']'); 0147 const int nameWidth = fontMetrics.boundingRect(title).width(); 0148 if (nameWidth > maxWidthForTitles) { 0149 // If it does not fit, try to cut only the whole path, though if the 0150 // name is too long (more than 3/4 of the whole text) we cut it a bit too 0151 const int nameValueMaxWidth = maxWidthForTitles * 3 / 4; 0152 QString cutNameValue; 0153 QString cutValue; 0154 if (nameWidth > nameValueMaxWidth) { 0155 cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth); 0156 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth); 0157 } else { 0158 cutNameValue = nameValue; 0159 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth); 0160 } 0161 title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']'); 0162 } 0163 return title; 0164 } 0165 0166 void KRecentFilesAction::addUrl(const QUrl &url, const QString &name) 0167 { 0168 Q_D(KRecentFilesAction); 0169 0170 // ensure we never add items if we want none 0171 if (d->m_maxItems == 0) { 0172 return; 0173 } 0174 0175 if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) { 0176 return; 0177 } 0178 0179 // Remove url if it already exists in the list 0180 removeUrl(url); 0181 0182 // Remove oldest item if already maxItems in list 0183 Q_ASSERT(d->m_maxItems > 0); 0184 if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) { 0185 d->removeAction(d->m_recentActions.begin()); 0186 } 0187 0188 const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile)); 0189 const QString tmpName = !name.isEmpty() ? name : url.fileName(); 0190 #ifdef Q_OS_WIN 0191 const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl; 0192 #else 0193 const QString file = pathOrUrl; 0194 #endif 0195 0196 d->m_noEntriesAction->setVisible(false); 0197 d->clearSeparator->setVisible(true); 0198 d->clearAction->setVisible(true); 0199 setEnabled(true); 0200 // add file to list 0201 const QString title = titleWithSensibleWidth(tmpName, file); 0202 0203 QAction *action = new QAction(title, selectableActionGroup()); 0204 addAction(action, url, tmpName); 0205 } 0206 0207 void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name) 0208 { 0209 Q_D(KRecentFilesAction); 0210 0211 const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension); 0212 if (!mimeType.isDefault()) { 0213 action->setIcon(QIcon::fromTheme(mimeType.iconName())); 0214 } 0215 menu()->insertAction(menu()->actions().value(0), action); 0216 d->m_recentActions.push_back({action, url, name}); 0217 } 0218 0219 QAction *KRecentFilesAction::removeAction(QAction *action) 0220 { 0221 Q_D(KRecentFilesAction); 0222 auto it = d->findByAction(action); 0223 Q_ASSERT(it != d->m_recentActions.cend()); 0224 d->m_recentActions.erase(it); 0225 return KSelectAction::removeAction(action); 0226 } 0227 0228 void KRecentFilesAction::removeUrl(const QUrl &url) 0229 { 0230 Q_D(KRecentFilesAction); 0231 0232 auto it = d->findByUrl(url); 0233 0234 if (it != d->m_recentActions.cend()) { 0235 d->removeAction(it); 0236 }; 0237 } 0238 0239 QList<QUrl> KRecentFilesAction::urls() const 0240 { 0241 Q_D(const KRecentFilesAction); 0242 0243 QList<QUrl> list; 0244 list.reserve(d->m_recentActions.size()); 0245 0246 using Info = KRecentFilesActionPrivate::RecentActionInfo; 0247 // Reverse order to match how the actions appear in the menu 0248 std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) { 0249 return info.url; 0250 }); 0251 0252 return list; 0253 } 0254 0255 void KRecentFilesAction::clear() 0256 { 0257 clearEntries(); 0258 Q_EMIT recentListCleared(); 0259 } 0260 0261 void KRecentFilesAction::clearEntries() 0262 { 0263 Q_D(KRecentFilesAction); 0264 KSelectAction::clear(); 0265 d->m_recentActions.clear(); 0266 d->m_noEntriesAction->setVisible(true); 0267 d->clearSeparator->setVisible(false); 0268 d->clearAction->setVisible(false); 0269 setEnabled(false); 0270 } 0271 0272 void KRecentFilesAction::loadEntries(const KConfigGroup &_config) 0273 { 0274 Q_D(KRecentFilesAction); 0275 clearEntries(); 0276 0277 QString key; 0278 QString value; 0279 QString nameKey; 0280 QString nameValue; 0281 QString title; 0282 QUrl url; 0283 0284 KConfigGroup cg = _config; 0285 // "<default>" means the group was constructed with an empty name 0286 if (cg.name() == QLatin1String("<default>")) { 0287 cg = KConfigGroup(cg.config(), "RecentFiles"); 0288 } 0289 0290 std::set<QUrl> seenUrls; 0291 0292 bool thereAreEntries = false; 0293 // read file list 0294 for (int i = 1; i <= d->m_maxItems; i++) { 0295 key = QStringLiteral("File%1").arg(i); 0296 value = cg.readPathEntry(key, QString()); 0297 if (value.isEmpty()) { 0298 continue; 0299 } 0300 url = QUrl::fromUserInput(value); 0301 0302 auto [it, isNewUrl] = seenUrls.insert(url); 0303 // Don't restore if this url has already been restored (e.g. broken config) 0304 if (!isNewUrl) { 0305 continue; 0306 } 0307 0308 #ifdef Q_OS_WIN 0309 // convert to backslashes 0310 if (url.isLocalFile()) { 0311 value = QDir::toNativeSeparators(value); 0312 } 0313 #endif 0314 0315 nameKey = QStringLiteral("Name%1").arg(i); 0316 nameValue = cg.readPathEntry(nameKey, url.fileName()); 0317 title = titleWithSensibleWidth(nameValue, value); 0318 if (!value.isNull()) { 0319 thereAreEntries = true; 0320 addAction(new QAction(title, selectableActionGroup()), url, nameValue); 0321 } 0322 } 0323 if (thereAreEntries) { 0324 d->m_noEntriesAction->setVisible(false); 0325 d->clearSeparator->setVisible(true); 0326 d->clearAction->setVisible(true); 0327 setEnabled(true); 0328 } 0329 } 0330 0331 void KRecentFilesAction::saveEntries(const KConfigGroup &_cg) 0332 { 0333 Q_D(KRecentFilesAction); 0334 0335 KConfigGroup cg = _cg; 0336 // "<default>" means the group was constructed with an empty name 0337 if (cg.name() == QLatin1String("<default>")) { 0338 cg = KConfigGroup(cg.config(), "RecentFiles"); 0339 } 0340 0341 cg.deleteGroup(); 0342 0343 // write file list 0344 int i = 1; 0345 for (const auto &[action, url, shortName] : d->m_recentActions) { 0346 cg.writePathEntry(QStringLiteral("File%1").arg(i), url.toDisplayString(QUrl::PreferLocalFile)); 0347 cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName); 0348 0349 ++i; 0350 } 0351 } 0352 0353 #include "moc_krecentfilesaction.cpp"