File indexing completed on 2024-04-28 07:41:36

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"