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

source code of kio/src/widgets/kurlcombobox.cpp