| 1 | // This file is part of the KDE libraries |
| 2 | // SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de> |
| 3 | // SPDX-License-Identifier: LGPL-2.1-or-later |
| 4 | |
| 5 | #include "krecentfilesmenu.h" |
| 6 | |
| 7 | #include <QGuiApplication> |
| 8 | #include <QIcon> |
| 9 | #include <QScreen> |
| 10 | #include <QSettings> |
| 11 | #include <QStandardPaths> |
| 12 | |
| 13 | class RecentFilesEntry |
| 14 | { |
| 15 | public: |
| 16 | QUrl url; |
| 17 | QString displayName; |
| 18 | QAction *action = nullptr; |
| 19 | |
| 20 | QString titleWithSensibleWidth(QWidget *widget) const |
| 21 | { |
| 22 | const QString urlString = url.toDisplayString(options: QUrl::PreferLocalFile); |
| 23 | // Calculate 3/4 of screen geometry, we do not want |
| 24 | // action titles to be bigger than that |
| 25 | // Since we do not know in which screen we are going to show |
| 26 | // we choose the min of all the screens |
| 27 | int maxWidthForTitles = INT_MAX; |
| 28 | const auto screens = QGuiApplication::screens(); |
| 29 | for (QScreen *screen : screens) { |
| 30 | maxWidthForTitles = qMin(a: maxWidthForTitles, b: screen->availableGeometry().width() * 3 / 4); |
| 31 | } |
| 32 | |
| 33 | const QFontMetrics fontMetrics = widget->fontMetrics(); |
| 34 | |
| 35 | QString title = displayName + QLatin1String(" [" ) + urlString + QLatin1Char(']'); |
| 36 | const int nameWidth = fontMetrics.boundingRect(text: title).width(); |
| 37 | if (nameWidth > maxWidthForTitles) { |
| 38 | // If it does not fit, try to cut only the whole path, though if the |
| 39 | // name is too long (more than 3/4 of the whole text) we cut it a bit too |
| 40 | const int displayNameMaxWidth = maxWidthForTitles * 3 / 4; |
| 41 | QString cutNameValue; |
| 42 | QString cutValue; |
| 43 | if (nameWidth > displayNameMaxWidth) { |
| 44 | cutNameValue = fontMetrics.elidedText(text: displayName, mode: Qt::ElideMiddle, width: displayNameMaxWidth); |
| 45 | cutValue = fontMetrics.elidedText(text: urlString, mode: Qt::ElideMiddle, width: maxWidthForTitles - displayNameMaxWidth); |
| 46 | } else { |
| 47 | cutNameValue = displayName; |
| 48 | cutValue = fontMetrics.elidedText(text: urlString, mode: Qt::ElideMiddle, width: maxWidthForTitles - nameWidth); |
| 49 | } |
| 50 | title = cutNameValue + QLatin1String(" [" ) + cutValue + QLatin1Char(']'); |
| 51 | } |
| 52 | return title; |
| 53 | } |
| 54 | |
| 55 | explicit (const QUrl &_url, const QString &_displayName, KRecentFilesMenu *) |
| 56 | : url(_url) |
| 57 | , displayName(_displayName) |
| 58 | { |
| 59 | action = new QAction(titleWithSensibleWidth(widget: menu)); |
| 60 | QObject::connect(sender: action, signal: &QAction::triggered, context: action, slot: [this, menu]() { |
| 61 | Q_EMIT menu->urlTriggered(url); |
| 62 | }); |
| 63 | } |
| 64 | |
| 65 | ~RecentFilesEntry() |
| 66 | { |
| 67 | delete action; |
| 68 | } |
| 69 | }; |
| 70 | |
| 71 | class |
| 72 | { |
| 73 | public: |
| 74 | explicit KRecentFilesMenuPrivate(KRecentFilesMenu *q_ptr); |
| 75 | |
| 76 | std::vector<RecentFilesEntry *>::iterator findEntry(const QUrl &url); |
| 77 | void recentFilesChanged() const; |
| 78 | |
| 79 | KRecentFilesMenu *const ; |
| 80 | QString = QStringLiteral("RecentFiles" ); |
| 81 | std::vector<RecentFilesEntry *> ; |
| 82 | QSettings *; |
| 83 | size_t = 10; |
| 84 | QAction *; |
| 85 | QAction *; |
| 86 | }; |
| 87 | |
| 88 | KRecentFilesMenuPrivate::(KRecentFilesMenu *q_ptr) |
| 89 | : q(q_ptr) |
| 90 | { |
| 91 | } |
| 92 | |
| 93 | std::vector<RecentFilesEntry *>::iterator KRecentFilesMenuPrivate::(const QUrl &url) |
| 94 | { |
| 95 | return std::find_if(first: m_entries.begin(), last: m_entries.end(), pred: [url](RecentFilesEntry *entry) { |
| 96 | return entry->url == url; |
| 97 | }); |
| 98 | } |
| 99 | |
| 100 | void KRecentFilesMenuPrivate::() const |
| 101 | { |
| 102 | q->rebuildMenu(); |
| 103 | Q_EMIT q->recentFilesChanged(); |
| 104 | } |
| 105 | |
| 106 | KRecentFilesMenu::(const QString &title, QWidget *parent) |
| 107 | : QMenu(title, parent) |
| 108 | , d(new KRecentFilesMenuPrivate(this)) |
| 109 | { |
| 110 | setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent" ))); |
| 111 | const QString fileName = |
| 112 | QStringLiteral("%1/%2_recentfiles" ).arg(args: QStandardPaths::writableLocation(type: QStandardPaths::AppDataLocation), args: QCoreApplication::applicationName()); |
| 113 | d->m_settings = new QSettings(fileName, QSettings::Format::IniFormat, this); |
| 114 | |
| 115 | d->m_noEntriesAction = new QAction(tr(s: "No Entries" )); |
| 116 | d->m_noEntriesAction->setDisabled(true); |
| 117 | |
| 118 | d->m_clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history" )), tr(s: "Clear List" )); |
| 119 | |
| 120 | readFromFile(); |
| 121 | } |
| 122 | |
| 123 | KRecentFilesMenu::(QWidget *parent) |
| 124 | : KRecentFilesMenu(tr(s: "Recent Files" ), parent) |
| 125 | { |
| 126 | } |
| 127 | |
| 128 | KRecentFilesMenu::() |
| 129 | { |
| 130 | writeToFile(); |
| 131 | qDeleteAll(c: d->m_entries); |
| 132 | delete d->m_clearAction; |
| 133 | delete d->m_noEntriesAction; |
| 134 | } |
| 135 | |
| 136 | void KRecentFilesMenu::() |
| 137 | { |
| 138 | qDeleteAll(c: d->m_entries); |
| 139 | d->m_entries.clear(); |
| 140 | |
| 141 | d->m_settings->beginGroup(prefix: d->m_group); |
| 142 | const int size = d->m_settings->beginReadArray(QStringLiteral("files" )); |
| 143 | |
| 144 | d->m_entries.reserve(n: size); |
| 145 | |
| 146 | for (int i = 0; i < size; ++i) { |
| 147 | d->m_settings->setArrayIndex(i); |
| 148 | |
| 149 | const QUrl url = d->m_settings->value(QStringLiteral("url" )).toUrl(); |
| 150 | RecentFilesEntry *entry = new RecentFilesEntry(url, d->m_settings->value(QStringLiteral("displayName" )).toString(), this); |
| 151 | d->m_entries.push_back(x: entry); |
| 152 | } |
| 153 | |
| 154 | d->m_settings->endArray(); |
| 155 | d->m_settings->endGroup(); |
| 156 | |
| 157 | d->recentFilesChanged(); |
| 158 | } |
| 159 | |
| 160 | void KRecentFilesMenu::(const QUrl &url, const QString &name) |
| 161 | { |
| 162 | if (d->m_entries.size() == d->m_maximumItems) { |
| 163 | delete d->m_entries.back(); |
| 164 | d->m_entries.pop_back(); |
| 165 | } |
| 166 | |
| 167 | // If it's already there remove the old one and reinsert so it appears as new |
| 168 | auto it = d->findEntry(url); |
| 169 | if (it != d->m_entries.cend()) { |
| 170 | delete *it; |
| 171 | d->m_entries.erase(position: it); |
| 172 | } |
| 173 | |
| 174 | QString displayName = name; |
| 175 | |
| 176 | if (displayName.isEmpty()) { |
| 177 | displayName = url.fileName(); |
| 178 | } |
| 179 | |
| 180 | RecentFilesEntry *entry = new RecentFilesEntry(url, displayName, this); |
| 181 | d->m_entries.insert(position: d->m_entries.begin(), x: entry); |
| 182 | |
| 183 | d->recentFilesChanged(); |
| 184 | } |
| 185 | |
| 186 | void KRecentFilesMenu::(const QUrl &url) |
| 187 | { |
| 188 | auto it = d->findEntry(url); |
| 189 | |
| 190 | if (it == d->m_entries.end()) { |
| 191 | return; |
| 192 | } |
| 193 | |
| 194 | delete *it; |
| 195 | d->m_entries.erase(position: it); |
| 196 | |
| 197 | d->recentFilesChanged(); |
| 198 | } |
| 199 | |
| 200 | void KRecentFilesMenu::() |
| 201 | { |
| 202 | clear(); |
| 203 | |
| 204 | if (d->m_entries.empty()) { |
| 205 | addAction(action: d->m_noEntriesAction); |
| 206 | return; |
| 207 | } |
| 208 | |
| 209 | for (const RecentFilesEntry *entry : d->m_entries) { |
| 210 | addAction(action: entry->action); |
| 211 | } |
| 212 | |
| 213 | addSeparator(); |
| 214 | addAction(action: d->m_clearAction); |
| 215 | |
| 216 | // reconnect d->m_clearAction, since it was disconnected in clear() |
| 217 | connect(sender: d->m_clearAction, signal: &QAction::triggered, context: this, slot: &KRecentFilesMenu::clearRecentFiles); |
| 218 | } |
| 219 | |
| 220 | void KRecentFilesMenu::() |
| 221 | { |
| 222 | d->m_settings->remove(key: QString()); |
| 223 | d->m_settings->beginGroup(prefix: d->m_group); |
| 224 | d->m_settings->beginWriteArray(QStringLiteral("files" )); |
| 225 | |
| 226 | int index = 0; |
| 227 | for (const RecentFilesEntry *entry : d->m_entries) { |
| 228 | d->m_settings->setArrayIndex(index); |
| 229 | d->m_settings->setValue(QStringLiteral("url" ), value: entry->url); |
| 230 | d->m_settings->setValue(QStringLiteral("displayName" ), value: entry->displayName); |
| 231 | ++index; |
| 232 | } |
| 233 | |
| 234 | d->m_settings->endArray(); |
| 235 | d->m_settings->endGroup(); |
| 236 | d->m_settings->sync(); |
| 237 | } |
| 238 | |
| 239 | QString KRecentFilesMenu::() const |
| 240 | { |
| 241 | return d->m_group; |
| 242 | } |
| 243 | |
| 244 | void KRecentFilesMenu::(const QString &group) |
| 245 | { |
| 246 | d->m_group = group; |
| 247 | readFromFile(); |
| 248 | } |
| 249 | |
| 250 | int KRecentFilesMenu::() const |
| 251 | { |
| 252 | return d->m_maximumItems; |
| 253 | } |
| 254 | |
| 255 | void KRecentFilesMenu::(size_t maximumItems) |
| 256 | { |
| 257 | d->m_maximumItems = maximumItems; |
| 258 | |
| 259 | // Truncate if there are more entries than the new maximum |
| 260 | if (d->m_entries.size() > maximumItems) { |
| 261 | qDeleteAll(begin: d->m_entries.begin() + maximumItems, end: d->m_entries.end()); |
| 262 | d->m_entries.erase(first: d->m_entries.begin() + maximumItems, last: d->m_entries.end()); |
| 263 | |
| 264 | d->recentFilesChanged(); |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | QList<QUrl> KRecentFilesMenu::() const |
| 269 | { |
| 270 | QList<QUrl> urls; |
| 271 | urls.reserve(asize: d->m_entries.size()); |
| 272 | std::transform(first: d->m_entries.cbegin(), last: d->m_entries.cend(), result: std::back_inserter(x&: urls), unary_op: [](const RecentFilesEntry *entry) { |
| 273 | return entry->url; |
| 274 | }); |
| 275 | |
| 276 | return urls; |
| 277 | } |
| 278 | |
| 279 | void KRecentFilesMenu::() |
| 280 | { |
| 281 | qDeleteAll(c: d->m_entries); |
| 282 | d->m_entries.clear(); |
| 283 | |
| 284 | d->recentFilesChanged(); |
| 285 | } |
| 286 | |
| 287 | #include "moc_krecentfilesmenu.cpp" |
| 288 | |