KCompletion

kcompletionbox.cpp
1 /*
2  This file is part of the KDE libraries
3 
4  SPDX-FileCopyrightText: 2000, 2001, 2002 Carsten Pfeiffer <[email protected]>
5  SPDX-FileCopyrightText: 2000 Stefan Schimanski <[email protected]>
6  SPDX-FileCopyrightText: 2000, 2001, 2002, 2003, 2004 Dawit Alemayehu <[email protected]>
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  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 
41  : QListWidget(parent)
42  , d_ptr(new KCompletionBoxPrivate(this))
43 {
45  d->m_parent = parent;
46  d->init();
47 }
48 
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")) {
65  } else {
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 
77  q->connect(q, &KCompletionBox::itemClicked, q, [this](QListWidgetItem *item) {
78  _k_itemClicked(item);
79  });
80 }
81 
83 {
85  d->m_parent = nullptr;
86 }
87 
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 
102 {
103  if (!item) {
104  return;
105  }
106 
107  hide();
108 
109 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 81)
111 #endif
113 }
114 
116 {
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)) {
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());
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 
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()) {
287  }
288  }
289 }
290 
292 {
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();
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 
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 
338 void KCompletionBox::setVisible(bool visible)
339 {
341  if (visible) {
342  d->upwardBox = false;
343  if (d->m_parent) {
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 
368 }
369 
371 {
372  Q_D(const KCompletionBox);
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 
388 QSize KCompletionBox::sizeHint() const
389 {
390  return calculateGeometry().size();
391 }
392 
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 
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 
422 {
424 }
425 
427 {
429 }
430 
432 {
433  setCurrentRow(0);
434 }
435 
437 {
438  setCurrentRow(count() - 1);
439 }
440 
442 {
444  d->tabHandling = enable;
445 }
446 
447 bool KCompletionBox::isTabHandling() const
448 {
449  Q_D(const KCompletionBox);
450  return d->tabHandling;
451 }
452 
454 {
456  d->cancelText = text;
457 }
458 
459 QString KCompletionBox::cancelledText() const
460 {
461  Q_D(const KCompletionBox);
462  return d->cancelText;
463 }
464 
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.
480  bool reuse(const QString &newText)
481  {
482  if (text() == newText) {
483  return false;
484  }
485  setText(newText);
486  return true;
487  }
488 };
489 
490 void KCompletionBox::insertItems(const QStringList &items, int index)
491 {
492  bool block = signalsBlocked();
493  blockSignals(true);
495  blockSignals(block);
496  setCurrentRow(-1);
497 }
498 
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 
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()) {
544  }
545 
546  blockSignals(block);
547 }
548 
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 
563 {
565  d->emitSelected = doEmit;
566 }
567 
568 bool KCompletionBox::activateOnSelect() const
569 {
570  Q_D(const KCompletionBox);
571  return d->emitSelected;
572 }
573 
574 #include "moc_kcompletionbox.cpp"
virtual void popup()
Adjusts the size of the box to fit the width of the parent given in the constructor and pops it up at...
void append(const T &value)
QRect calculateGeometry() const
This calculates the size of the dropdown and the relative position of the top left corner with respec...
QString text() const const
virtual bool event(QEvent *e) override
void activated(const QString &text)
Emitted when an item is selected, text is the text of the selected item.
QWidget * window() const const
virtual void setCurrentIndex(const QModelIndex &index, QItemSelectionModel::SelectionFlags command)
void setCancelledText(const QString &text)
Sets the text to be emitted if the user chooses not to pick from the available matches.
void pageUp()
Moves the selection one page up.
void end()
Moves the selection down to the last item.
void setVisible(bool visible) override
Reimplemented for internal reasons.
QSize size() const const
QListWidgetItem * item(int row) const const
int right() const const
Q_EMITQ_EMIT
QCA_EXPORT void init()
bool isAncestorOf(const QWidget *child) const const
QItemSelectionModel * selectionModel() const const
void itemClicked(QListWidgetItem *item)
QScreen * screen() const const
int x() const const
int y() const const
QWidget * activeWindow()
ScrollBarAsNeeded
virtual QModelIndex moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override
QList::const_iterator constBegin() const const
int x() const const
int y() const const
int width() const const
QListWidgetItem * takeItem(int row)
NoButton
QStringList items() const
Returns a list of all items currently in the box.
~KCompletionBox() override
Destroys the box.
QRect visualItemRect(const QListWidgetItem *item) const const
void home()
Moves the selection up to the first item.
void hide()
void reserve(int alloc)
bool isVisible() const const
bool signalsBlocked() const const
virtual QRect visualRect(const QModelIndex &index) const const override
Qt::KeyboardModifiers modifiers() const const
int bottom() const const
void setItems(const QStringList &items)
Clears the box and inserts items.
PopupFocusReason
void insertItems(int row, const QStringList &labels)
void currentTextChanged(const QString &currentText)
bool blockSignals(bool block)
void up()
Moves the selection one line up or select the first item if nothing is selected yet.
void setActivateOnSelect(bool doEmit)
Set whether or not the selected signal should be emitted when an item is selected.
KCompletionBox(QWidget *parent=nullptr)
Constructs a KCompletionBox.
virtual QSize minimumSizeHint() const const override
bool eventFilter(QObject *, QEvent *) override
Reimplemented from QListWidget to get events from the viewport (to hide this widget on mouse-click,...
QPoint mapToGlobal(const QPoint &pos) const const
QList< QListWidgetItem * > selectedItems() const const
A helper widget for "completion-widgets" (KLineEdit, KComboBox))
Key_Backtab
QListWidgetItem * currentItem() const const
QScreen * screenAt(const QPoint &point)
void show()
virtual bool eventFilter(QObject *object, QEvent *event) override
typedef ConstIterator
void setTabHandling(bool enable)
Makes this widget (when visible) capture Tab-key events to traverse the items in the dropdown list (T...
void addItems(const QStringList &labels)
void insertItems(const QStringList &items, int index=-1)
Inserts items into the box.
void resize(int w, int h)
void itemDoubleClicked(QListWidgetItem *item)
int height() const const
QList::const_iterator constEnd() const const
int key() const const
QEvent::Type type() const const
void addItem(const QString &label)
virtual QPoint globalPositionHint() const
The preferred global coordinate at which the completion box's top left corner should be positioned.
void pageDown()
Moves the selection one page down.
void move(int x, int y)
int row(const QListWidgetItem *item) const const
void setText(const QString &text)
void resizeAndReposition()
This properly resizes and repositions the listbox.
void textActivated(const QString &text)
Emitted when an item is selected, text is the text of the selected item.
ShiftModifier
virtual void slotActivated(QListWidgetItem *)
Called when an item is activated.
void setCurrentRow(int row)
QObject * parent() const const
void down()
Moves the selection one line down or select the first item if nothing is selected yet.
WA_ShowWithoutActivating
Q_D(Todo)
void accept()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Mon Jun 27 2022 04:07:52 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.