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