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