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"