File indexing completed on 2024-12-22 04:14:00
0001 /* This file is part of the KDE libraries 0002 SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org> 0003 SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org> 0004 SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org> 0005 SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> 0006 SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org> 0007 SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org> 0008 SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org> 0009 SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org> 0010 SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org> 0011 SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org> 0012 SPDX-FileCopyrightText: 2022 Alvin Wong <alvin@alvinhc.com> 0013 0014 SPDX-License-Identifier: LGPL-2.0-only 0015 */ 0016 0017 #include "krecentfilesaction.h" 0018 #include "krecentfilesaction_p.h" 0019 0020 #include <QFile> 0021 #include <QGuiApplication> 0022 #include <QDir> 0023 #include <QMenu> 0024 #include <QComboBox> 0025 #include <QScreen> 0026 #include <QProxyStyle> 0027 #include <QStandardItemModel> 0028 #include <QStyleFactory> 0029 0030 #include <kconfig.h> 0031 #include <kconfiggroup.h> 0032 #include <klocalizedstring.h> 0033 0034 #include "KisRecentFilesManager.h" 0035 0036 class KRecentFilesIconProxyStyle : public QProxyStyle 0037 { 0038 public: 0039 KRecentFilesIconProxyStyle(QStyle *style = nullptr) 0040 : QProxyStyle(style) 0041 { 0042 } 0043 0044 int pixelMetric(PixelMetric metric, const QStyleOption *option = nullptr, const QWidget *widget = nullptr) const override 0045 { 0046 if (metric == QStyle::PM_SmallIconSize) { 0047 return 48; 0048 } 0049 return QProxyStyle::pixelMetric(metric, option, widget); 0050 } 0051 }; 0052 0053 KRecentFilesAction::KRecentFilesAction(QObject *parent) 0054 : KSelectAction(parent), 0055 d_ptr(new KRecentFilesActionPrivate(this)) 0056 { 0057 Q_D(KRecentFilesAction); 0058 d->init(); 0059 } 0060 0061 KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent) 0062 : KSelectAction(parent), 0063 d_ptr(new KRecentFilesActionPrivate(this)) 0064 { 0065 Q_D(KRecentFilesAction); 0066 d->init(); 0067 0068 // Want to keep the ampersands 0069 setText(text); 0070 } 0071 0072 KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent) 0073 : KSelectAction(parent), 0074 d_ptr(new KRecentFilesActionPrivate(this)) 0075 { 0076 Q_D(KRecentFilesAction); 0077 d->init(); 0078 0079 setIcon(icon); 0080 // Want to keep the ampersands 0081 setText(text); 0082 } 0083 0084 void KRecentFilesActionPrivate::init() 0085 { 0086 Q_Q(KRecentFilesAction); 0087 delete q->menu(); 0088 q->setMenu(new QMenu()); 0089 q->setToolBarMode(KSelectAction::MenuMode); 0090 m_noEntriesAction = q->menu()->addAction(i18n("No Entries")); 0091 m_noEntriesAction->setObjectName(QLatin1String("no_entries")); 0092 m_noEntriesAction->setEnabled(false); 0093 clearSeparator = q->menu()->addSeparator(); 0094 clearSeparator->setVisible(false); 0095 clearSeparator->setObjectName(QLatin1String("separator")); 0096 clearAction = q->menu()->addAction(i18n("Clear List"), q, SLOT(clearActionTriggered())); 0097 clearAction->setObjectName(QLatin1String("clear_action")); 0098 clearAction->setVisible(false); 0099 q->setEnabled(false); 0100 q->connect(q, SIGNAL(triggered(QAction*)), SLOT(_k_urlSelected(QAction*))); 0101 0102 QString baseStyleName = q->menu()->style()->objectName(); 0103 if (baseStyleName != QLatin1String("windows")) { 0104 // Force Fusion theme because other themes like Breeze doesn't 0105 // work well with QProxyStyle, may result in small icons. 0106 baseStyleName = QStringLiteral("fusion"); 0107 } 0108 QStyle *baseStyle = QStyleFactory::create(baseStyleName); 0109 QStyle *newStyle = new KRecentFilesIconProxyStyle(baseStyle); 0110 newStyle->setParent(q->menu()); 0111 q->menu()->setStyle(newStyle); 0112 0113 q->connect(q->menu(), 0114 SIGNAL(aboutToShow()), 0115 SLOT(menuAboutToShow())); 0116 0117 q->connect(KisRecentFilesManager::instance(), 0118 SIGNAL(fileAdded(const QUrl &)), 0119 SLOT(fileAdded(const QUrl &))); 0120 q->connect(KisRecentFilesManager::instance(), 0121 SIGNAL(fileRemoved(const QUrl &)), 0122 SLOT(fileRemoved(const QUrl &))); 0123 q->connect(KisRecentFilesManager::instance(), 0124 SIGNAL(listRenewed()), 0125 SLOT(listRenewed())); 0126 0127 // We have to manually trigger the initial load because 0128 // KisRecentFilesManager is initialized earlier than this. 0129 q->rebuildEntries(); 0130 } 0131 0132 KRecentFilesAction::~KRecentFilesAction() 0133 { 0134 delete d_ptr; 0135 } 0136 0137 void KRecentFilesActionPrivate::_k_urlSelected(QAction *action) 0138 { 0139 Q_Q(KRecentFilesAction); 0140 emit q->urlSelected(m_urls[action]); 0141 } 0142 0143 void KRecentFilesActionPrivate::updateIcon(const QStandardItem *item) 0144 { 0145 if (!item) { 0146 return; 0147 } 0148 const QUrl url = item->data().toUrl(); 0149 if (!url.isValid()) { 0150 return; 0151 } 0152 QAction *action = m_urls.key(url); 0153 if (!action) { 0154 return; 0155 } 0156 const QIcon icon = item->icon(); 0157 if (icon.isNull()) { 0158 return; 0159 } 0160 action->setIcon(icon); 0161 action->setIconVisibleInMenu(true); 0162 } 0163 0164 static QString titleWithSensibleWidth(const QString &nameValue, const QString &value) 0165 { 0166 // Calculate 3/4 of screen geometry, we do not want 0167 // action titles to be bigger than that 0168 // Since we do not know in which screen we are going to show 0169 // we choose the min of all the screens 0170 int maxWidthForTitles = INT_MAX; 0171 Q_FOREACH(const QScreen *screen, QGuiApplication::screens()) { 0172 maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4); 0173 } 0174 const QFontMetrics fontMetrics = QFontMetrics(QFont()); 0175 0176 QString title = nameValue + " [" + value + ']'; 0177 if (fontMetrics.boundingRect(title).width() > maxWidthForTitles) { 0178 // If it does not fit, try to cut only the whole path, though if the 0179 // name is too long (more than 3/4 of the whole text) we cut it a bit too 0180 const int nameValueMaxWidth = maxWidthForTitles * 3 / 4; 0181 const int nameWidth = fontMetrics.boundingRect(nameValue).width(); 0182 QString cutNameValue, cutValue; 0183 if (nameWidth > nameValueMaxWidth) { 0184 cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth); 0185 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth); 0186 } else { 0187 cutNameValue = nameValue; 0188 cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth); 0189 } 0190 title = cutNameValue + " [" + cutValue + ']'; 0191 } 0192 return title; 0193 } 0194 0195 void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &/*name*/) 0196 { 0197 Q_D(KRecentFilesAction); 0198 0199 menu()->insertAction(menu()->actions().value(0), action); 0200 d->m_urls.insert(action, url); 0201 } 0202 0203 QAction *KRecentFilesAction::removeAction(QAction *action) 0204 { 0205 Q_D(KRecentFilesAction); 0206 KSelectAction::removeAction(action); 0207 0208 d->m_urls.remove(action); 0209 0210 return action; 0211 } 0212 0213 void KRecentFilesAction::setRecentFilesModel(const QStandardItemModel *model) 0214 { 0215 Q_D(KRecentFilesAction); 0216 0217 if (d->m_recentFilesModel) { 0218 disconnect(d->m_recentFilesModel, nullptr, this, nullptr); 0219 } 0220 0221 d->m_recentFilesModel = model; 0222 // Do not connect the signals or populate the icons now, because we want 0223 // them to be lazy-loaded only when the menu is opened for the first time. 0224 d->m_fileIconsPopulated = false; 0225 } 0226 0227 void KRecentFilesAction::modelItemChanged(QStandardItem *item) 0228 { 0229 Q_D(KRecentFilesAction); 0230 d->updateIcon(item); 0231 } 0232 0233 void KRecentFilesAction::modelRowsInserted(const QModelIndex &/*parent*/, int first, int last) 0234 { 0235 Q_D(KRecentFilesAction); 0236 for (int i = first; i <= last; i++) { 0237 d->updateIcon(d->m_recentFilesModel->item(i)); 0238 } 0239 } 0240 0241 void KRecentFilesAction::menuAboutToShow() 0242 { 0243 Q_D(KRecentFilesAction); 0244 if (!d->m_fileIconsPopulated) { 0245 d->m_fileIconsPopulated = true; 0246 connect(d->m_recentFilesModel, SIGNAL(itemChanged(QStandardItem *)), 0247 SLOT(modelItemChanged(QStandardItem *))); 0248 connect(d->m_recentFilesModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), 0249 SLOT(modelRowsInserted(const QModelIndex &, int, int))); 0250 // Populate the file icons only on first showing the menu, so lazy 0251 // loading actually works. 0252 const int count = d->m_recentFilesModel->rowCount(); 0253 for (int i = 0; i < count; i++) { 0254 d->updateIcon(d->m_recentFilesModel->item(i)); 0255 } 0256 } 0257 } 0258 0259 void KRecentFilesAction::clearActionTriggered() 0260 { 0261 KisRecentFilesManager::instance()->clear(); 0262 } 0263 0264 void KRecentFilesAction::clearEntries() 0265 { 0266 Q_D(KRecentFilesAction); 0267 KSelectAction::clear(); 0268 d->m_urls.clear(); 0269 d->m_noEntriesAction->setVisible(true); 0270 d->clearSeparator->setVisible(false); 0271 d->clearAction->setVisible(false); 0272 setEnabled(false); 0273 } 0274 0275 void KRecentFilesAction::rebuildEntries() 0276 { 0277 Q_D(KRecentFilesAction); 0278 0279 clearEntries(); 0280 0281 QVector<KisRecentFilesEntry> items = KisRecentFilesManager::instance()->recentFiles(); 0282 if (items.count() > d->m_visibleItemsCount) { 0283 items = items.mid(items.count() - d->m_visibleItemsCount); 0284 } 0285 bool thereAreEntries = false; 0286 Q_FOREACH(const auto &item, items) { 0287 QString value; 0288 if (item.m_url.isLocalFile()) { 0289 value = item.m_url.toLocalFile(); 0290 #ifdef Q_OS_WIN 0291 // Convert forward slashes to backslashes 0292 value = QDir::toNativeSeparators(value); 0293 #endif 0294 } else { 0295 value = item.m_url.toDisplayString(); 0296 } 0297 const QString nameValue = item.m_displayName; 0298 const QString title = titleWithSensibleWidth(nameValue, value); 0299 if (!value.isNull()) { 0300 thereAreEntries = true; 0301 addAction(new QAction(title, selectableActionGroup()), item.m_url, nameValue); 0302 } 0303 } 0304 if (thereAreEntries) { 0305 d->m_noEntriesAction->setVisible(false); 0306 d->clearSeparator->setVisible(true); 0307 d->clearAction->setVisible(true); 0308 setEnabled(true); 0309 } 0310 } 0311 0312 void KRecentFilesAction::fileAdded(const QUrl &url) 0313 { 0314 Q_D(KRecentFilesAction); 0315 const QString name; // Dummy 0316 0317 if (d->m_visibleItemsCount <= 0) { 0318 return; 0319 } 0320 0321 // remove oldest item if already maxitems in list 0322 if (selectableActionGroup()->actions().count() >= d->m_visibleItemsCount) { 0323 // remove oldest added item 0324 delete removeAction(selectableActionGroup()->actions().first()); 0325 } 0326 0327 const QString tmpName = name.isEmpty() ? url.fileName() : name; 0328 const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile)); 0329 0330 #ifdef Q_OS_WIN 0331 const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl; 0332 #else 0333 const QString file = pathOrUrl; 0334 #endif 0335 0336 d->m_noEntriesAction->setVisible(false); 0337 d->clearSeparator->setVisible(true); 0338 d->clearAction->setVisible(true); 0339 setEnabled(true); 0340 // add file to list 0341 const QString title = titleWithSensibleWidth(tmpName, file); 0342 QAction *action = new QAction(title, selectableActionGroup()); 0343 addAction(action, url, tmpName); 0344 } 0345 0346 void KRecentFilesAction::fileRemoved(const QUrl &url) 0347 { 0348 Q_D(KRecentFilesAction); 0349 for (QMap<QAction *, QUrl>::ConstIterator it = d->m_urls.constBegin(); it != d->m_urls.constEnd(); ++it) { 0350 if (it.value() == url) { 0351 delete removeAction(it.key()); 0352 return; 0353 } 0354 } 0355 } 0356 0357 void KRecentFilesAction::listRenewed() 0358 { 0359 rebuildEntries(); 0360 } 0361 0362 #include "moc_krecentfilesaction.cpp"