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"