File indexing completed on 2024-05-12 15:34:00

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"