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