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