1 /*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000, 2001 Carsten Pfeiffer <pfeiffer@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6 */
7
8 #include "kurlcombobox.h"
9
10 #include "../pathhelpers_p.h" // isAbsoluteLocalPath()
11
12 #include <QApplication>
13 #include <QDir>
14 #include <QDrag>
15 #include <QMimeData>
16 #include <QMouseEvent>
17
18 #include <KIconLoader>
19 #include <KLocalizedString>
20 #include <QDebug>
21 #include <kio/global.h>
22
23 #include <algorithm>
24 #include <memory>
25 #include <vector>
26
27 class KUrlComboBoxPrivate
28 {
29 public:
KUrlComboBoxPrivate(KUrlComboBox * parent)30 KUrlComboBoxPrivate(KUrlComboBox *parent)
31 : m_parent(parent)
32 , dirIcon(QIcon::fromTheme(QStringLiteral("folder")))
33 {
34 }
35
36 struct KUrlComboItem {
KUrlComboItemKUrlComboBoxPrivate::KUrlComboItem37 KUrlComboItem(const QUrl &url, const QIcon &icon, const QString &text = QString())
38 : url(url)
39 , icon(icon)
40 , text(text)
41 {
42 }
43 QUrl url;
44 QIcon icon;
45 QString text; // if empty, calculated from the QUrl
46 };
47
48 void init(KUrlComboBox::Mode mode);
49 QString textForItem(const KUrlComboItem *item) const;
50 void insertUrlItem(const KUrlComboItem *);
51 QIcon getIcon(const QUrl &url) const;
52 void updateItem(const KUrlComboItem *item, int index, const QIcon &icon);
53
54 void _k_slotActivated(int);
55
56 KUrlComboBox *const m_parent;
57 QIcon dirIcon;
58 bool urlAdded;
59 int myMaximum;
60 KUrlComboBox::Mode myMode;
61 QPoint m_dragPoint;
62
63 using KUrlComboItemList = std::vector<std::unique_ptr<const KUrlComboItem>>;
64 KUrlComboItemList itemList;
65 KUrlComboItemList defaultList;
66 QMap<int, const KUrlComboItem *> itemMapper;
67
68 QIcon opendirIcon;
69 };
70
textForItem(const KUrlComboItem * item) const71 QString KUrlComboBoxPrivate::textForItem(const KUrlComboItem *item) const
72 {
73 if (!item->text.isEmpty()) {
74 return item->text;
75 }
76 QUrl url = item->url;
77
78 if (myMode == KUrlComboBox::Directories) {
79 if (!url.path().isEmpty() && !url.path().endsWith(QLatin1Char('/'))) {
80 url.setPath(url.path() + QLatin1Char('/'));
81 }
82 } else {
83 url = url.adjusted(QUrl::StripTrailingSlash);
84 }
85 if (url.isLocalFile()) {
86 return url.toLocalFile();
87 } else {
88 return url.toDisplayString();
89 }
90 }
91
KUrlComboBox(Mode mode,QWidget * parent)92 KUrlComboBox::KUrlComboBox(Mode mode, QWidget *parent)
93 : KComboBox(parent)
94 , d(new KUrlComboBoxPrivate(this))
95 {
96 d->init(mode);
97 }
98
KUrlComboBox(Mode mode,bool rw,QWidget * parent)99 KUrlComboBox::KUrlComboBox(Mode mode, bool rw, QWidget *parent)
100 : KComboBox(rw, parent)
101 , d(new KUrlComboBoxPrivate(this))
102 {
103 d->init(mode);
104 }
105
~KUrlComboBox()106 KUrlComboBox::~KUrlComboBox()
107 {
108 delete d;
109 }
110
init(KUrlComboBox::Mode mode)111 void KUrlComboBoxPrivate::init(KUrlComboBox::Mode mode)
112 {
113 myMode = mode;
114 urlAdded = false;
115 myMaximum = 10; // default
116 m_parent->setInsertPolicy(KUrlComboBox::NoInsert);
117 m_parent->setTrapReturnKey(true);
118 m_parent->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
119 m_parent->setLayoutDirection(Qt::LeftToRight);
120 if (m_parent->completionObject()) {
121 m_parent->completionObject()->setOrder(KCompletion::Sorted);
122 }
123
124 opendirIcon = QIcon::fromTheme(QStringLiteral("folder-open"));
125
126 m_parent->connect(m_parent, qOverload<int>(&KUrlComboBox::activated), m_parent, [this](int index) {
127 _k_slotActivated(index);
128 });
129 }
130
urls() const131 QStringList KUrlComboBox::urls() const
132 {
133 // qDebug() << "::urls()";
134 QStringList list;
135 QString url;
136 for (int i = static_cast<int>(d->defaultList.size()); i < count(); i++) {
137 url = itemText(i);
138 if (!url.isEmpty()) {
139 if (isAbsoluteLocalPath(url)) {
140 list.append(QUrl::fromLocalFile(url).toString());
141 } else {
142 list.append(url);
143 }
144 }
145 }
146
147 return list;
148 }
149
addDefaultUrl(const QUrl & url,const QString & text)150 void KUrlComboBox::addDefaultUrl(const QUrl &url, const QString &text)
151 {
152 addDefaultUrl(url, d->getIcon(url), text);
153 }
154
addDefaultUrl(const QUrl & url,const QIcon & icon,const QString & text)155 void KUrlComboBox::addDefaultUrl(const QUrl &url, const QIcon &icon, const QString &text)
156 {
157 d->defaultList.push_back(std::unique_ptr<KUrlComboBoxPrivate::KUrlComboItem>(new KUrlComboBoxPrivate::KUrlComboItem(url, icon, text)));
158 }
159
setDefaults()160 void KUrlComboBox::setDefaults()
161 {
162 clear();
163 d->itemMapper.clear();
164
165 for (const auto &item : d->defaultList) {
166 d->insertUrlItem(item.get());
167 }
168 }
169
setUrls(const QStringList & urls)170 void KUrlComboBox::setUrls(const QStringList &urls)
171 {
172 setUrls(urls, RemoveBottom);
173 }
174
setUrls(const QStringList & _urls,OverLoadResolving remove)175 void KUrlComboBox::setUrls(const QStringList &_urls, OverLoadResolving remove)
176 {
177 setDefaults();
178 d->itemList.clear();
179 d->urlAdded = false;
180
181 if (_urls.isEmpty()) {
182 return;
183 }
184
185 QStringList urls;
186 QStringList::ConstIterator it = _urls.constBegin();
187
188 // kill duplicates
189 while (it != _urls.constEnd()) {
190 if (!urls.contains(*it)) {
191 urls += *it;
192 }
193 ++it;
194 }
195
196 // limit to myMaximum items
197 /* Note: overload is an (old) C++ keyword, some compilers (KCC) choke
198 on that, so call it Overload (capital 'O'). (matz) */
199 int Overload = urls.count() - d->myMaximum + static_cast<int>(d->defaultList.size());
200 while (Overload > 0) {
201 if (remove == RemoveBottom) {
202 if (!urls.isEmpty()) {
203 urls.removeLast();
204 }
205 } else {
206 if (!urls.isEmpty()) {
207 urls.removeFirst();
208 }
209 }
210 Overload--;
211 }
212
213 it = urls.constBegin();
214
215 while (it != urls.constEnd()) {
216 if ((*it).isEmpty()) {
217 ++it;
218 continue;
219 }
220 QUrl u;
221 if (isAbsoluteLocalPath(*it)) {
222 u = QUrl::fromLocalFile(*it);
223 } else {
224 u.setUrl(*it);
225 }
226
227 // Don't restore if file doesn't exist anymore
228 if (u.isLocalFile() && !QFile::exists(u.toLocalFile())) {
229 ++it;
230 continue;
231 }
232
233 std::unique_ptr<KUrlComboBoxPrivate::KUrlComboItem> item(new KUrlComboBoxPrivate::KUrlComboItem(u, d->getIcon(u)));
234 d->insertUrlItem(item.get());
235 d->itemList.push_back(std::move(item));
236 ++it;
237 }
238 }
239
setUrl(const QUrl & url)240 void KUrlComboBox::setUrl(const QUrl &url)
241 {
242 if (url.isEmpty()) {
243 return;
244 }
245
246 bool blocked = blockSignals(true);
247
248 // check for duplicates
249 auto mit = d->itemMapper.constBegin();
250 QString urlToInsert = url.toString(QUrl::StripTrailingSlash);
251 while (mit != d->itemMapper.constEnd()) {
252 Q_ASSERT(mit.value());
253
254 if (urlToInsert == mit.value()->url.toString(QUrl::StripTrailingSlash)) {
255 setCurrentIndex(mit.key());
256
257 if (d->myMode == Directories) {
258 d->updateItem(mit.value(), mit.key(), d->opendirIcon);
259 }
260
261 blockSignals(blocked);
262 return;
263 }
264 ++mit;
265 }
266
267 // not in the combo yet -> create a new item and insert it
268
269 // first remove the old item
270 if (d->urlAdded) {
271 Q_ASSERT(!d->itemList.empty());
272 d->itemList.pop_back();
273 d->urlAdded = false;
274 }
275
276 setDefaults();
277
278 const int offset = qMax(0, static_cast<int>(d->itemList.size() + d->defaultList.size()) - d->myMaximum);
279 for (size_t i = offset; i < d->itemList.size(); ++i) {
280 d->insertUrlItem(d->itemList.at(i).get());
281 }
282
283 std::unique_ptr<KUrlComboBoxPrivate::KUrlComboItem> item(new KUrlComboBoxPrivate::KUrlComboItem(url, d->getIcon(url)));
284
285 const int id = count();
286 const QString text = d->textForItem(item.get());
287 if (d->myMode == Directories) {
288 KComboBox::insertItem(id, d->opendirIcon, text);
289 } else {
290 KComboBox::insertItem(id, item->icon, text);
291 }
292
293 d->itemMapper.insert(id, item.get());
294 d->itemList.push_back(std::move(item));
295
296 setCurrentIndex(id);
297 Q_ASSERT(!d->itemList.empty());
298 d->urlAdded = true;
299 blockSignals(blocked);
300 }
301
_k_slotActivated(int index)302 void KUrlComboBoxPrivate::_k_slotActivated(int index)
303 {
304 auto item = itemMapper.value(index);
305
306 if (item) {
307 m_parent->setUrl(item->url);
308 Q_EMIT m_parent->urlActivated(item->url);
309 }
310 }
311
insertUrlItem(const KUrlComboItem * item)312 void KUrlComboBoxPrivate::insertUrlItem(const KUrlComboItem *item)
313 {
314 Q_ASSERT(item);
315
316 // qDebug() << "insertURLItem " << d->textForItem(item);
317 int id = m_parent->count();
318 m_parent->KComboBox::insertItem(id, item->icon, textForItem(item));
319 itemMapper.insert(id, item);
320 }
321
setMaxItems(int max)322 void KUrlComboBox::setMaxItems(int max)
323 {
324 d->myMaximum = max;
325
326 if (count() > d->myMaximum) {
327 int oldCurrent = currentIndex();
328
329 setDefaults();
330
331 const int offset = qMax(0, static_cast<int>(d->itemList.size() + d->defaultList.size()) - d->myMaximum);
332 for (size_t i = offset; i < d->itemList.size(); ++i) {
333 d->insertUrlItem(d->itemList.at(i).get());
334 }
335
336 if (count() > 0) { // restore the previous currentItem
337 if (oldCurrent >= count()) {
338 oldCurrent = count() - 1;
339 }
340 setCurrentIndex(oldCurrent);
341 }
342 }
343 }
344
maxItems() const345 int KUrlComboBox::maxItems() const
346 {
347 return d->myMaximum;
348 }
349
removeUrl(const QUrl & url,bool checkDefaultURLs)350 void KUrlComboBox::removeUrl(const QUrl &url, bool checkDefaultURLs)
351 {
352 auto mit = d->itemMapper.constBegin();
353 while (mit != d->itemMapper.constEnd()) {
354 if (url.toString(QUrl::StripTrailingSlash) == mit.value()->url.toString(QUrl::StripTrailingSlash)) {
355 auto removePredicate = [&mit](const std::unique_ptr<const KUrlComboBoxPrivate::KUrlComboItem> &item) {
356 return item.get() == mit.value();
357 };
358 d->itemList.erase(std::remove_if(d->itemList.begin(), d->itemList.end(), removePredicate), d->itemList.end());
359 if (checkDefaultURLs) {
360 d->defaultList.erase(std::remove_if(d->defaultList.begin(), d->defaultList.end(), removePredicate), d->defaultList.end());
361 }
362 }
363 ++mit;
364 }
365
366 bool blocked = blockSignals(true);
367 setDefaults();
368 for (const auto &item : d->itemList) {
369 d->insertUrlItem(item.get());
370 }
371 blockSignals(blocked);
372 }
373
setCompletionObject(KCompletion * compObj,bool hsig)374 void KUrlComboBox::setCompletionObject(KCompletion *compObj, bool hsig)
375 {
376 if (compObj) {
377 // on a url combo box we want completion matches to be sorted. This way, if we are given
378 // a suggestion, we match the "best" one. For instance, if we have "foo" and "foobar",
379 // and we write "foo", the match is "foo" and never "foobar". (ereslibre)
380 compObj->setOrder(KCompletion::Sorted);
381 }
382 KComboBox::setCompletionObject(compObj, hsig);
383 }
384
mousePressEvent(QMouseEvent * event)385 void KUrlComboBox::mousePressEvent(QMouseEvent *event)
386 {
387 QStyleOptionComboBox comboOpt;
388 comboOpt.initFrom(this);
389 const int x0 =
390 QStyle::visualRect(layoutDirection(), rect(), style()->subControlRect(QStyle::CC_ComboBox, &comboOpt, QStyle::SC_ComboBoxEditField, this)).x();
391 const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &comboOpt, this);
392
393 if (event->x() < (x0 + KIconLoader::SizeSmall + frameWidth)) {
394 d->m_dragPoint = event->pos();
395 } else {
396 d->m_dragPoint = QPoint();
397 }
398
399 KComboBox::mousePressEvent(event);
400 }
401
mouseMoveEvent(QMouseEvent * event)402 void KUrlComboBox::mouseMoveEvent(QMouseEvent *event)
403 {
404 const int index = currentIndex();
405 auto item = d->itemMapper.value(index);
406
407 if (item && !d->m_dragPoint.isNull() && event->buttons() & Qt::LeftButton
408 && (event->pos() - d->m_dragPoint).manhattanLength() > QApplication::startDragDistance()) {
409 QDrag *drag = new QDrag(this);
410 QMimeData *mime = new QMimeData();
411 mime->setUrls(QList<QUrl>() << item->url);
412 mime->setText(itemText(index));
413 if (!itemIcon(index).isNull()) {
414 drag->setPixmap(itemIcon(index).pixmap(KIconLoader::SizeMedium));
415 }
416 drag->setMimeData(mime);
417 drag->exec();
418 }
419
420 KComboBox::mouseMoveEvent(event);
421 }
422
getIcon(const QUrl & url) const423 QIcon KUrlComboBoxPrivate::getIcon(const QUrl &url) const
424 {
425 if (myMode == KUrlComboBox::Directories) {
426 return dirIcon;
427 } else {
428 return QIcon::fromTheme(KIO::iconNameForUrl(url));
429 }
430 }
431
432 // updates "item" with icon "icon"
433 // kdelibs4 used to also say "and sets the URL instead of text", but this breaks const-ness,
434 // now that it would require clearing the text, and I don't see the point since the URL was already in the text.
updateItem(const KUrlComboItem * item,int index,const QIcon & icon)435 void KUrlComboBoxPrivate::updateItem(const KUrlComboItem *item, int index, const QIcon &icon)
436 {
437 m_parent->setItemIcon(index, icon);
438 m_parent->setItemText(index, textForItem(item));
439 }
440
441 #include "moc_kurlcombobox.cpp"
442