1/*
2 This file is part of the KDE libraries
3
4 SPDX-FileCopyrightText: 2000, 2001 Dawit Alemayehu <adawit@kde.org>
5 SPDX-FileCopyrightText: 2000, 2001 Carsten Pfeiffer <pfeiffer@kde.org>
6 SPDX-FileCopyrightText: 2000 Stefan Schimanski <1Stein@gmx.de>
7
8 SPDX-License-Identifier: LGPL-2.1-or-later
9*/
10
11#include "khistorycombobox.h"
12#include "kcombobox_p.h"
13
14#include <KStandardShortcut>
15
16#include <QAbstractItemView>
17#include <QApplication>
18#include <QComboBox>
19#include <QMenu>
20#include <QWheelEvent>
21
22class KHistoryComboBoxPrivate : public KComboBoxPrivate
23{
24 Q_DECLARE_PUBLIC(KHistoryComboBox)
25
26public:
27 KHistoryComboBoxPrivate(KHistoryComboBox *q)
28 : KComboBoxPrivate(q)
29 {
30 }
31
32 void init(bool useCompletion);
33 void rotateUp();
34 void rotateDown();
35
36 /**
37 * Called from the popupmenu,
38 * calls clearHistory() and emits cleared()
39 */
40 void _k_clear();
41
42 /**
43 * Appends our own context menu entry.
44 */
45 void _k_addContextMenuItems(QMenu *);
46
47 /**
48 * Used to emit the activated(QString) signal when enter is pressed
49 */
50 void _k_simulateActivated(const QString &);
51
52 /**
53 * The text typed before Up or Down was pressed.
54 */
55 QString typedText;
56
57 /**
58 * The current index in the combobox, used for Up and Down
59 */
60 int currentIndex;
61
62 /**
63 * Indicates that the user at least once rotated Up through the entire list
64 * Needed to allow going back after rotation.
65 */
66 bool rotated = false;
67
68 std::function<QIcon(QString)> iconProvider;
69};
70
71void KHistoryComboBoxPrivate::init(bool useCompletion)
72{
73 Q_Q(KHistoryComboBox);
74 // Set a default history size to something reasonable, Qt sets it to INT_MAX by default
75 q->setMaxCount(50);
76
77 if (useCompletion) {
78 q->completionObject()->setOrder(KCompletion::Weighted);
79 }
80
81 q->setInsertPolicy(KHistoryComboBox::NoInsert);
82 currentIndex = -1;
83 rotated = false;
84
85 // obey HISTCONTROL setting
86 QByteArray histControl = qgetenv(varName: "HISTCONTROL");
87 if (histControl == "ignoredups" || histControl == "ignoreboth") {
88 q->setDuplicatesEnabled(false);
89 }
90
91 q->connect(sender: q, signal: &KComboBox::aboutToShowContextMenu, context: q, slot: [this](QMenu *menu) {
92 _k_addContextMenuItems(menu);
93 });
94 QObject::connect(sender: q, signal: qOverload<int>(&QComboBox::activated), context: q, slot: &KHistoryComboBox::reset);
95 QObject::connect(sender: q, signal: qOverload<const QString &>(&KComboBox::returnPressed), context: q, slot: [q]() {
96 q->reset();
97 });
98 // We want _k_simulateActivated to be called _after_ QComboBoxPrivate::_q_returnPressed
99 // otherwise there's a risk of emitting activated twice (_k_simulateActivated will find
100 // the item, after some app's slotActivated inserted the item into the combo).
101 q->connect(
102 sender: q,
103 signal: qOverload<const QString &>(&KComboBox::returnPressed),
104 context: q,
105 slot: [this](const QString &text) {
106 _k_simulateActivated(text);
107 },
108 type: Qt::QueuedConnection);
109}
110
111// we are always read-write
112KHistoryComboBox::KHistoryComboBox(QWidget *parent)
113 : KComboBox(*new KHistoryComboBoxPrivate(this), parent)
114{
115 Q_D(KHistoryComboBox);
116 d->init(useCompletion: true); // using completion
117 setEditable(true);
118}
119
120// we are always read-write
121KHistoryComboBox::KHistoryComboBox(bool useCompletion, QWidget *parent)
122 : KComboBox(*new KHistoryComboBoxPrivate(this), parent)
123{
124 Q_D(KHistoryComboBox);
125 d->init(useCompletion);
126 setEditable(true);
127}
128
129KHistoryComboBox::~KHistoryComboBox()
130{
131}
132
133void KHistoryComboBox::setHistoryItems(const QStringList &items)
134{
135 setHistoryItems(items, setCompletionList: false);
136}
137
138void KHistoryComboBox::setHistoryItems(const QStringList &items, bool setCompletionList)
139{
140 QStringList insertingItems = items;
141 KComboBox::clear();
142
143 // limit to maxCount()
144 const int itemCount = insertingItems.count();
145 const int toRemove = itemCount - maxCount();
146
147 if (toRemove >= itemCount) {
148 insertingItems.clear();
149 } else {
150 for (int i = 0; i < toRemove; ++i) {
151 insertingItems.pop_front();
152 }
153 }
154
155 insertItems(items: insertingItems);
156
157 if (setCompletionList && useCompletion()) {
158 // we don't have any weighting information here ;(
159 KCompletion *comp = completionObject();
160 comp->setOrder(KCompletion::Insertion);
161 comp->setItems(insertingItems);
162 comp->setOrder(KCompletion::Weighted);
163 }
164
165 clearEditText();
166}
167
168QStringList KHistoryComboBox::historyItems() const
169{
170 QStringList list;
171 const int itemCount = count();
172 list.reserve(asize: itemCount);
173 for (int i = 0; i < itemCount; ++i) {
174 list.append(t: itemText(index: i));
175 }
176
177 return list;
178}
179
180bool KHistoryComboBox::useCompletion() const
181{
182 return compObj();
183}
184
185void KHistoryComboBox::clearHistory()
186{
187 const QString temp = currentText();
188 KComboBox::clear();
189 if (useCompletion()) {
190 completionObject()->clear();
191 }
192 setEditText(temp);
193}
194
195void KHistoryComboBoxPrivate::_k_addContextMenuItems(QMenu *menu)
196{
197 Q_Q(KHistoryComboBox);
198 if (menu) {
199 menu->addSeparator();
200 QAction *clearHistory =
201 menu->addAction(icon: QIcon::fromTheme(QStringLiteral("edit-clear-history")), text: KHistoryComboBox::tr(s: "Clear &History", c: "@action:inmenu"), args: q, args: [this]() {
202 _k_clear();
203 });
204 if (!q->count()) {
205 clearHistory->setEnabled(false);
206 }
207 }
208}
209
210void KHistoryComboBox::addToHistory(const QString &item)
211{
212 Q_D(KHistoryComboBox);
213 if (item.isEmpty() || (count() > 0 && item == itemText(index: 0))) {
214 return;
215 }
216
217 bool wasCurrent = false;
218 // remove all existing items before adding
219 if (!duplicatesEnabled()) {
220 int i = 0;
221 int itemCount = count();
222 while (i < itemCount) {
223 if (itemText(index: i) == item) {
224 if (!wasCurrent) {
225 wasCurrent = (i == currentIndex());
226 }
227 removeItem(index: i);
228 --itemCount;
229 } else {
230 ++i;
231 }
232 }
233 }
234
235 // now add the item
236 if (d->iconProvider) {
237 insertItem(index: 0, icon: d->iconProvider(item), text: item);
238 } else {
239 insertItem(aindex: 0, atext: item);
240 }
241
242 if (wasCurrent) {
243 setCurrentIndex(0);
244 }
245
246 const bool useComp = useCompletion();
247
248 const int last = count() - 1; // last valid index
249 const int mc = maxCount();
250 const int stopAt = qMax(a: mc, b: 0);
251
252 for (int rmIndex = last; rmIndex >= stopAt; --rmIndex) {
253 // remove the last item, as long as we are longer than maxCount()
254 // remove the removed item from the completionObject if it isn't
255 // anymore available at all in the combobox.
256 const QString rmItem = itemText(index: rmIndex);
257 removeItem(index: rmIndex);
258 if (useComp && !contains(text: rmItem)) {
259 completionObject()->removeItem(item: rmItem);
260 }
261 }
262
263 if (useComp) {
264 completionObject()->addItem(item);
265 }
266}
267
268bool KHistoryComboBox::removeFromHistory(const QString &item)
269{
270 if (item.isEmpty()) {
271 return false;
272 }
273
274 bool removed = false;
275 const QString temp = currentText();
276 int i = 0;
277 int itemCount = count();
278 while (i < itemCount) {
279 if (item == itemText(index: i)) {
280 removed = true;
281 removeItem(index: i);
282 --itemCount;
283 } else {
284 ++i;
285 }
286 }
287
288 if (removed && useCompletion()) {
289 completionObject()->removeItem(item);
290 }
291
292 setEditText(temp);
293 return removed;
294}
295
296// going up in the history, rotating when reaching QListBox::count()
297//
298// Note: this differs from QComboBox because "up" means ++index here,
299// to simulate the way shell history works (up goes to the most
300// recent item). In QComboBox "down" means ++index, to match the popup...
301//
302void KHistoryComboBoxPrivate::rotateUp()
303{
304 Q_Q(KHistoryComboBox);
305 // save the current text in the lineedit
306 // (This is also where this differs from standard up/down in QComboBox,
307 // where a single keypress can make you lose your typed text)
308 if (currentIndex == -1) {
309 typedText = q->currentText();
310 }
311
312 ++currentIndex;
313
314 // skip duplicates/empty items
315 const int last = q->count() - 1; // last valid index
316 const QString currText = q->currentText();
317
318 while (currentIndex < last && (currText == q->itemText(index: currentIndex) || q->itemText(index: currentIndex).isEmpty())) {
319 ++currentIndex;
320 }
321
322 if (currentIndex >= q->count()) {
323 rotated = true;
324 currentIndex = -1;
325
326 // if the typed text is the same as the first item, skip the first
327 if (q->count() > 0 && typedText == q->itemText(index: 0)) {
328 currentIndex = 0;
329 }
330
331 q->setEditText(typedText);
332 } else {
333 q->setCurrentIndex(currentIndex);
334 }
335}
336
337// going down in the history, no rotation possible. Last item will be
338// the text that was in the lineedit before Up was called.
339void KHistoryComboBoxPrivate::rotateDown()
340{
341 Q_Q(KHistoryComboBox);
342 // save the current text in the lineedit
343 if (currentIndex == -1) {
344 typedText = q->currentText();
345 }
346
347 --currentIndex;
348
349 const QString currText = q->currentText();
350 // skip duplicates/empty items
351 while (currentIndex >= 0 //
352 && (currText == q->itemText(index: currentIndex) || q->itemText(index: currentIndex).isEmpty())) {
353 --currentIndex;
354 }
355
356 if (currentIndex < 0) {
357 if (rotated && currentIndex == -2) {
358 rotated = false;
359 currentIndex = q->count() - 1;
360 q->setEditText(q->itemText(index: currentIndex));
361 } else { // bottom of history
362 currentIndex = -1;
363 if (q->currentText() != typedText) {
364 q->setEditText(typedText);
365 }
366 }
367 } else {
368 q->setCurrentIndex(currentIndex);
369 }
370}
371
372void KHistoryComboBox::keyPressEvent(QKeyEvent *e)
373{
374 Q_D(KHistoryComboBox);
375 int event_key = e->key() | e->modifiers();
376
377 if (KStandardShortcut::rotateUp().contains(t: event_key)) {
378 d->rotateUp();
379 } else if (KStandardShortcut::rotateDown().contains(t: event_key)) {
380 d->rotateDown();
381 } else {
382 KComboBox::keyPressEvent(e);
383 }
384}
385
386void KHistoryComboBox::wheelEvent(QWheelEvent *ev)
387{
388 Q_D(KHistoryComboBox);
389 // Pass to poppable listbox if it's up
390 QAbstractItemView *const iv = view();
391 if (iv && iv->isVisible()) {
392 QApplication::sendEvent(receiver: iv, event: ev);
393 return;
394 }
395 // Otherwise make it change the text without emitting activated
396 if (ev->angleDelta().y() > 0) {
397 d->rotateUp();
398 } else {
399 d->rotateDown();
400 }
401 ev->accept();
402}
403
404void KHistoryComboBox::setIconProvider(std::function<QIcon(const QString &)> providerFunction)
405{
406 Q_D(KHistoryComboBox);
407 d->iconProvider = providerFunction;
408}
409
410void KHistoryComboBox::insertItems(const QStringList &items)
411{
412 Q_D(KHistoryComboBox);
413
414 for (const QString &item : items) {
415 if (item.isEmpty()) {
416 continue;
417 }
418
419 if (d->iconProvider) {
420 addItem(aicon: d->iconProvider(item), atext: item);
421 } else {
422 addItem(atext: item);
423 }
424 }
425}
426
427void KHistoryComboBoxPrivate::_k_clear()
428{
429 Q_Q(KHistoryComboBox);
430 q->clearHistory();
431 Q_EMIT q->cleared();
432}
433
434void KHistoryComboBoxPrivate::_k_simulateActivated(const QString &text)
435{
436 Q_Q(KHistoryComboBox);
437 /* With the insertion policy NoInsert, which we use by default,
438 Qt doesn't emit activated on typed text if the item is not already there,
439 which is perhaps reasonable. Generate the signal ourselves if that's the case.
440 */
441 if ((q->insertPolicy() == q->NoInsert && q->findText(text, flags: Qt::MatchFixedString | Qt::MatchCaseSensitive) == -1)) {
442 Q_EMIT q->textActivated(text);
443 }
444
445 /*
446 Qt also doesn't emit it if the box is full, and policy is not
447 InsertAtCurrent
448 */
449 else if (q->insertPolicy() != q->InsertAtCurrent && q->count() >= q->maxCount()) {
450 Q_EMIT q->textActivated(text);
451 }
452}
453
454void KHistoryComboBox::reset()
455{
456 Q_D(KHistoryComboBox);
457 d->currentIndex = -1;
458 d->rotated = false;
459}
460
461#include "moc_khistorycombobox.cpp"
462

source code of kcompletion/src/khistorycombobox.cpp