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: |
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 | |
29 | KCompletionBox::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 | |
65 | KCompletionBox::~KCompletionBox() |
66 | { |
67 | d->m_parent = nullptr; |
68 | } |
69 | |
70 | QStringList 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 | |
83 | void KCompletionBox::slotActivated(QListWidgetItem *item) |
84 | { |
85 | if (item) { |
86 | hide(); |
87 | Q_EMIT textActivated(text: item->text()); |
88 | } |
89 | } |
90 | |
91 | bool 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 | |
253 | void KCompletionBox::() |
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 | |
271 | void 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 | |
308 | QPoint 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 | |
316 | void 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 | |
347 | QRect 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 | |
364 | QSize KCompletionBox::sizeHint() const |
365 | { |
366 | return calculateGeometry().size(); |
367 | } |
368 | |
369 | void 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 | |
383 | void 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 | |
397 | void KCompletionBox::pageDown() |
398 | { |
399 | selectionModel()->setCurrentIndex(index: moveCursor(cursorAction: QAbstractItemView::MovePageDown, modifiers: Qt::NoModifier), command: QItemSelectionModel::SelectCurrent); |
400 | } |
401 | |
402 | void KCompletionBox::pageUp() |
403 | { |
404 | selectionModel()->setCurrentIndex(index: moveCursor(cursorAction: QAbstractItemView::MovePageUp, modifiers: Qt::NoModifier), command: QItemSelectionModel::SelectCurrent); |
405 | } |
406 | |
407 | void KCompletionBox::home() |
408 | { |
409 | setCurrentRow(0); |
410 | } |
411 | |
412 | void KCompletionBox::end() |
413 | { |
414 | setCurrentRow(count() - 1); |
415 | } |
416 | |
417 | void KCompletionBox::setTabHandling(bool enable) |
418 | { |
419 | d->tabHandling = enable; |
420 | } |
421 | |
422 | bool KCompletionBox::isTabHandling() const |
423 | { |
424 | return d->tabHandling; |
425 | } |
426 | |
427 | void KCompletionBox::setCancelledText(const QString &text) |
428 | { |
429 | d->cancelText = text; |
430 | } |
431 | |
432 | QString KCompletionBox::cancelledText() const |
433 | { |
434 | return d->cancelText; |
435 | } |
436 | |
437 | void 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 | |
446 | void 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 | |
483 | void KCompletionBox::setActivateOnSelect(bool doEmit) |
484 | { |
485 | d->emitSelected = doEmit; |
486 | } |
487 | |
488 | bool KCompletionBox::activateOnSelect() const |
489 | { |
490 | return d->emitSelected; |
491 | } |
492 | |
493 | #include "moc_kcompletionbox.cpp" |
494 | |