File indexing completed on 2024-04-28 11:34:27
0001 /* 0002 This file is part of the KDE libraries 0003 0004 SPDX-FileCopyrightText: 2000, 2001 Dawit Alemayehu <adawit@kde.org> 0005 SPDX-FileCopyrightText: 2000, 2001 Carsten Pfeiffer <pfeiffer@kde.org> 0006 SPDX-FileCopyrightText: 2000 Stefan Schimanski <1Stein@gmx.de> 0007 0008 SPDX-License-Identifier: LGPL-2.1-or-later 0009 */ 0010 0011 #include "khistorycombobox.h" 0012 #include "kcombobox_p.h" 0013 0014 #include <KStandardShortcut> 0015 #include <kpixmapprovider.h> 0016 0017 #include <QAbstractItemView> 0018 #include <QApplication> 0019 #include <QComboBox> 0020 #include <QMenu> 0021 #include <QWheelEvent> 0022 0023 class KHistoryComboBoxPrivate : public KComboBoxPrivate 0024 { 0025 Q_DECLARE_PUBLIC(KHistoryComboBox) 0026 0027 public: 0028 KHistoryComboBoxPrivate(KHistoryComboBox *q) 0029 : KComboBoxPrivate(q) 0030 { 0031 } 0032 0033 void init(bool useCompletion); 0034 void rotateUp(); 0035 void rotateDown(); 0036 0037 /** 0038 * Called from the popupmenu, 0039 * calls clearHistory() and emits cleared() 0040 */ 0041 void _k_clear(); 0042 0043 /** 0044 * Appends our own context menu entry. 0045 */ 0046 void _k_addContextMenuItems(QMenu *); 0047 0048 /** 0049 * Used to emit the activated(QString) signal when enter is pressed 0050 */ 0051 void _k_simulateActivated(const QString &); 0052 0053 /** 0054 * The text typed before Up or Down was pressed. 0055 */ 0056 QString typedText; 0057 0058 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0059 KPixmapProvider *pixmapProvider = nullptr; 0060 #endif 0061 0062 /** 0063 * The current index in the combobox, used for Up and Down 0064 */ 0065 int currentIndex; 0066 0067 /** 0068 * Indicates that the user at least once rotated Up through the entire list 0069 * Needed to allow going back after rotation. 0070 */ 0071 bool rotated = false; 0072 0073 std::function<QIcon(QString)> iconProvider; 0074 }; 0075 0076 void KHistoryComboBoxPrivate::init(bool useCompletion) 0077 { 0078 Q_Q(KHistoryComboBox); 0079 // Set a default history size to something reasonable, Qt sets it to INT_MAX by default 0080 q->setMaxCount(50); 0081 0082 if (useCompletion) { 0083 q->completionObject()->setOrder(KCompletion::Weighted); 0084 } 0085 0086 q->setInsertPolicy(KHistoryComboBox::NoInsert); 0087 currentIndex = -1; 0088 rotated = false; 0089 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0090 pixmapProvider = nullptr; 0091 #endif 0092 0093 // obey HISTCONTROL setting 0094 QByteArray histControl = qgetenv("HISTCONTROL"); 0095 if (histControl == "ignoredups" || histControl == "ignoreboth") { 0096 q->setDuplicatesEnabled(false); 0097 } 0098 0099 q->connect(q, &KComboBox::aboutToShowContextMenu, q, [this](QMenu *menu) { 0100 _k_addContextMenuItems(menu); 0101 }); 0102 QObject::connect(q, qOverload<int>(&QComboBox::activated), q, &KHistoryComboBox::reset); 0103 QObject::connect(q, qOverload<const QString &>(&KComboBox::returnPressed), q, [q]() { 0104 q->reset(); 0105 }); 0106 // We want _k_simulateActivated to be called _after_ QComboBoxPrivate::_q_returnPressed 0107 // otherwise there's a risk of emitting activated twice (_k_simulateActivated will find 0108 // the item, after some app's slotActivated inserted the item into the combo). 0109 q->connect( 0110 q, 0111 qOverload<const QString &>(&KComboBox::returnPressed), 0112 q, 0113 [this](const QString &text) { 0114 _k_simulateActivated(text); 0115 }, 0116 Qt::QueuedConnection); 0117 } 0118 0119 // we are always read-write 0120 KHistoryComboBox::KHistoryComboBox(QWidget *parent) 0121 : KComboBox(*new KHistoryComboBoxPrivate(this), parent) 0122 { 0123 Q_D(KHistoryComboBox); 0124 d->init(true); // using completion 0125 setEditable(true); 0126 } 0127 0128 // we are always read-write 0129 KHistoryComboBox::KHistoryComboBox(bool useCompletion, QWidget *parent) 0130 : KComboBox(*new KHistoryComboBoxPrivate(this), parent) 0131 { 0132 Q_D(KHistoryComboBox); 0133 d->init(useCompletion); 0134 setEditable(true); 0135 } 0136 0137 KHistoryComboBox::~KHistoryComboBox() 0138 { 0139 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0140 Q_D(KHistoryComboBox); 0141 delete d->pixmapProvider; 0142 #endif 0143 } 0144 0145 void KHistoryComboBox::setHistoryItems(const QStringList &items) 0146 { 0147 setHistoryItems(items, false); 0148 } 0149 0150 void KHistoryComboBox::setHistoryItems(const QStringList &items, bool setCompletionList) 0151 { 0152 QStringList insertingItems = items; 0153 KComboBox::clear(); 0154 0155 // limit to maxCount() 0156 const int itemCount = insertingItems.count(); 0157 const int toRemove = itemCount - maxCount(); 0158 0159 if (toRemove >= itemCount) { 0160 insertingItems.clear(); 0161 } else { 0162 for (int i = 0; i < toRemove; ++i) { 0163 insertingItems.pop_front(); 0164 } 0165 } 0166 0167 insertItems(insertingItems); 0168 0169 if (setCompletionList && useCompletion()) { 0170 // we don't have any weighting information here ;( 0171 KCompletion *comp = completionObject(); 0172 comp->setOrder(KCompletion::Insertion); 0173 comp->setItems(insertingItems); 0174 comp->setOrder(KCompletion::Weighted); 0175 } 0176 0177 clearEditText(); 0178 } 0179 0180 QStringList KHistoryComboBox::historyItems() const 0181 { 0182 QStringList list; 0183 const int itemCount = count(); 0184 list.reserve(itemCount); 0185 for (int i = 0; i < itemCount; ++i) { 0186 list.append(itemText(i)); 0187 } 0188 0189 return list; 0190 } 0191 0192 bool KHistoryComboBox::useCompletion() const 0193 { 0194 return compObj(); 0195 } 0196 0197 void KHistoryComboBox::clearHistory() 0198 { 0199 const QString temp = currentText(); 0200 KComboBox::clear(); 0201 if (useCompletion()) { 0202 completionObject()->clear(); 0203 } 0204 setEditText(temp); 0205 } 0206 0207 void KHistoryComboBoxPrivate::_k_addContextMenuItems(QMenu *menu) 0208 { 0209 Q_Q(KHistoryComboBox); 0210 if (menu) { 0211 menu->addSeparator(); 0212 QAction *clearHistory = 0213 menu->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), KHistoryComboBox::tr("Clear &History", "@action:inmenu"), q, [this]() { 0214 _k_clear(); 0215 }); 0216 if (!q->count()) { 0217 clearHistory->setEnabled(false); 0218 } 0219 } 0220 } 0221 0222 void KHistoryComboBox::addToHistory(const QString &item) 0223 { 0224 Q_D(KHistoryComboBox); 0225 if (item.isEmpty() || (count() > 0 && item == itemText(0))) { 0226 return; 0227 } 0228 0229 bool wasCurrent = false; 0230 // remove all existing items before adding 0231 if (!duplicatesEnabled()) { 0232 int i = 0; 0233 int itemCount = count(); 0234 while (i < itemCount) { 0235 if (itemText(i) == item) { 0236 if (!wasCurrent) { 0237 wasCurrent = (i == currentIndex()); 0238 } 0239 removeItem(i); 0240 --itemCount; 0241 } else { 0242 ++i; 0243 } 0244 } 0245 } 0246 0247 // now add the item 0248 if (d->iconProvider) { 0249 insertItem(0, d->iconProvider(item), item); 0250 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0251 } else if (d->pixmapProvider) { 0252 insertItem(0, d->pixmapProvider->pixmapFor(item, iconSize().height()), item); 0253 #endif 0254 } else { 0255 insertItem(0, item); 0256 } 0257 0258 if (wasCurrent) { 0259 setCurrentIndex(0); 0260 } 0261 0262 const bool useComp = useCompletion(); 0263 0264 const int last = count() - 1; // last valid index 0265 const int mc = maxCount(); 0266 const int stopAt = qMax(mc, 0); 0267 0268 for (int rmIndex = last; rmIndex >= stopAt; --rmIndex) { 0269 // remove the last item, as long as we are longer than maxCount() 0270 // remove the removed item from the completionObject if it isn't 0271 // anymore available at all in the combobox. 0272 const QString rmItem = itemText(rmIndex); 0273 removeItem(rmIndex); 0274 if (useComp && !contains(rmItem)) { 0275 completionObject()->removeItem(rmItem); 0276 } 0277 } 0278 0279 if (useComp) { 0280 completionObject()->addItem(item); 0281 } 0282 } 0283 0284 bool KHistoryComboBox::removeFromHistory(const QString &item) 0285 { 0286 if (item.isEmpty()) { 0287 return false; 0288 } 0289 0290 bool removed = false; 0291 const QString temp = currentText(); 0292 int i = 0; 0293 int itemCount = count(); 0294 while (i < itemCount) { 0295 if (item == itemText(i)) { 0296 removed = true; 0297 removeItem(i); 0298 --itemCount; 0299 } else { 0300 ++i; 0301 } 0302 } 0303 0304 if (removed && useCompletion()) { 0305 completionObject()->removeItem(item); 0306 } 0307 0308 setEditText(temp); 0309 return removed; 0310 } 0311 0312 // going up in the history, rotating when reaching QListBox::count() 0313 // 0314 // Note: this differs from QComboBox because "up" means ++index here, 0315 // to simulate the way shell history works (up goes to the most 0316 // recent item). In QComboBox "down" means ++index, to match the popup... 0317 // 0318 void KHistoryComboBoxPrivate::rotateUp() 0319 { 0320 Q_Q(KHistoryComboBox); 0321 // save the current text in the lineedit 0322 // (This is also where this differs from standard up/down in QComboBox, 0323 // where a single keypress can make you lose your typed text) 0324 if (currentIndex == -1) { 0325 typedText = q->currentText(); 0326 } 0327 0328 ++currentIndex; 0329 0330 // skip duplicates/empty items 0331 const int last = q->count() - 1; // last valid index 0332 const QString currText = q->currentText(); 0333 0334 while (currentIndex < last && (currText == q->itemText(currentIndex) || q->itemText(currentIndex).isEmpty())) { 0335 ++currentIndex; 0336 } 0337 0338 if (currentIndex >= q->count()) { 0339 rotated = true; 0340 currentIndex = -1; 0341 0342 // if the typed text is the same as the first item, skip the first 0343 if (q->count() > 0 && typedText == q->itemText(0)) { 0344 currentIndex = 0; 0345 } 0346 0347 q->setEditText(typedText); 0348 } else { 0349 q->setCurrentIndex(currentIndex); 0350 } 0351 } 0352 0353 // going down in the history, no rotation possible. Last item will be 0354 // the text that was in the lineedit before Up was called. 0355 void KHistoryComboBoxPrivate::rotateDown() 0356 { 0357 Q_Q(KHistoryComboBox); 0358 // save the current text in the lineedit 0359 if (currentIndex == -1) { 0360 typedText = q->currentText(); 0361 } 0362 0363 --currentIndex; 0364 0365 const QString currText = q->currentText(); 0366 // skip duplicates/empty items 0367 while (currentIndex >= 0 // 0368 && (currText == q->itemText(currentIndex) || q->itemText(currentIndex).isEmpty())) { 0369 --currentIndex; 0370 } 0371 0372 if (currentIndex < 0) { 0373 if (rotated && currentIndex == -2) { 0374 rotated = false; 0375 currentIndex = q->count() - 1; 0376 q->setEditText(q->itemText(currentIndex)); 0377 } else { // bottom of history 0378 currentIndex = -1; 0379 if (q->currentText() != typedText) { 0380 q->setEditText(typedText); 0381 } 0382 } 0383 } else { 0384 q->setCurrentIndex(currentIndex); 0385 } 0386 } 0387 0388 void KHistoryComboBox::keyPressEvent(QKeyEvent *e) 0389 { 0390 Q_D(KHistoryComboBox); 0391 int event_key = e->key() | e->modifiers(); 0392 0393 if (KStandardShortcut::rotateUp().contains(event_key)) { 0394 d->rotateUp(); 0395 } else if (KStandardShortcut::rotateDown().contains(event_key)) { 0396 d->rotateDown(); 0397 } else { 0398 KComboBox::keyPressEvent(e); 0399 } 0400 } 0401 0402 void KHistoryComboBox::wheelEvent(QWheelEvent *ev) 0403 { 0404 Q_D(KHistoryComboBox); 0405 // Pass to poppable listbox if it's up 0406 QAbstractItemView *const iv = view(); 0407 if (iv && iv->isVisible()) { 0408 QApplication::sendEvent(iv, ev); 0409 return; 0410 } 0411 // Otherwise make it change the text without emitting activated 0412 if (ev->angleDelta().y() > 0) { 0413 d->rotateUp(); 0414 } else { 0415 d->rotateDown(); 0416 } 0417 ev->accept(); 0418 } 0419 0420 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0421 void KHistoryComboBox::setPixmapProvider(KPixmapProvider *provider) 0422 { 0423 Q_D(KHistoryComboBox); 0424 if (d->pixmapProvider == provider) { 0425 return; 0426 } 0427 0428 delete d->pixmapProvider; 0429 d->pixmapProvider = provider; 0430 0431 // re-insert all the items with/without pixmap 0432 // I would prefer to use changeItem(), but that doesn't honor the pixmap 0433 // when using an editable combobox (what we do) 0434 if (count() > 0) { 0435 QStringList items(historyItems()); 0436 clear(); 0437 insertItems(items); 0438 } 0439 } 0440 #endif 0441 0442 void KHistoryComboBox::setIconProvider(std::function<QIcon(const QString &)> providerFunction) 0443 { 0444 Q_D(KHistoryComboBox); 0445 d->iconProvider = providerFunction; 0446 } 0447 0448 void KHistoryComboBox::insertItems(const QStringList &items) 0449 { 0450 Q_D(KHistoryComboBox); 0451 0452 for (const QString &item : items) { 0453 if (item.isEmpty()) { 0454 continue; 0455 } 0456 0457 if (d->iconProvider) { 0458 addItem(d->iconProvider(item), item); 0459 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0460 } else if (d->pixmapProvider) { 0461 addItem(d->pixmapProvider->pixmapFor(item, iconSize().height()), item); 0462 #endif 0463 } else { 0464 addItem(item); 0465 } 0466 } 0467 } 0468 0469 void KHistoryComboBoxPrivate::_k_clear() 0470 { 0471 Q_Q(KHistoryComboBox); 0472 q->clearHistory(); 0473 Q_EMIT q->cleared(); 0474 } 0475 0476 void KHistoryComboBoxPrivate::_k_simulateActivated(const QString &text) 0477 { 0478 Q_Q(KHistoryComboBox); 0479 /* With the insertion policy NoInsert, which we use by default, 0480 Qt doesn't emit activated on typed text if the item is not already there, 0481 which is perhaps reasonable. Generate the signal ourselves if that's the case. 0482 */ 0483 if ((q->insertPolicy() == q->NoInsert && q->findText(text, Qt::MatchFixedString | Qt::MatchCaseSensitive) == -1)) { 0484 #if QT_DEPRECATED_SINCE(5, 15) 0485 QT_WARNING_PUSH 0486 QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations") 0487 QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations") 0488 Q_EMIT q->activated(text); 0489 QT_WARNING_POP 0490 #endif 0491 Q_EMIT q->textActivated(text); 0492 } 0493 0494 /* 0495 Qt also doesn't emit it if the box is full, and policy is not 0496 InsertAtCurrent 0497 */ 0498 else if (q->insertPolicy() != q->InsertAtCurrent && q->count() >= q->maxCount()) { 0499 #if QT_DEPRECATED_SINCE(5, 15) 0500 QT_WARNING_PUSH 0501 QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations") 0502 QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations") 0503 Q_EMIT q->activated(text); 0504 QT_WARNING_POP 0505 #endif 0506 Q_EMIT q->textActivated(text); 0507 } 0508 } 0509 0510 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 66) 0511 KPixmapProvider *KHistoryComboBox::pixmapProvider() const 0512 { 0513 Q_D(const KHistoryComboBox); 0514 return d->pixmapProvider; 0515 } 0516 #endif 0517 0518 void KHistoryComboBox::reset() 0519 { 0520 Q_D(KHistoryComboBox); 0521 d->currentIndex = -1; 0522 d->rotated = false; 0523 } 0524 0525 #include "moc_khistorycombobox.cpp"