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

source code of kcompletion/src/kcompletionbox.cpp