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

KDE's Doxygen guidelines are available online.