File indexing completed on 2024-05-12 11:47:09
0001 /* 0002 This file is part of the KDE libraries 0003 0004 SPDX-FileCopyrightText: 2000, 2001, 2002 Carsten Pfeiffer <pfeiffer@kde.org> 0005 SPDX-FileCopyrightText: 2000 Stefan Schimanski <1Stein@gmx.de> 0006 SPDX-FileCopyrightText: 2000, 2001, 2002, 2003, 2004 Dawit Alemayehu <adawit@kde.org> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "kcompletionbox.h" 0012 #include "klineedit.h" 0013 0014 #include <QApplication> 0015 #include <QKeyEvent> 0016 #include <QScreen> 0017 #include <QScrollBar> 0018 0019 class KCompletionBoxPrivate 0020 { 0021 public: 0022 KCompletionBoxPrivate(KCompletionBox *parent) 0023 : q_ptr(parent) 0024 { 0025 } 0026 void init(); 0027 void cancelled(); 0028 void _k_itemClicked(QListWidgetItem *); 0029 0030 QWidget *m_parent = nullptr; // necessary to set the focus back 0031 QString cancelText; 0032 bool tabHandling; 0033 bool upwardBox; 0034 bool emitSelected; 0035 0036 KCompletionBox *const q_ptr; 0037 Q_DECLARE_PUBLIC(KCompletionBox) 0038 }; 0039 0040 KCompletionBox::KCompletionBox(QWidget *parent) 0041 : QListWidget(parent) 0042 , d_ptr(new KCompletionBoxPrivate(this)) 0043 { 0044 Q_D(KCompletionBox); 0045 d->m_parent = parent; 0046 d->init(); 0047 } 0048 0049 void KCompletionBoxPrivate::init() 0050 { 0051 Q_Q(KCompletionBox); 0052 tabHandling = true; 0053 upwardBox = false; 0054 emitSelected = true; 0055 0056 // we can't link to QXcbWindowFunctions::Combo 0057 // also, q->setAttribute(Qt::WA_X11NetWmWindowTypeCombo); is broken in Qt xcb 0058 q->setProperty("_q_xcb_wm_window_type", 0x001000); 0059 q->setAttribute(Qt::WA_ShowWithoutActivating); 0060 0061 // on wayland, we need an xdg-popup but we don't want it to grab 0062 // calls setVisible, so must be done after initializations 0063 if (qGuiApp->platformName() == QLatin1String("wayland")) { 0064 q->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint); 0065 } else { 0066 q->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint); 0067 } 0068 q->setUniformItemSizes(true); 0069 0070 q->setLineWidth(1); 0071 q->setFrameStyle(QFrame::Box | QFrame::Plain); 0072 0073 q->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); 0074 q->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0075 0076 q->connect(q, &QListWidget::itemDoubleClicked, q, &KCompletionBox::slotActivated); 0077 q->connect(q, &KCompletionBox::itemClicked, q, [this](QListWidgetItem *item) { 0078 _k_itemClicked(item); 0079 }); 0080 } 0081 0082 KCompletionBox::~KCompletionBox() 0083 { 0084 Q_D(KCompletionBox); 0085 d->m_parent = nullptr; 0086 } 0087 0088 QStringList KCompletionBox::items() const 0089 { 0090 QStringList list; 0091 list.reserve(count()); 0092 for (int i = 0; i < count(); i++) { 0093 const QListWidgetItem *currItem = item(i); 0094 0095 list.append(currItem->text()); 0096 } 0097 0098 return list; 0099 } 0100 0101 void KCompletionBox::slotActivated(QListWidgetItem *item) 0102 { 0103 if (!item) { 0104 return; 0105 } 0106 0107 hide(); 0108 0109 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 81) 0110 Q_EMIT activated(item->text()); 0111 #endif 0112 Q_EMIT textActivated(item->text()); 0113 } 0114 0115 bool KCompletionBox::eventFilter(QObject *o, QEvent *e) 0116 { 0117 Q_D(KCompletionBox); 0118 int type = e->type(); 0119 QWidget *wid = qobject_cast<QWidget *>(o); 0120 0121 if (o == this) { 0122 return false; 0123 } 0124 0125 if (wid && wid == d->m_parent // 0126 && (type == QEvent::Move || type == QEvent::Resize)) { 0127 resizeAndReposition(); 0128 return false; 0129 } 0130 0131 if (wid && (wid->windowFlags() & Qt::Window) // 0132 && type == QEvent::Move && wid == d->m_parent->window()) { 0133 hide(); 0134 return false; 0135 } 0136 0137 if (type == QEvent::MouseButtonPress && (wid && !isAncestorOf(wid))) { 0138 if (!d->emitSelected && currentItem() && !qobject_cast<QScrollBar *>(o)) { 0139 Q_ASSERT(currentItem()); 0140 Q_EMIT currentTextChanged(currentItem()->text()); 0141 } 0142 hide(); 0143 e->accept(); 0144 return true; 0145 } 0146 0147 if (wid && wid->isAncestorOf(d->m_parent) && isVisible()) { 0148 if (type == QEvent::KeyPress) { 0149 QKeyEvent *ev = static_cast<QKeyEvent *>(e); 0150 switch (ev->key()) { 0151 case Qt::Key_Backtab: 0152 if (d->tabHandling && (ev->modifiers() == Qt::NoButton || (ev->modifiers() & Qt::ShiftModifier))) { 0153 up(); 0154 ev->accept(); 0155 return true; 0156 } 0157 break; 0158 case Qt::Key_Tab: 0159 if (d->tabHandling && (ev->modifiers() == Qt::NoButton)) { 0160 down(); 0161 // #65877: Key_Tab should complete using the first 0162 // (or selected) item, and then offer completions again 0163 if (count() == 1) { 0164 KLineEdit *parent = qobject_cast<KLineEdit *>(d->m_parent); 0165 if (parent) { 0166 parent->doCompletion(currentItem()->text()); 0167 } else { 0168 hide(); 0169 } 0170 } 0171 ev->accept(); 0172 return true; 0173 } 0174 break; 0175 case Qt::Key_Down: 0176 down(); 0177 ev->accept(); 0178 return true; 0179 case Qt::Key_Up: 0180 // If there is no selected item and we've popped up above 0181 // our parent, select the first item when they press up. 0182 if (!selectedItems().isEmpty() // 0183 || mapToGlobal(QPoint(0, 0)).y() > d->m_parent->mapToGlobal(QPoint(0, 0)).y()) { 0184 up(); 0185 } else { 0186 down(); 0187 } 0188 ev->accept(); 0189 return true; 0190 case Qt::Key_PageUp: 0191 pageUp(); 0192 ev->accept(); 0193 return true; 0194 case Qt::Key_PageDown: 0195 pageDown(); 0196 ev->accept(); 0197 return true; 0198 case Qt::Key_Escape: 0199 d->cancelled(); 0200 ev->accept(); 0201 return true; 0202 case Qt::Key_Enter: 0203 case Qt::Key_Return: 0204 if (ev->modifiers() & Qt::ShiftModifier) { 0205 hide(); 0206 ev->accept(); // Consume the Enter event 0207 return true; 0208 } 0209 break; 0210 case Qt::Key_End: 0211 if (ev->modifiers() & Qt::ControlModifier) { 0212 end(); 0213 ev->accept(); 0214 return true; 0215 } 0216 break; 0217 case Qt::Key_Home: 0218 if (ev->modifiers() & Qt::ControlModifier) { 0219 home(); 0220 ev->accept(); 0221 return true; 0222 } 0223 Q_FALLTHROUGH(); 0224 default: 0225 break; 0226 } 0227 } else if (type == QEvent::ShortcutOverride) { 0228 // Override any accelerators that match 0229 // the key sequences we use here... 0230 QKeyEvent *ev = static_cast<QKeyEvent *>(e); 0231 switch (ev->key()) { 0232 case Qt::Key_Down: 0233 case Qt::Key_Up: 0234 case Qt::Key_PageUp: 0235 case Qt::Key_PageDown: 0236 case Qt::Key_Escape: 0237 case Qt::Key_Enter: 0238 case Qt::Key_Return: 0239 ev->accept(); 0240 return true; 0241 case Qt::Key_Tab: 0242 case Qt::Key_Backtab: 0243 if (ev->modifiers() == Qt::NoButton || (ev->modifiers() & Qt::ShiftModifier)) { 0244 ev->accept(); 0245 return true; 0246 } 0247 break; 0248 case Qt::Key_Home: 0249 case Qt::Key_End: 0250 if (ev->modifiers() & Qt::ControlModifier) { 0251 ev->accept(); 0252 return true; 0253 } 0254 break; 0255 default: 0256 break; 0257 } 0258 } else if (type == QEvent::FocusOut) { 0259 QFocusEvent *event = static_cast<QFocusEvent *>(e); 0260 if (event->reason() != Qt::PopupFocusReason 0261 #ifdef Q_OS_WIN 0262 && (event->reason() != Qt::ActiveWindowFocusReason || QApplication::activeWindow() != this) 0263 #endif 0264 ) { 0265 hide(); 0266 } 0267 } 0268 } 0269 0270 return QListWidget::eventFilter(o, e); 0271 } 0272 0273 void KCompletionBox::popup() 0274 { 0275 if (count() == 0) { 0276 hide(); 0277 } else { 0278 bool block = signalsBlocked(); 0279 blockSignals(true); 0280 setCurrentRow(-1); 0281 blockSignals(block); 0282 clearSelection(); 0283 if (!isVisible()) { 0284 show(); 0285 } else if (size().height() != sizeHint().height()) { 0286 resizeAndReposition(); 0287 } 0288 } 0289 } 0290 0291 void KCompletionBox::resizeAndReposition() 0292 { 0293 Q_D(KCompletionBox); 0294 int currentGeom = height(); 0295 QPoint currentPos = pos(); 0296 QRect geom = calculateGeometry(); 0297 resize(geom.size()); 0298 0299 int x = currentPos.x(); 0300 int y = currentPos.y(); 0301 if (d->m_parent) { 0302 if (!isVisible()) { 0303 const QPoint orig = globalPositionHint(); 0304 QScreen *screen = QGuiApplication::screenAt(orig); 0305 if (screen) { 0306 const QRect screenSize = screen->geometry(); 0307 0308 x = orig.x() + geom.x(); 0309 y = orig.y() + geom.y(); 0310 0311 if (x + width() > screenSize.right()) { 0312 x = screenSize.right() - width(); 0313 } 0314 if (y + height() > screenSize.bottom()) { 0315 y = y - height() - d->m_parent->height(); 0316 d->upwardBox = true; 0317 } 0318 } 0319 } else { 0320 // Are we above our parent? If so we must keep bottom edge anchored. 0321 if (d->upwardBox) { 0322 y += (currentGeom - height()); 0323 } 0324 } 0325 move(x, y); 0326 } 0327 } 0328 0329 QPoint KCompletionBox::globalPositionHint() const 0330 { 0331 Q_D(const KCompletionBox); 0332 if (!d->m_parent) { 0333 return QPoint(); 0334 } 0335 return d->m_parent->mapToGlobal(QPoint(0, d->m_parent->height())); 0336 } 0337 0338 void KCompletionBox::setVisible(bool visible) 0339 { 0340 Q_D(KCompletionBox); 0341 if (visible) { 0342 d->upwardBox = false; 0343 if (d->m_parent) { 0344 resizeAndReposition(); 0345 qApp->installEventFilter(this); 0346 } 0347 0348 // FIXME: Is this comment still valid or can it be deleted? Is a patch already sent to Qt? 0349 // Following lines are a workaround for a bug (not sure whose this is): 0350 // If this KCompletionBox' parent is in a layout, that layout will detect the 0351 // insertion of a new child (posting a ChildInserted event). Then it will trigger relayout 0352 // (posting a LayoutHint event). 0353 // 0354 // QWidget::show() then sends also posted ChildInserted events for the parent, 0355 // and later all LayoutHint events, which cause layout updating. 0356 // The problem is that KCompletionBox::eventFilter() detects the resizing 0357 // of the parent, calls hide() and this hide() happens in the middle 0358 // of show(), causing inconsistent state. I'll try to submit a Qt patch too. 0359 qApp->sendPostedEvents(); 0360 } else { 0361 if (d->m_parent) { 0362 qApp->removeEventFilter(this); 0363 } 0364 d->cancelText.clear(); 0365 } 0366 0367 QListWidget::setVisible(visible); 0368 } 0369 0370 QRect KCompletionBox::calculateGeometry() const 0371 { 0372 Q_D(const KCompletionBox); 0373 QRect visualRect; 0374 if (count() == 0 || !(visualRect = visualItemRect(item(0))).isValid()) { 0375 return QRect(); 0376 } 0377 0378 int x = 0; 0379 int y = 0; 0380 int ih = visualRect.height(); 0381 int h = qMin(15 * ih, count() * ih) + 2 * frameWidth(); 0382 0383 int w = (d->m_parent) ? d->m_parent->width() : QListWidget::minimumSizeHint().width(); 0384 w = qMax(QListWidget::minimumSizeHint().width(), w); 0385 return QRect(x, y, w, h); 0386 } 0387 0388 QSize KCompletionBox::sizeHint() const 0389 { 0390 return calculateGeometry().size(); 0391 } 0392 0393 void KCompletionBox::down() 0394 { 0395 const int row = currentRow(); 0396 const int lastRow = count() - 1; 0397 if (row < lastRow) { 0398 setCurrentRow(row + 1); 0399 return; 0400 } 0401 0402 if (lastRow > -1) { 0403 setCurrentRow(0); 0404 } 0405 } 0406 0407 void KCompletionBox::up() 0408 { 0409 const int row = currentRow(); 0410 if (row > 0) { 0411 setCurrentRow(row - 1); 0412 return; 0413 } 0414 0415 const int lastRow = count() - 1; 0416 if (lastRow > 0) { 0417 setCurrentRow(lastRow); 0418 } 0419 } 0420 0421 void KCompletionBox::pageDown() 0422 { 0423 selectionModel()->setCurrentIndex(moveCursor(QAbstractItemView::MovePageDown, Qt::NoModifier), QItemSelectionModel::SelectCurrent); 0424 } 0425 0426 void KCompletionBox::pageUp() 0427 { 0428 selectionModel()->setCurrentIndex(moveCursor(QAbstractItemView::MovePageUp, Qt::NoModifier), QItemSelectionModel::SelectCurrent); 0429 } 0430 0431 void KCompletionBox::home() 0432 { 0433 setCurrentRow(0); 0434 } 0435 0436 void KCompletionBox::end() 0437 { 0438 setCurrentRow(count() - 1); 0439 } 0440 0441 void KCompletionBox::setTabHandling(bool enable) 0442 { 0443 Q_D(KCompletionBox); 0444 d->tabHandling = enable; 0445 } 0446 0447 bool KCompletionBox::isTabHandling() const 0448 { 0449 Q_D(const KCompletionBox); 0450 return d->tabHandling; 0451 } 0452 0453 void KCompletionBox::setCancelledText(const QString &text) 0454 { 0455 Q_D(KCompletionBox); 0456 d->cancelText = text; 0457 } 0458 0459 QString KCompletionBox::cancelledText() const 0460 { 0461 Q_D(const KCompletionBox); 0462 return d->cancelText; 0463 } 0464 0465 void KCompletionBoxPrivate::cancelled() 0466 { 0467 Q_Q(KCompletionBox); 0468 if (!cancelText.isNull()) { 0469 Q_EMIT q->userCancelled(cancelText); 0470 } 0471 if (q->isVisible()) { 0472 q->hide(); 0473 } 0474 } 0475 0476 class KCompletionBoxItem : public QListWidgetItem 0477 { 0478 public: 0479 // Returns true if dirty. 0480 bool reuse(const QString &newText) 0481 { 0482 if (text() == newText) { 0483 return false; 0484 } 0485 setText(newText); 0486 return true; 0487 } 0488 }; 0489 0490 void KCompletionBox::insertItems(const QStringList &items, int index) 0491 { 0492 bool block = signalsBlocked(); 0493 blockSignals(true); 0494 QListWidget::insertItems(index, items); 0495 blockSignals(block); 0496 setCurrentRow(-1); 0497 } 0498 0499 void KCompletionBox::setItems(const QStringList &items) 0500 { 0501 bool block = signalsBlocked(); 0502 blockSignals(true); 0503 0504 int rowIndex = 0; 0505 0506 if (!count()) { 0507 addItems(items); 0508 } else { 0509 // Keep track of whether we need to change anything, 0510 // so we can avoid a repaint for identical updates, 0511 // to reduce flicker 0512 bool dirty = false; 0513 0514 QStringList::ConstIterator it = items.constBegin(); 0515 const QStringList::ConstIterator itEnd = items.constEnd(); 0516 0517 for (; it != itEnd; ++it) { 0518 if (rowIndex < count()) { 0519 const bool changed = ((KCompletionBoxItem *)item(rowIndex))->reuse(*it); 0520 dirty = dirty || changed; 0521 } else { 0522 dirty = true; 0523 // Inserting an item is a way of making this dirty 0524 addItem(*it); 0525 } 0526 rowIndex++; 0527 } 0528 0529 // If there is an unused item, mark as dirty -> less items now 0530 if (rowIndex < count()) { 0531 dirty = true; 0532 } 0533 0534 // remove unused items with an index >= rowIndex 0535 for (; rowIndex < count();) { 0536 QListWidgetItem *item = takeItem(rowIndex); 0537 Q_ASSERT(item); 0538 delete item; 0539 } 0540 } 0541 0542 if (isVisible() && size().height() != sizeHint().height()) { 0543 resizeAndReposition(); 0544 } 0545 0546 blockSignals(block); 0547 } 0548 0549 void KCompletionBoxPrivate::_k_itemClicked(QListWidgetItem *item) 0550 { 0551 Q_Q(KCompletionBox); 0552 if (item) { 0553 q->hide(); 0554 Q_EMIT q->currentTextChanged(item->text()); 0555 #if KCOMPLETION_BUILD_DEPRECATED_SINCE(5, 81) 0556 Q_EMIT q->activated(item->text()); 0557 #endif 0558 Q_EMIT q->textActivated(item->text()); 0559 } 0560 } 0561 0562 void KCompletionBox::setActivateOnSelect(bool doEmit) 0563 { 0564 Q_D(KCompletionBox); 0565 d->emitSelected = doEmit; 0566 } 0567 0568 bool KCompletionBox::activateOnSelect() const 0569 { 0570 Q_D(const KCompletionBox); 0571 return d->emitSelected; 0572 } 0573 0574 #include "moc_kcompletionbox.cpp"