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