1 /*
2     This file is part of the KDE libraries
3 
4     SPDX-FileCopyrightText: 2000, 2001, 2002 Carsten Pfeiffer <pfeiffer@kde.org>
5     SPDX-FileCopyrightText: 2000 Stefan Schimanski <1Stein@gmx.de>
6     SPDX-FileCopyrightText: 2000, 2001, 2002, 2003, 2004 Dawit Alemayehu <adawit@kde.org>
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "kcompletionbox.h"
12 #include "klineedit.h"
13 
14 #include <QApplication>
15 #include <QKeyEvent>
16 #include <QScreen>
17 #include <QScrollBar>
18 
19 class KCompletionBoxPrivate
20 {
21 public:
KCompletionBoxPrivate(KCompletionBox * parent)22     KCompletionBoxPrivate(KCompletionBox *parent)
23         : q_ptr(parent)
24     {
25     }
26     void init();
27     void cancelled();
28     void _k_itemClicked(QListWidgetItem *);
29 
30     QWidget *m_parent = nullptr; // necessary to set the focus back
31     QString cancelText;
32     bool tabHandling;
33     bool upwardBox;
34     bool emitSelected;
35 
36     KCompletionBox *const q_ptr;
37     Q_DECLARE_PUBLIC(KCompletionBox)
38 };
39 
KCompletionBox(QWidget * parent)40 KCompletionBox::KCompletionBox(QWidget *parent)
41     : QListWidget(parent)
42     , d_ptr(new KCompletionBoxPrivate(this))
43 {
44     Q_D(KCompletionBox);
45     d->m_parent = parent;
46     d->init();
47 }
48 
init()49 void KCompletionBoxPrivate::init()
50 {
51     Q_Q(KCompletionBox);
52     tabHandling = true;
53     upwardBox = false;
54     emitSelected = true;
55 
56     // we can't link to QXcbWindowFunctions::Combo
57     // also, q->setAttribute(Qt::WA_X11NetWmWindowTypeCombo); is broken in Qt xcb
58     q->setProperty("_q_xcb_wm_window_type", 0x001000);
59     q->setAttribute(Qt::WA_ShowWithoutActivating);
60 
61     // on wayland, we need an xdg-popup but we don't want it to grab
62     // calls setVisible, so must be done after initializations
63     if (qGuiApp->platformName() == QLatin1String("wayland")) {
64         q->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint);
65     } else {
66         q->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint);
67     }
68     q->setUniformItemSizes(true);
69 
70     q->setLineWidth(1);
71     q->setFrameStyle(QFrame::Box | QFrame::Plain);
72 
73     q->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
74     q->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
75 
76     q->connect(q, &QListWidget::itemDoubleClicked, q, &KCompletionBox::slotActivated);
77     q->connect(q, &KCompletionBox::itemClicked, q, [this](QListWidgetItem *item) {
78         _k_itemClicked(item);
79     });
80 }
81 
~KCompletionBox()82 KCompletionBox::~KCompletionBox()
83 {
84     Q_D(KCompletionBox);
85     d->m_parent = nullptr;
86 }
87 
items() const88 QStringList KCompletionBox::items() const
89 {
90     QStringList list;
91     list.reserve(count());
92     for (int i = 0; i < count(); i++) {
93         const QListWidgetItem *currItem = item(i);
94 
95         list.append(currItem->text());
96     }
97 
98     return list;
99 }
100 
slotActivated(QListWidgetItem * item)101 void KCompletionBox::slotActivated(QListWidgetItem *item)
102 {
103     if (!item) {
104         return;
105     }
106 
107     hide();
108 
109 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 81)
110     Q_EMIT activated(item->text());
111 #endif
112     Q_EMIT textActivated(item->text());
113 }
114 
eventFilter(QObject * o,QEvent * e)115 bool KCompletionBox::eventFilter(QObject *o, QEvent *e)
116 {
117     Q_D(KCompletionBox);
118     int type = e->type();
119     QWidget *wid = qobject_cast<QWidget *>(o);
120 
121     if (o == this) {
122         return false;
123     }
124 
125     if (wid && wid == d->m_parent //
126         && (type == QEvent::Move || type == QEvent::Resize)) {
127         resizeAndReposition();
128         return false;
129     }
130 
131     if (wid && (wid->windowFlags() & Qt::Window) //
132         && type == QEvent::Move && wid == d->m_parent->window()) {
133         hide();
134         return false;
135     }
136 
137     if (type == QEvent::MouseButtonPress && (wid && !isAncestorOf(wid))) {
138         if (!d->emitSelected && currentItem() && !qobject_cast<QScrollBar *>(o)) {
139             Q_ASSERT(currentItem());
140             Q_EMIT currentTextChanged(currentItem()->text());
141         }
142         hide();
143         e->accept();
144         return true;
145     }
146 
147     if (wid && wid->isAncestorOf(d->m_parent) && isVisible()) {
148         if (type == QEvent::KeyPress) {
149             QKeyEvent *ev = static_cast<QKeyEvent *>(e);
150             switch (ev->key()) {
151             case Qt::Key_Backtab:
152                 if (d->tabHandling && (ev->modifiers() == Qt::NoButton || (ev->modifiers() & Qt::ShiftModifier))) {
153                     up();
154                     ev->accept();
155                     return true;
156                 }
157                 break;
158             case Qt::Key_Tab:
159                 if (d->tabHandling && (ev->modifiers() == Qt::NoButton)) {
160                     down();
161                     // #65877: Key_Tab should complete using the first
162                     // (or selected) item, and then offer completions again
163                     if (count() == 1) {
164                         KLineEdit *parent = qobject_cast<KLineEdit *>(d->m_parent);
165                         if (parent) {
166                             parent->doCompletion(currentItem()->text());
167                         } else {
168                             hide();
169                         }
170                     }
171                     ev->accept();
172                     return true;
173                 }
174                 break;
175             case Qt::Key_Down:
176                 down();
177                 ev->accept();
178                 return true;
179             case Qt::Key_Up:
180                 // If there is no selected item and we've popped up above
181                 // our parent, select the first item when they press up.
182                 if (!selectedItems().isEmpty() //
183                     || mapToGlobal(QPoint(0, 0)).y() > d->m_parent->mapToGlobal(QPoint(0, 0)).y()) {
184                     up();
185                 } else {
186                     down();
187                 }
188                 ev->accept();
189                 return true;
190             case Qt::Key_PageUp:
191                 pageUp();
192                 ev->accept();
193                 return true;
194             case Qt::Key_PageDown:
195                 pageDown();
196                 ev->accept();
197                 return true;
198             case Qt::Key_Escape:
199                 d->cancelled();
200                 ev->accept();
201                 return true;
202             case Qt::Key_Enter:
203             case Qt::Key_Return:
204                 if (ev->modifiers() & Qt::ShiftModifier) {
205                     hide();
206                     ev->accept(); // Consume the Enter event
207                     return true;
208                 }
209                 break;
210             case Qt::Key_End:
211                 if (ev->modifiers() & Qt::ControlModifier) {
212                     end();
213                     ev->accept();
214                     return true;
215                 }
216                 break;
217             case Qt::Key_Home:
218                 if (ev->modifiers() & Qt::ControlModifier) {
219                     home();
220                     ev->accept();
221                     return true;
222                 }
223                 Q_FALLTHROUGH();
224             default:
225                 break;
226             }
227         } else if (type == QEvent::ShortcutOverride) {
228             // Override any accelerators that match
229             // the key sequences we use here...
230             QKeyEvent *ev = static_cast<QKeyEvent *>(e);
231             switch (ev->key()) {
232             case Qt::Key_Down:
233             case Qt::Key_Up:
234             case Qt::Key_PageUp:
235             case Qt::Key_PageDown:
236             case Qt::Key_Escape:
237             case Qt::Key_Enter:
238             case Qt::Key_Return:
239                 ev->accept();
240                 return true;
241             case Qt::Key_Tab:
242             case Qt::Key_Backtab:
243                 if (ev->modifiers() == Qt::NoButton || (ev->modifiers() & Qt::ShiftModifier)) {
244                     ev->accept();
245                     return true;
246                 }
247                 break;
248             case Qt::Key_Home:
249             case Qt::Key_End:
250                 if (ev->modifiers() & Qt::ControlModifier) {
251                     ev->accept();
252                     return true;
253                 }
254                 break;
255             default:
256                 break;
257             }
258         } else if (type == QEvent::FocusOut) {
259             QFocusEvent *event = static_cast<QFocusEvent *>(e);
260             if (event->reason() != Qt::PopupFocusReason
261 #ifdef Q_OS_WIN
262                 && (event->reason() != Qt::ActiveWindowFocusReason || QApplication::activeWindow() != this)
263 #endif
264             ) {
265                 hide();
266             }
267         }
268     }
269 
270     return QListWidget::eventFilter(o, e);
271 }
272 
popup()273 void KCompletionBox::popup()
274 {
275     if (count() == 0) {
276         hide();
277     } else {
278         bool block = signalsBlocked();
279         blockSignals(true);
280         setCurrentRow(-1);
281         blockSignals(block);
282         clearSelection();
283         if (!isVisible()) {
284             show();
285         } else if (size().height() != sizeHint().height()) {
286             resizeAndReposition();
287         }
288     }
289 }
290 
resizeAndReposition()291 void KCompletionBox::resizeAndReposition()
292 {
293     Q_D(KCompletionBox);
294     int currentGeom = height();
295     QPoint currentPos = pos();
296     QRect geom = calculateGeometry();
297     resize(geom.size());
298 
299     int x = currentPos.x();
300     int y = currentPos.y();
301     if (d->m_parent) {
302         if (!isVisible()) {
303             const QPoint orig = globalPositionHint();
304             QScreen *screen = QGuiApplication::screenAt(orig);
305             if (screen) {
306                 const QRect screenSize = screen->geometry();
307 
308                 x = orig.x() + geom.x();
309                 y = orig.y() + geom.y();
310 
311                 if (x + width() > screenSize.right()) {
312                     x = screenSize.right() - width();
313                 }
314                 if (y + height() > screenSize.bottom()) {
315                     y = y - height() - d->m_parent->height();
316                     d->upwardBox = true;
317                 }
318             }
319         } else {
320             // Are we above our parent? If so we must keep bottom edge anchored.
321             if (d->upwardBox) {
322                 y += (currentGeom - height());
323             }
324         }
325         move(x, y);
326     }
327 }
328 
globalPositionHint() const329 QPoint KCompletionBox::globalPositionHint() const
330 {
331     Q_D(const KCompletionBox);
332     if (!d->m_parent) {
333         return QPoint();
334     }
335     return d->m_parent->mapToGlobal(QPoint(0, d->m_parent->height()));
336 }
337 
setVisible(bool visible)338 void KCompletionBox::setVisible(bool visible)
339 {
340     Q_D(KCompletionBox);
341     if (visible) {
342         d->upwardBox = false;
343         if (d->m_parent) {
344             resizeAndReposition();
345             qApp->installEventFilter(this);
346         }
347 
348         // FIXME: Is this comment still valid or can it be deleted? Is a patch already sent to Qt?
349         // Following lines are a workaround for a bug (not sure whose this is):
350         // If this KCompletionBox' parent is in a layout, that layout will detect the
351         // insertion of a new child (posting a ChildInserted event). Then it will trigger relayout
352         // (posting a LayoutHint event).
353         //
354         // QWidget::show() then sends also posted ChildInserted events for the parent,
355         // and later all LayoutHint events, which cause layout updating.
356         // The problem is that KCompletionBox::eventFilter() detects the resizing
357         // of the parent, calls hide() and this hide() happens in the middle
358         // of show(), causing inconsistent state. I'll try to submit a Qt patch too.
359         qApp->sendPostedEvents();
360     } else {
361         if (d->m_parent) {
362             qApp->removeEventFilter(this);
363         }
364         d->cancelText.clear();
365     }
366 
367     QListWidget::setVisible(visible);
368 }
369 
calculateGeometry() const370 QRect KCompletionBox::calculateGeometry() const
371 {
372     Q_D(const KCompletionBox);
373     QRect visualRect;
374     if (count() == 0 || !(visualRect = visualItemRect(item(0))).isValid()) {
375         return QRect();
376     }
377 
378     int x = 0;
379     int y = 0;
380     int ih = visualRect.height();
381     int h = qMin(15 * ih, count() * ih) + 2 * frameWidth();
382 
383     int w = (d->m_parent) ? d->m_parent->width() : QListWidget::minimumSizeHint().width();
384     w = qMax(QListWidget::minimumSizeHint().width(), w);
385     return QRect(x, y, w, h);
386 }
387 
sizeHint() const388 QSize KCompletionBox::sizeHint() const
389 {
390     return calculateGeometry().size();
391 }
392 
down()393 void KCompletionBox::down()
394 {
395     const int row = currentRow();
396     const int lastRow = count() - 1;
397     if (row < lastRow) {
398         setCurrentRow(row + 1);
399         return;
400     }
401 
402     if (lastRow > -1) {
403         setCurrentRow(0);
404     }
405 }
406 
up()407 void KCompletionBox::up()
408 {
409     const int row = currentRow();
410     if (row > 0) {
411         setCurrentRow(row - 1);
412         return;
413     }
414 
415     const int lastRow = count() - 1;
416     if (lastRow > 0) {
417         setCurrentRow(lastRow);
418     }
419 }
420 
pageDown()421 void KCompletionBox::pageDown()
422 {
423     selectionModel()->setCurrentIndex(moveCursor(QAbstractItemView::MovePageDown, Qt::NoModifier), QItemSelectionModel::SelectCurrent);
424 }
425 
pageUp()426 void KCompletionBox::pageUp()
427 {
428     selectionModel()->setCurrentIndex(moveCursor(QAbstractItemView::MovePageUp, Qt::NoModifier), QItemSelectionModel::SelectCurrent);
429 }
430 
home()431 void KCompletionBox::home()
432 {
433     setCurrentRow(0);
434 }
435 
end()436 void KCompletionBox::end()
437 {
438     setCurrentRow(count() - 1);
439 }
440 
setTabHandling(bool enable)441 void KCompletionBox::setTabHandling(bool enable)
442 {
443     Q_D(KCompletionBox);
444     d->tabHandling = enable;
445 }
446 
isTabHandling() const447 bool KCompletionBox::isTabHandling() const
448 {
449     Q_D(const KCompletionBox);
450     return d->tabHandling;
451 }
452 
setCancelledText(const QString & text)453 void KCompletionBox::setCancelledText(const QString &text)
454 {
455     Q_D(KCompletionBox);
456     d->cancelText = text;
457 }
458 
cancelledText() const459 QString KCompletionBox::cancelledText() const
460 {
461     Q_D(const KCompletionBox);
462     return d->cancelText;
463 }
464 
cancelled()465 void KCompletionBoxPrivate::cancelled()
466 {
467     Q_Q(KCompletionBox);
468     if (!cancelText.isNull()) {
469         Q_EMIT q->userCancelled(cancelText);
470     }
471     if (q->isVisible()) {
472         q->hide();
473     }
474 }
475 
476 class KCompletionBoxItem : public QListWidgetItem
477 {
478 public:
479     // Returns true if dirty.
reuse(const QString & newText)480     bool reuse(const QString &newText)
481     {
482         if (text() == newText) {
483             return false;
484         }
485         setText(newText);
486         return true;
487     }
488 };
489 
insertItems(const QStringList & items,int index)490 void KCompletionBox::insertItems(const QStringList &items, int index)
491 {
492     bool block = signalsBlocked();
493     blockSignals(true);
494     QListWidget::insertItems(index, items);
495     blockSignals(block);
496     setCurrentRow(-1);
497 }
498 
setItems(const QStringList & items)499 void KCompletionBox::setItems(const QStringList &items)
500 {
501     bool block = signalsBlocked();
502     blockSignals(true);
503 
504     int rowIndex = 0;
505 
506     if (!count()) {
507         addItems(items);
508     } else {
509         // Keep track of whether we need to change anything,
510         // so we can avoid a repaint for identical updates,
511         // to reduce flicker
512         bool dirty = false;
513 
514         QStringList::ConstIterator it = items.constBegin();
515         const QStringList::ConstIterator itEnd = items.constEnd();
516 
517         for (; it != itEnd; ++it) {
518             if (rowIndex < count()) {
519                 const bool changed = ((KCompletionBoxItem *)item(rowIndex))->reuse(*it);
520                 dirty = dirty || changed;
521             } else {
522                 dirty = true;
523                 // Inserting an item is a way of making this dirty
524                 addItem(*it);
525             }
526             rowIndex++;
527         }
528 
529         // If there is an unused item, mark as dirty -> less items now
530         if (rowIndex < count()) {
531             dirty = true;
532         }
533 
534         // remove unused items with an index >= rowIndex
535         for (; rowIndex < count();) {
536             QListWidgetItem *item = takeItem(rowIndex);
537             Q_ASSERT(item);
538             delete item;
539         }
540     }
541 
542     if (isVisible() && size().height() != sizeHint().height()) {
543         resizeAndReposition();
544     }
545 
546     blockSignals(block);
547 }
548 
_k_itemClicked(QListWidgetItem * item)549 void KCompletionBoxPrivate::_k_itemClicked(QListWidgetItem *item)
550 {
551     Q_Q(KCompletionBox);
552     if (item) {
553         q->hide();
554         Q_EMIT q->currentTextChanged(item->text());
555 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 81)
556         Q_EMIT q->activated(item->text());
557 #endif
558         Q_EMIT q->textActivated(item->text());
559     }
560 }
561 
setActivateOnSelect(bool doEmit)562 void KCompletionBox::setActivateOnSelect(bool doEmit)
563 {
564     Q_D(KCompletionBox);
565     d->emitSelected = doEmit;
566 }
567 
activateOnSelect() const568 bool KCompletionBox::activateOnSelect() const
569 {
570     Q_D(const KCompletionBox);
571     return d->emitSelected;
572 }
573 
574 #include "moc_kcompletionbox.cpp"
575