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