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 std::unique_ptr<KUrlComboBoxPrivate::KUrlComboItem> item(new KUrlComboBoxPrivate::KUrlComboItem(u, d->getIcon(url: u)));
222 d->insertUrlItem(item.get());
223 d->itemList.push_back(x: std::move(item));
224 ++it;
225 }
226}
227
228void KUrlComboBox::setUrl(const QUrl &url)
229{
230 if (url.isEmpty()) {
231 return;
232 }
233
234 bool blocked = blockSignals(b: true);
235
236 // check for duplicates
237 auto mit = d->itemMapper.constBegin();
238 QString urlToInsert = url.toString(options: QUrl::StripTrailingSlash);
239 while (mit != d->itemMapper.constEnd()) {
240 Q_ASSERT(mit.value());
241
242 if (urlToInsert == mit.value()->url.toString(options: QUrl::StripTrailingSlash)) {
243 setCurrentIndex(mit.key());
244
245 if (d->myMode == Directories) {
246 d->updateItem(item: mit.value(), index: mit.key(), icon: d->opendirIcon);
247 }
248
249 blockSignals(b: blocked);
250 return;
251 }
252 ++mit;
253 }
254
255 // not in the combo yet -> create a new item and insert it
256
257 // first remove the old item
258 if (d->urlAdded) {
259 Q_ASSERT(!d->itemList.empty());
260 d->itemList.pop_back();
261 d->urlAdded = false;
262 }
263
264 setDefaults();
265
266 const int offset = qMax(a: 0, b: static_cast<int>(d->itemList.size() + d->defaultList.size()) - d->myMaximum);
267 for (size_t i = offset; i < d->itemList.size(); ++i) {
268 d->insertUrlItem(d->itemList.at(n: i).get());
269 }
270
271 std::unique_ptr<KUrlComboBoxPrivate::KUrlComboItem> item(new KUrlComboBoxPrivate::KUrlComboItem(url, d->getIcon(url)));
272
273 const int id = count();
274 const QString text = d->textForItem(item: item.get());
275 if (d->myMode == Directories) {
276 KComboBox::insertItem(index: id, icon: d->opendirIcon, text);
277 } else {
278 KComboBox::insertItem(index: id, icon: item->icon, text);
279 }
280
281 d->itemMapper.insert(key: id, value: item.get());
282 d->itemList.push_back(x: std::move(item));
283
284 setCurrentIndex(id);
285 Q_ASSERT(!d->itemList.empty());
286 d->urlAdded = true;
287 blockSignals(b: blocked);
288}
289
290void KUrlComboBoxPrivate::slotActivated(int index)
291{
292 auto item = itemMapper.value(key: index);
293
294 if (item) {
295 m_parent->setUrl(item->url);
296 Q_EMIT m_parent->urlActivated(url: item->url);
297 }
298}
299
300void KUrlComboBoxPrivate::insertUrlItem(const KUrlComboItem *item)
301{
302 Q_ASSERT(item);
303
304 // qDebug() << "insertURLItem " << d->textForItem(item);
305 int id = m_parent->count();
306 m_parent->KComboBox::insertItem(index: id, icon: item->icon, text: textForItem(item));
307 itemMapper.insert(key: id, value: item);
308}
309
310void KUrlComboBox::setMaxItems(int max)
311{
312 d->myMaximum = max;
313
314 if (count() > d->myMaximum) {
315 int oldCurrent = currentIndex();
316
317 setDefaults();
318
319 const int offset = qMax(a: 0, b: static_cast<int>(d->itemList.size() + d->defaultList.size()) - d->myMaximum);
320 for (size_t i = offset; i < d->itemList.size(); ++i) {
321 d->insertUrlItem(item: d->itemList.at(n: i).get());
322 }
323
324 if (count() > 0) { // restore the previous currentItem
325 if (oldCurrent >= count()) {
326 oldCurrent = count() - 1;
327 }
328 setCurrentIndex(oldCurrent);
329 }
330 }
331}
332
333int KUrlComboBox::maxItems() const
334{
335 return d->myMaximum;
336}
337
338void KUrlComboBox::removeUrl(const QUrl &url, bool checkDefaultURLs)
339{
340 auto mit = d->itemMapper.constBegin();
341 while (mit != d->itemMapper.constEnd()) {
342 if (url.toString(options: QUrl::StripTrailingSlash) == mit.value()->url.toString(options: QUrl::StripTrailingSlash)) {
343 auto removePredicate = [&mit](const std::unique_ptr<const KUrlComboBoxPrivate::KUrlComboItem> &item) {
344 return item.get() == mit.value();
345 };
346 d->itemList.erase(first: std::remove_if(first: d->itemList.begin(), last: d->itemList.end(), pred: removePredicate), last: d->itemList.end());
347 if (checkDefaultURLs) {
348 d->defaultList.erase(first: std::remove_if(first: d->defaultList.begin(), last: d->defaultList.end(), pred: removePredicate), last: d->defaultList.end());
349 }
350 }
351 ++mit;
352 }
353
354 bool blocked = blockSignals(b: true);
355 setDefaults();
356 for (const auto &item : d->itemList) {
357 d->insertUrlItem(item: item.get());
358 }
359 blockSignals(b: blocked);
360}
361
362void KUrlComboBox::setCompletionObject(KCompletion *compObj, bool hsig)
363{
364 if (compObj) {
365 // on a url combo box we want completion matches to be sorted. This way, if we are given
366 // a suggestion, we match the "best" one. For instance, if we have "foo" and "foobar",
367 // and we write "foo", the match is "foo" and never "foobar". (ereslibre)
368 compObj->setOrder(KCompletion::Sorted);
369 }
370 KComboBox::setCompletionObject(completionObject: compObj, handleSignals: hsig);
371}
372
373void KUrlComboBox::mousePressEvent(QMouseEvent *event)
374{
375 QStyleOptionComboBox comboOpt;
376 comboOpt.initFrom(w: this);
377 const int x0 =
378 QStyle::visualRect(direction: layoutDirection(), boundingRect: rect(), logicalRect: style()->subControlRect(cc: QStyle::CC_ComboBox, opt: &comboOpt, sc: QStyle::SC_ComboBoxEditField, widget: this)).x();
379 const int frameWidth = style()->pixelMetric(metric: QStyle::PM_DefaultFrameWidth, option: &comboOpt, widget: this);
380
381 if (qRound(d: event->position().x()) < (x0 + KIconLoader::SizeSmall + frameWidth)) {
382 d->m_dragPoint = event->pos();
383 } else {
384 d->m_dragPoint = QPoint();
385 }
386
387 KComboBox::mousePressEvent(e: event);
388}
389
390void KUrlComboBox::mouseMoveEvent(QMouseEvent *event)
391{
392 const int index = currentIndex();
393 auto item = d->itemMapper.value(key: index);
394
395 if (item && !d->m_dragPoint.isNull() && event->buttons() & Qt::LeftButton
396 && (event->pos() - d->m_dragPoint).manhattanLength() > QApplication::startDragDistance()) {
397 QDrag *drag = new QDrag(this);
398 QMimeData *mime = new QMimeData();
399 mime->setUrls(QList<QUrl>() << item->url);
400 mime->setText(itemText(index));
401 if (!itemIcon(index).isNull()) {
402 drag->setPixmap(itemIcon(index).pixmap(extent: KIconLoader::SizeMedium));
403 }
404 drag->setMimeData(mime);
405 drag->exec();
406 }
407
408 KComboBox::mouseMoveEvent(event);
409}
410
411QIcon KUrlComboBoxPrivate::getIcon(const QUrl &url) const
412{
413 if (myMode == KUrlComboBox::Directories) {
414 return dirIcon;
415 } else {
416 return QIcon::fromTheme(name: KIO::iconNameForUrl(url));
417 }
418}
419
420// updates "item" with icon "icon"
421// kdelibs4 used to also say "and sets the URL instead of text", but this breaks const-ness,
422// now that it would require clearing the text, and I don't see the point since the URL was already in the text.
423void KUrlComboBoxPrivate::updateItem(const KUrlComboItem *item, int index, const QIcon &icon)
424{
425 m_parent->setItemIcon(index, icon);
426 m_parent->setItemText(index, text: textForItem(item));
427}
428
429#include "moc_kurlcombobox.cpp"
430

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