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