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 | |
26 | class KUrlComboBoxPrivate |
27 | { |
28 | public: |
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 | |
70 | QString 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 | |
89 | KUrlComboBox::KUrlComboBox(Mode mode, QWidget *parent) |
90 | : KComboBox(parent) |
91 | , d(new KUrlComboBoxPrivate(this)) |
92 | { |
93 | d->init(mode); |
94 | } |
95 | |
96 | KUrlComboBox::KUrlComboBox(Mode mode, bool rw, QWidget *parent) |
97 | : KComboBox(rw, parent) |
98 | , d(new KUrlComboBoxPrivate(this)) |
99 | { |
100 | d->init(mode); |
101 | } |
102 | |
103 | KUrlComboBox::~KUrlComboBox() = default; |
104 | |
105 | void 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 | |
125 | QStringList 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 | |
144 | void KUrlComboBox::addDefaultUrl(const QUrl &url, const QString &text) |
145 | { |
146 | addDefaultUrl(url, icon: d->getIcon(url), text); |
147 | } |
148 | |
149 | void 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 | |
154 | void 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 | |
164 | void KUrlComboBox::setUrls(const QStringList &urls) |
165 | { |
166 | setUrls(urls, remove: RemoveBottom); |
167 | } |
168 | |
169 | void 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 | |
234 | void 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 | |
296 | void 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 | |
306 | void 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 | |
316 | void 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 | |
339 | int KUrlComboBox::maxItems() const |
340 | { |
341 | return d->myMaximum; |
342 | } |
343 | |
344 | void 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 | |
368 | void 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 | |
379 | void 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 | |
396 | void 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 | |
417 | QIcon 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. |
429 | void 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 | |