diff --git a/autotests/kcombobox_unittest.cpp b/autotests/kcombobox_unittest.cpp index a4ae551..4c4e2b2 100644 --- a/autotests/kcombobox_unittest.cpp +++ b/autotests/kcombobox_unittest.cpp @@ -1,217 +1,227 @@ /* This file is part of the KDE libraries Copyright (c) 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include class KTestComboBox : public KComboBox { public: KTestComboBox(bool rw, QWidget *parent = nullptr) : KComboBox(rw, parent) {} KCompletionBase *delegate() const { return KCompletionBase::delegate(); } }; class KComboBox_UnitTest : public QObject { Q_OBJECT private: void testComboReturnPressed(bool ctorArg) { KComboBox w(ctorArg /*initial value for editable*/); w.setEditable(true); w.setCompletionMode(KCompletion::CompletionPopup); w.addItem(QStringLiteral("Hello world")); QVERIFY(w.lineEdit()); QVERIFY(qobject_cast(w.lineEdit())); auto lineEdit = w.lineEdit(); // set editable again, don't recreate the line edit w.setEditable(true); QCOMPARE(w.lineEdit(), lineEdit); // KLineEdit signals QSignalSpy qReturnPressedSpy(w.lineEdit(), SIGNAL(returnPressed())); QSignalSpy kReturnPressedSpy(w.lineEdit(), SIGNAL(returnPressed(QString))); // KComboBox signals QSignalSpy comboReturnPressedSpy(&w, SIGNAL(returnPressed())); QSignalSpy comboReturnPressedStringSpy(&w, SIGNAL(returnPressed(QString))); +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) QSignalSpy comboActivatedSpy(&w, SIGNAL(activated(QString))); +#else + QSignalSpy comboActivatedSpy(&w, &QComboBox::textActivated); +#endif QTest::keyClick(&w, Qt::Key_Return); QCOMPARE(qReturnPressedSpy.count(), 1); QCOMPARE(kReturnPressedSpy.count(), 1); QCOMPARE(kReturnPressedSpy[0][0].toString(), QString("Hello world")); QCOMPARE(comboReturnPressedSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy[0][0].toString(), QString("Hello world")); QCOMPARE(comboActivatedSpy.count(), 1); QCOMPARE(comboActivatedSpy[0][0].toString(), QString("Hello world")); } private Q_SLOTS: void testComboReturnPressed() { testComboReturnPressed(false); testComboReturnPressed(true); } void testHistoryComboReturnPressed() { KHistoryComboBox w; QVERIFY(qobject_cast(w.lineEdit())); - connect(&w, SIGNAL(activated(QString)), - &w, SLOT(addToHistory(QString))); QSignalSpy comboReturnPressedSpy(&w, SIGNAL(returnPressed())); QSignalSpy comboReturnPressedStringSpy(&w, SIGNAL(returnPressed(QString))); +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + connect(&w, SIGNAL(activated(QString)), + &w, SLOT(addToHistory(QString))); QSignalSpy comboActivatedSpy(&w, SIGNAL(activated(QString))); +#else + connect(&w, &KHistoryComboBox::textActivated, + &w, &KHistoryComboBox::addToHistory); + QSignalSpy comboActivatedSpy(&w, &QComboBox::textActivated); +#endif QTest::keyClicks(&w, QStringLiteral("Hello world")); QTest::keyClick(&w, Qt::Key_Return); qApp->processEvents(); // QueuedConnection in KHistoryComboBox QCOMPARE(comboReturnPressedSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy[0][0].toString(), QString("Hello world")); QCOMPARE(comboActivatedSpy.count(), 1); QCOMPARE(comboActivatedSpy[0][0].toString(), QString("Hello world")); } void testHistoryComboKeyUp() { KHistoryComboBox w; QStringList items; items << QStringLiteral("One") << QStringLiteral("Two") << QStringLiteral("Three") << QStringLiteral("Four"); w.addItems(items); QSignalSpy currentIndexChangedSpy(&w, SIGNAL(currentIndexChanged(int))); w.completionObject()->setItems(items); QCOMPARE(w.currentIndex(), 0); QTest::keyClick(&w, Qt::Key_Up); QCOMPARE(w.currentIndex(), 1); QCOMPARE(currentIndexChangedSpy.count(), 1); QCOMPARE(currentIndexChangedSpy[0][0].toInt(), 1); } void testHistoryComboInsertItems() { KHistoryComboBox combo; // uic generates code like this, let's make sure it compiles combo.insertItems(0, QStringList() << QStringLiteral("foo")); } void testHistoryComboReset() { //It only tests that it doesn't crash //TODO: Finish KHistoryComboBox combo; QStringList items; items << QStringLiteral("One") << QStringLiteral("Two"); combo.addItems(items); combo.reset(); } void testDeleteLineEdit() { // Test for KCombo's KLineEdit destruction KTestComboBox *testCombo = new KTestComboBox(true, nullptr); // rw, with KLineEdit testCombo->setEditable(false); // destroys our KLineEdit, with deleteLater qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); QVERIFY(testCombo->KTestComboBox::delegate() == nullptr); delete testCombo; // not needed anymore } void testLineEditCompletion() { QFETCH(bool, editable); QPointer completion; QPointer lineEdit, lineEdit2; { // Test for KCombo's KLineEdit inheriting the completion object of the parent KTestComboBox testCombo(editable, nullptr); // we only have a line edit when we are editable already QCOMPARE(static_cast(testCombo.lineEdit()), editable); // we can always get a completion object // NOTE: for an editable combo, this refers to the completion object of // the internal line edit // NOTE: for a not-yet-editable combo, this refers to the completion object // that belongs to the combo directly completion = testCombo.completionObject(); QVERIFY(completion); // make editable testCombo.setEditable(true); QVERIFY(completion); // completion is still alive // verify that the completion object was set on the line edit lineEdit = qobject_cast(testCombo.lineEdit()); QVERIFY(lineEdit); QVERIFY(lineEdit->compObj()); QCOMPARE(lineEdit->compObj(), completion.data()); QCOMPARE(testCombo.completionObject(), completion.data()); // don't lose the completion and don't crash when we set a new line edit // NOTE: only reuse the completion when it belongs to the combo box lineEdit2 = new KLineEdit(&testCombo); QVERIFY(!lineEdit2->compObj()); testCombo.setLineEdit(lineEdit2); QVERIFY(!lineEdit); // first line edit got deleted now if (editable) { QVERIFY(!completion); // got deleted with the line edit // but we get a new one from the second line edit completion = testCombo.completionObject(); } QVERIFY(completion); QCOMPARE(lineEdit2->compObj(), completion.data()); QCOMPARE(testCombo.completionObject(), completion.data()); } // ensure nothing gets leaked QVERIFY(!completion); QVERIFY(!lineEdit2); } void testLineEditCompletion_data() { QTest::addColumn("editable"); QTest::newRow("deferred-editable") << false; QTest::newRow("editable") << true; } void testSelectionResetOnReturn() { // void QComboBoxPrivate::_q_returnPressed() calls lineEdit->deselect() KHistoryComboBox *testCombo = new KHistoryComboBox(true, nullptr); QCOMPARE(testCombo->insertPolicy(), QComboBox::NoInsert); // not the Qt default; KHistoryComboBox changes that QTest::keyClicks(testCombo, QStringLiteral("Hello world")); testCombo->lineEdit()->setSelection(5, 3); QVERIFY(testCombo->lineEdit()->hasSelectedText()); QTest::keyClick(testCombo, Qt::Key_Return); // Changed in Qt5: it only does that if insertionPolicy isn't NoInsert. // Should we add a lineEdit()->deselect() in KHistoryComboBox? Why does this matter? QEXPECT_FAIL("", "Qt5: QComboBox doesn't deselect text anymore on returnPressed", Continue); QVERIFY(!testCombo->lineEdit()->hasSelectedText()); } }; QTEST_MAIN(KComboBox_UnitTest) #include "kcombobox_unittest.moc" diff --git a/src/kcombobox.cpp b/src/kcombobox.cpp index 0dd220f..0940fa4 100644 --- a/src/kcombobox.cpp +++ b/src/kcombobox.cpp @@ -1,406 +1,412 @@ /* This file is part of the KDE libraries Copyright (c) 2000,2001 Dawit Alemayehu Copyright (c) 2000,2001 Carsten Pfeiffer Copyright (c) 2000 Stefan Schimanski <1Stein@gmx.de> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcombobox.h" #include #include #include #include #include class KComboBoxPrivate { public: KComboBoxPrivate(KComboBox *parent) : q_ptr(parent) { } ~KComboBoxPrivate() { } /** * Initializes the variables upon construction. */ void init(); void _k_lineEditDeleted(); KLineEdit *klineEdit = nullptr; bool trapReturnKey = false; KComboBox * const q_ptr; Q_DECLARE_PUBLIC(KComboBox) }; void KComboBoxPrivate::init() { Q_Q(KComboBox); - q->QComboBox::setAutoCompletion(false); // otherwise setLineEdit will create a completer... if (q->isEditable()) { - q->setCompleter(nullptr); + q->setCompleter(nullptr); // remove the builtin completer, we have our own q->lineEdit()->setContextMenuPolicy(Qt::DefaultContextMenu); } } void KComboBoxPrivate::_k_lineEditDeleted() { Q_Q(KComboBox); // yes, we need those ugly casts due to the multiple inheritance // sender() is guaranteed to be a KLineEdit (see the connect() to the // destroyed() signal const KCompletionBase *base = static_cast(static_cast(q->sender())); // is it our delegate, that is destroyed? if (base == q->delegate()) { q->setDelegate(nullptr); } } KComboBox::KComboBox(QWidget *parent) : QComboBox(parent), d_ptr(new KComboBoxPrivate(this)) { Q_D(KComboBox); d->init(); } KComboBox::KComboBox(bool rw, QWidget *parent) : QComboBox(parent), d_ptr(new KComboBoxPrivate(this)) { Q_D(KComboBox); d->init(); setEditable(rw); } KComboBox::~KComboBox() { } bool KComboBox::contains(const QString &text) const { if (text.isEmpty()) { return false; } const int itemCount = count(); for (int i = 0; i < itemCount; ++i) { if (itemText(i) == text) { return true; } } return false; } int KComboBox::cursorPosition() const { return (isEditable()) ? lineEdit()->cursorPosition() : -1; } void KComboBox::setAutoCompletion(bool autocomplete) { Q_D(KComboBox); if (d->klineEdit) { if (autocomplete) { d->klineEdit->setCompletionMode(KCompletion::CompletionAuto); setCompletionMode(KCompletion::CompletionAuto); } else { d->klineEdit->setCompletionMode(KCompletion::CompletionPopup); setCompletionMode(KCompletion::CompletionPopup); } } } bool KComboBox::autoCompletion() const { return completionMode() == KCompletion::CompletionAuto; } #ifndef KCOMPLETION_NO_DEPRECATED void KComboBox::setContextMenuEnabled(bool showMenu) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setContextMenuPolicy(showMenu ? Qt::DefaultContextMenu : Qt::NoContextMenu); } } void KComboBox::setUrlDropsEnabled(bool enable) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setUrlDropsEnabled(enable); } } #endif bool KComboBox::urlDropsEnabled() const { Q_D(const KComboBox); return d->klineEdit && d->klineEdit->urlDropsEnabled(); } void KComboBox::setCompletedText(const QString &text, bool marked) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setCompletedText(text, marked); } } void KComboBox::setCompletedText(const QString &text) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setCompletedText(text); } } void KComboBox::makeCompletion(const QString &text) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->makeCompletion(text); } else { // read-only combo completion if (text.isNull() || !view()) { return; } view()->keyboardSearch(text); } } void KComboBox::rotateText(KCompletionBase::KeyBindingType type) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->rotateText(type); } } void KComboBox::setTrapReturnKey(bool trap) { Q_D(KComboBox); d->trapReturnKey = trap; if (d->klineEdit) { d->klineEdit->setTrapReturnKey(trap); } else { qWarning("KComboBox::setTrapReturnKey not supported with a non-KLineEdit."); } } bool KComboBox::trapReturnKey() const { Q_D(const KComboBox); return d->trapReturnKey; } void KComboBox::setEditUrl(const QUrl &url) { QComboBox::setEditText(url.toDisplayString()); } void KComboBox::addUrl(const QUrl &url) { QComboBox::addItem(url.toDisplayString()); } void KComboBox::addUrl(const QIcon &icon, const QUrl &url) { QComboBox::addItem(icon, url.toDisplayString()); } void KComboBox::insertUrl(int index, const QUrl &url) { QComboBox::insertItem(index, url.toDisplayString()); } void KComboBox::insertUrl(int index, const QIcon &icon, const QUrl &url) { QComboBox::insertItem(index, icon, url.toDisplayString()); } void KComboBox::changeUrl(int index, const QUrl &url) { QComboBox::setItemText(index, url.toDisplayString()); } void KComboBox::changeUrl(int index, const QIcon &icon, const QUrl &url) { QComboBox::setItemIcon(index, icon); QComboBox::setItemText(index, url.toDisplayString()); } void KComboBox::setCompletedItems(const QStringList &items, bool autosubject) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setCompletedItems(items, autosubject); } } KCompletionBox *KComboBox::completionBox(bool create) { Q_D(KComboBox); if (d->klineEdit) { return d->klineEdit->completionBox(create); } return nullptr; } QSize KComboBox::minimumSizeHint() const { Q_D(const KComboBox); QSize size = QComboBox::minimumSizeHint(); if (isEditable() && d->klineEdit) { // if it's a KLineEdit and it's editable add the clear button size // to the minimum size hint, otherwise looks ugly because the // clear button will cover the last 2/3 letters of the biggest entry QSize bs = d->klineEdit->clearButtonUsedSize(); if (bs.isValid()) { size.rwidth() += bs.width(); size.rheight() = qMax(size.height(), bs.height()); } } return size; } void KComboBox::setLineEdit(QLineEdit *edit) { Q_D(KComboBox); if (!isEditable() && edit && !qstrcmp(edit->metaObject()->className(), "QLineEdit")) { // uic generates code that creates a read-only KComboBox and then // calls combo->setEditable(true), which causes QComboBox to set up // a dumb QLineEdit instead of our nice KLineEdit. // As some KComboBox features rely on the KLineEdit, we reject // this order here. delete edit; KLineEdit *kedit = new KLineEdit(this); if (isEditable()) { kedit->setClearButtonEnabled(true); } edit = kedit; } // reuse an existing completion object, if it does not belong to the previous // line edit and gets destroyed with it QPointer completion = compObj(); QComboBox::setLineEdit(edit); d->klineEdit = qobject_cast(edit); setDelegate(d->klineEdit); if (completion && d->klineEdit) { d->klineEdit->setCompletionObject(completion); } // Connect the returnPressed signal for both Q[K]LineEdits' if (edit) { connect(edit, QOverload<>::of(&QLineEdit::returnPressed), this, QOverload<>::of(&KComboBox::returnPressed)); } if (d->klineEdit) { // someone calling KComboBox::setEditable(false) destroys our // line edit without us noticing. And KCompletionBase::delegate would // be a dangling pointer then, so prevent that. Note: only do this // when it is a KLineEdit! connect(edit, SIGNAL(destroyed()), SLOT(_k_lineEditDeleted())); connect(d->klineEdit, QOverload::of(&KLineEdit::returnPressed), this, QOverload::of(&KComboBox::returnPressed)); connect(d->klineEdit, &KLineEdit::completion, this, &KComboBox::completion); connect(d->klineEdit, &KLineEdit::substringCompletion, this, &KComboBox::substringCompletion); connect(d->klineEdit, &KLineEdit::textRotation, this, &KComboBox::textRotation); connect(d->klineEdit, &KLineEdit::completionModeChanged, this, &KComboBox::completionModeChanged); connect(d->klineEdit, &KLineEdit::aboutToShowContextMenu, this, &KComboBox::aboutToShowContextMenu); + // match the declaration of the deprecated signal +#if QT_DEPRECATED_SINCE(5, 15) || QT_VERSION < QT_VERSION_CHECK(5, 14, 0) connect(d->klineEdit, &KLineEdit::completionBoxActivated, this, QOverload::of(&QComboBox::activated)); +#endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + connect(d->klineEdit, &KLineEdit::completionBoxActivated, + this, QOverload::of(&QComboBox::textActivated)); +#endif d->klineEdit->setTrapReturnKey(d->trapReturnKey); } } void KComboBox::setCurrentItem(const QString &item, bool insert, int index) { int sel = -1; const int itemCount = count(); for (int i = 0; i < itemCount; ++i) { if (itemText(i) == item) { sel = i; break; } } if (sel == -1 && insert) { if (index >= 0) { insertItem(index, item); sel = index; } else { addItem(item); sel = count() - 1; } } setCurrentIndex(sel); } void KComboBox::setEditable(bool editable) { if (editable == isEditable()) { return; } if (editable) { // Create a KLineEdit instead of a QLineEdit // Compared to QComboBox::setEditable, we might be missing the SH_ComboBox_Popup code though... // If a style needs this, then we'll need to call QComboBox::setEditable and then setLineEdit again KLineEdit *edit = new KLineEdit(this); edit->setClearButtonEnabled(true); setLineEdit(edit); } else { QComboBox::setEditable(editable); } } #include "moc_kcombobox.cpp" diff --git a/src/kcompletion.cpp b/src/kcompletion.cpp index 835b1bf..d93c437 100644 --- a/src/kcompletion.cpp +++ b/src/kcompletion.cpp @@ -1,857 +1,857 @@ /* This file is part of the KDE libraries Copyright (C) 1999,2000,2001 Carsten Pfeiffer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcompletion.h" #include "kcompletion_p.h" #include #include void KCompletionPrivate::init() { completionMode = KCompletion::CompletionPopup; treeNodeAllocator = KCompTreeNode::allocator(); // keep strong-ref to allocator instance treeRoot = new KCompTreeNode; beep = true; ignoreCase = false; hasMultipleMatches = false; rotationIndex = 0; } void KCompletionPrivate::addWeightedItem(const QString &item) { Q_Q(KCompletion); if (order != KCompletion::Weighted) { q->addItem(item, 0); return; } int len = item.length(); uint weight = 0; // find out the weighting of this item (appended to the string as ":num") int index = item.lastIndexOf(QLatin1Char(':')); if (index > 0) { bool ok; weight = item.midRef(index + 1).toUInt(&ok); if (!ok) { weight = 0; } len = index; // only insert until the ':' } q->addItem(item.left(len), weight); return; } // tries to complete "string" from the tree-root QString KCompletionPrivate::findCompletion(const QString &string) { QChar ch; QString completion; const KCompTreeNode *node = treeRoot; // start at the tree-root and try to find the search-string for (int i = 0; i < string.length(); i++) { ch = string.at(i); node = node->find(ch); if (node) { completion += ch; } else { return QString(); // no completion } } // Now we have the last node of the to be completed string. // Follow it as long as it has exactly one child (= longest possible // completion) while (node->childrenCount() == 1) { node = node->firstChild(); if (!node->isNull()) { completion += *node; } } // if multiple matches and auto-completion mode // -> find the first complete match if (node && node->childrenCount() > 1) { hasMultipleMatches = true; if (completionMode == KCompletion::CompletionAuto) { rotationIndex = 1; if (order != KCompletion::Weighted) { while ((node = node->firstChild())) { if (!node->isNull()) { completion += *node; } else { break; } } } else { // don't just find the "first" match, but the one with the // highest priority const KCompTreeNode *temp_node = nullptr; while (1) { int count = node->childrenCount(); temp_node = node->firstChild(); uint weight = temp_node->weight(); const KCompTreeNode *hit = temp_node; for (int i = 1; i < count; i++) { temp_node = node->childAt(i); if (temp_node->weight() > weight) { hit = temp_node; weight = hit->weight(); } } // 0x0 has the highest priority -> we have the best match if (hit->isNull()) { break; } node = hit; completion += *node; } } } } return completion; } KCompletion::KCompletion() : d_ptr(new KCompletionPrivate(this)) { Q_D(KCompletion); d->init(); setOrder(Insertion); } KCompletion::~KCompletion() { } void KCompletion::setOrder(CompOrder order) { Q_D(KCompletion); d->order = order; d->matches.setSorting(order); } KCompletion::CompOrder KCompletion::order() const { Q_D(const KCompletion); return d->order; } void KCompletion::setIgnoreCase(bool ignoreCase) { Q_D(KCompletion); d->ignoreCase = ignoreCase; } bool KCompletion::ignoreCase() const { Q_D(const KCompletion); return d->ignoreCase; } void KCompletion::setItems(const QStringList &itemList) { clear(); insertItems(itemList); } void KCompletion::insertItems(const QStringList &items) { Q_D(KCompletion); bool weighted = (d->order == Weighted); QStringList::ConstIterator it; if (weighted) { // determine weight for (it = items.begin(); it != items.end(); ++it) { d->addWeightedItem(*it); } } else { for (it = items.begin(); it != items.end(); ++it) { addItem(*it, 0); } } } QStringList KCompletion::items() const { Q_D(const KCompletion); KCompletionMatchesWrapper list; // unsorted bool addWeight = (d->order == Weighted); list.extractStringsFromNode(d->treeRoot, QString(), addWeight); return list.list(); } bool KCompletion::isEmpty() const { Q_D(const KCompletion); return (d->treeRoot->childrenCount() == 0); } void KCompletion::postProcessMatch(QString *) const { } void KCompletion::postProcessMatches(QStringList *) const { } void KCompletion::postProcessMatches(KCompletionMatches *) const { } void KCompletion::addItem(const QString &item) { Q_D(KCompletion); d->matches.clear(); d->rotationIndex = 0; d->lastString.clear(); addItem(item, 0); } void KCompletion::addItem(const QString &item, uint weight) { Q_D(KCompletion); if (item.isEmpty()) { return; } KCompTreeNode *node = d->treeRoot; int len = item.length(); bool sorted = (d->order == Sorted); bool weighted = ((d->order == Weighted) && weight > 1); // knowing the weight of an item, we simply add this weight to all of its // nodes. for (int i = 0; i < len; i++) { node = node->insert(item.at(i), sorted); if (weighted) { node->confirm(weight - 1); // node->insert() sets weighting to 1 } } // add 0x0-item as delimiter with evtl. weight node = node->insert(0x0, true); if (weighted) { node->confirm(weight - 1); } // qDebug("*** added: %s (%i)", item.toLatin1().constData(), node->weight()); } void KCompletion::removeItem(const QString &item) { Q_D(KCompletion); d->matches.clear(); d->rotationIndex = 0; d->lastString.clear(); d->treeRoot->remove(item); } void KCompletion::clear() { Q_D(KCompletion); d->matches.clear(); d->rotationIndex = 0; d->lastString.clear(); delete d->treeRoot; d->treeRoot = new KCompTreeNode; } QString KCompletion::makeCompletion(const QString &string) { Q_D(KCompletion); if (d->completionMode == CompletionNone) { return QString(); } //qDebug() << "KCompletion: completing: " << string; d->matches.clear(); d->rotationIndex = 0; d->hasMultipleMatches = false; d->lastMatch = d->currentMatch; // in Shell-completion-mode, emit all matches when we get the same // complete-string twice if (d->completionMode == CompletionShell && string == d->lastString) { // Don't use d->matches since calling postProcessMatches() // on d->matches here would interfere with call to // postProcessMatch() during rotation d->matches.findAllCompletions(d->treeRoot, string, d->ignoreCase, d->hasMultipleMatches); QStringList l = d->matches.list(); postProcessMatches(&l); emit matches(l); return QString(); } QString completion; // in case-insensitive popup mode, we search all completions at once if (d->completionMode == CompletionPopup || d->completionMode == CompletionPopupAuto) { d->matches.findAllCompletions(d->treeRoot, string, d->ignoreCase, d->hasMultipleMatches); if (!d->matches.isEmpty()) { completion = d->matches.first(); } } else { completion = d->findCompletion(string); } if (d->hasMultipleMatches) { emit multipleMatches(); } d->lastString = string; d->currentMatch = completion; postProcessMatch(&completion); if (!string.isEmpty()) { // only emit match when string is not empty //qDebug() << "KCompletion: Match: " << completion; emit match(completion); } return completion; } QStringList KCompletion::substringCompletion(const QString &string) const { Q_D(const KCompletion); // get all items in the tree, eventually in sorted order KCompletionMatchesWrapper allItems(d->order); allItems.extractStringsFromNode(d->treeRoot, QString(), false); QStringList list = allItems.list(); // subStringMatches is invoked manually, via a shortcut if (list.isEmpty()) { return list; } if (string.isEmpty()) { // shortcut postProcessMatches(&list); return list; } QStringList matches; for (QStringList::ConstIterator it = list.constBegin(), total = list.constEnd(); it != total; ++it) { QString item = *it; if (item.indexOf(string, 0, Qt::CaseInsensitive) != -1) { // always case insensitive postProcessMatch(&item); matches.append(item); } } return matches; } void KCompletion::setCompletionMode(CompletionMode mode) { Q_D(KCompletion); d->completionMode = mode; } KCompletion::CompletionMode KCompletion::completionMode() const { Q_D(const KCompletion); return d->completionMode; } QStringList KCompletion::allMatches() { Q_D(KCompletion); // Don't use d->matches since calling postProcessMatches() // on d->matches here would interfere with call to // postProcessMatch() during rotation KCompletionMatchesWrapper matches(d->order); bool dummy; matches.findAllCompletions(d->treeRoot, d->lastString, d->ignoreCase, dummy); QStringList l = matches.list(); postProcessMatches(&l); return l; } KCompletionMatches KCompletion::allWeightedMatches() { Q_D(KCompletion); // Don't use d->matches since calling postProcessMatches() // on d->matches here would interfere with call to // postProcessMatch() during rotation KCompletionMatchesWrapper matches(d->order); bool dummy; matches.findAllCompletions(d->treeRoot, d->lastString, d->ignoreCase, dummy); KCompletionMatches ret(matches); postProcessMatches(&ret); return ret; } QStringList KCompletion::allMatches(const QString &string) { Q_D(KCompletion); KCompletionMatchesWrapper matches(d->order); bool dummy; matches.findAllCompletions(d->treeRoot, string, d->ignoreCase, dummy); QStringList l = matches.list(); postProcessMatches(&l); return l; } KCompletionMatches KCompletion::allWeightedMatches(const QString &string) { Q_D(KCompletion); KCompletionMatchesWrapper matches(d->order); bool dummy; matches.findAllCompletions(d->treeRoot, string, d->ignoreCase, dummy); KCompletionMatches ret(matches); postProcessMatches(&ret); return ret; } void KCompletion::setSoundsEnabled(bool enable) { Q_D(KCompletion); d->beep = enable; } bool KCompletion::soundsEnabled() const { Q_D(const KCompletion); return d->beep; } bool KCompletion::hasMultipleMatches() const { Q_D(const KCompletion); return d->hasMultipleMatches; } ///////////////////////////////////////////////////// ///////////////// tree operations /////////////////// QString KCompletion::nextMatch() { Q_D(KCompletion); QString completion; d->lastMatch = d->currentMatch; if (d->matches.isEmpty()) { d->matches.findAllCompletions(d->treeRoot, d->lastString, d->ignoreCase, d->hasMultipleMatches); if (!d->matches.isEmpty()) { completion = d->matches.first(); } d->currentMatch = completion; d->rotationIndex = 0; postProcessMatch(&completion); emit match(completion); return completion; } QStringList matches = d->matches.list(); d->lastMatch = matches[ d->rotationIndex++ ]; if (d->rotationIndex == matches.count()) { d->rotationIndex = 0; } completion = matches[ d->rotationIndex ]; d->currentMatch = completion; postProcessMatch(&completion); emit match(completion); return completion; } const QString &KCompletion::lastMatch() const { Q_D(const KCompletion); return d->lastMatch; } QString KCompletion::previousMatch() { Q_D(KCompletion); QString completion; d->lastMatch = d->currentMatch; if (d->matches.isEmpty()) { d->matches.findAllCompletions(d->treeRoot, d->lastString, d->ignoreCase, d->hasMultipleMatches); if (!d->matches.isEmpty()) { completion = d->matches.last(); } d->currentMatch = completion; d->rotationIndex = 0; postProcessMatch(&completion); emit match(completion); return completion; } QStringList matches = d->matches.list(); d->lastMatch = matches[ d->rotationIndex ]; if (d->rotationIndex == 0) { d->rotationIndex = matches.count(); } d->rotationIndex--; completion = matches[ d->rotationIndex ]; d->currentMatch = completion; postProcessMatch(&completion); emit match(completion); return completion; } ///////////////////////////////// ///////// // Implements the tree. Every node is a QChar and has a list of children, which // are Nodes as well. // QChar( 0x0 ) is used as the delimiter of a string; the last child of each // inserted string is 0x0. KCompTreeNode::~KCompTreeNode() { // delete all children KCompTreeNode *cur = m_children.begin(); while (cur) { KCompTreeNode *next = cur->m_next; delete m_children.remove(cur); cur = next; } } // Adds a child-node "ch" to this node. If such a node is already existent, // it will not be created. Returns the new/existing node. KCompTreeNode *KCompTreeNode::insert(const QChar &ch, bool sorted) { KCompTreeNode *child = find(ch); if (!child) { child = new KCompTreeNode(ch); // FIXME, first (slow) sorted insertion implementation if (sorted) { KCompTreeNode *prev = nullptr; KCompTreeNode *cur = m_children.begin(); while (cur) { if (ch > *cur) { prev = cur; cur = cur->m_next; } else { break; } } if (prev) { m_children.insert(prev, child); } else { m_children.prepend(child); } } else { m_children.append(child); } } // implicit weighting: the more often an item is inserted, the higher // priority it gets. child->confirm(); return child; } // Iteratively removes a string from the tree. The nicer recursive // version apparently was a little memory hungry (see #56757) void KCompTreeNode::remove(const QString &str) { QString string = str; string += QChar(0x0); QVector deletables(string.length() + 1); KCompTreeNode *child = nullptr; KCompTreeNode *parent = this; deletables.replace(0, parent); int i = 0; for (; i < string.length(); i++) { child = parent->find(string.at(i)); if (child) { deletables.replace(i + 1, child); } else { break; } parent = child; } for (; i >= 1; i--) { parent = deletables.at(i - 1); child = deletables.at(i); if (child->m_children.count() == 0) { delete parent->m_children.remove(child); } } } QStringList KCompletionMatchesWrapper::list() const { if (m_sortedList && m_dirty) { m_sortedList->sort(); m_dirty = false; m_stringList.clear(); // high weight == sorted last -> reverse the sorting here QList >::const_iterator it; for (it = m_sortedList->constBegin(); it != m_sortedList->constEnd(); ++it) { m_stringList.prepend((*it).value()); } } else if (m_compOrder == KCompletion::Sorted) { QCollator c; c.setCaseSensitivity(Qt::CaseSensitive); - qStableSort(m_stringList.begin(), m_stringList.end(), c); + std::stable_sort(m_stringList.begin(), m_stringList.end(), c); } return m_stringList; } void KCompletionMatchesWrapper::findAllCompletions(const KCompTreeNode *treeRoot,const QString &string, bool ignoreCase, bool &hasMultipleMatches) { //qDebug() << "*** finding all completions for " << string; if (string.isEmpty()) { return; } if (ignoreCase) { // case insensitive completion extractStringsFromNodeCI(treeRoot, QString(), string); hasMultipleMatches = (count() > 1); return; } QChar ch; QString completion; const KCompTreeNode *node = treeRoot; // start at the tree-root and try to find the search-string for (int i = 0; i < string.length(); i++) { ch = string.at(i); node = node->find(ch); if (node) { completion += ch; } else { return; // no completion -> return empty list } } // Now we have the last node of the to be completed string. // Follow it as long as it has exactly one child (= longest possible // completion) while (node->childrenCount() == 1) { node = node->firstChild(); if (!node->isNull()) { completion += *node; } // qDebug() << completion << node->latin1(); } // there is just one single match) if (node->childrenCount() == 0) { append(node->weight(), completion); } else { // node has more than one child // -> recursively find all remaining completions hasMultipleMatches = true; extractStringsFromNode(node, completion); } } void KCompletionMatchesWrapper::extractStringsFromNode(const KCompTreeNode *node, const QString &beginning, bool addWeight) { if (!node) { return; } // qDebug() << "Beginning: " << beginning; const KCompTreeChildren *list = node->children(); QString string; QString w; // loop thru all children for (KCompTreeNode *cur = list->begin(); cur; cur = cur->m_next) { string = beginning; node = cur; if (!node->isNull()) { string += *node; } while (node && node->childrenCount() == 1) { node = node->firstChild(); if (node->isNull()) { break; } string += *node; } if (node && node->isNull()) { // we found a leaf if (addWeight) { // add ":num" to the string to store the weighting string += QLatin1Char(':'); w.setNum(node->weight()); string.append(w); } append(node->weight(), string); } // recursively find all other strings. if (node && node->childrenCount() > 1) { extractStringsFromNode(node, string, addWeight); } } } void KCompletionMatchesWrapper::extractStringsFromNodeCI(const KCompTreeNode *node, const QString &beginning, const QString &restString) { if (restString.isEmpty()) { extractStringsFromNode(node, beginning, false /*noweight*/); return; } QChar ch1 = restString.at(0); QString newRest = restString.mid(1); KCompTreeNode *child1, *child2; child1 = node->find(ch1); // the correct match if (child1) extractStringsFromNodeCI(child1, beginning + QChar(*child1), newRest); // append the case insensitive matches, if available if (ch1.isLetter()) { // find out if we have to lower or upper it. Is there a better way? QChar ch2 = ch1.toLower(); if (ch1 == ch2) { ch2 = ch1.toUpper(); } if (ch1 != ch2) { child2 = node->find(ch2); if (child2) extractStringsFromNodeCI(child2, beginning + QChar(*child2), newRest); } } } void KCompTreeNodeList::append(KCompTreeNode *item) { m_count++; if (!m_last) { m_last = item; m_last->m_next = nullptr; m_first = item; return; } m_last->m_next = item; item->m_next = nullptr; m_last = item; } void KCompTreeNodeList::prepend(KCompTreeNode *item) { m_count++; if (!m_last) { m_last = item; m_last->m_next = nullptr; m_first = item; return; } item->m_next = m_first; m_first = item; } void KCompTreeNodeList::insert(KCompTreeNode *after, KCompTreeNode *item) { if (!after) { append(item); return; } m_count++; item->m_next = after->m_next; after->m_next = item; if (after == m_last) { m_last = item; } } KCompTreeNode *KCompTreeNodeList::remove(KCompTreeNode *item) { if (!m_first || !item) { return nullptr; } KCompTreeNode *cur = nullptr; if (item == m_first) { m_first = m_first->m_next; } else { cur = m_first; while (cur && cur->m_next != item) { cur = cur->m_next; } if (!cur) { return nullptr; } cur->m_next = item->m_next; } if (item == m_last) { m_last = cur; } m_count--; return item; } KCompTreeNode *KCompTreeNodeList::at(uint index) const { KCompTreeNode *cur = m_first; while (index-- && cur) { cur = cur->m_next; } return cur; } QSharedPointer KCompTreeNode::m_alloc(new KZoneAllocator(8*1024)); diff --git a/src/kcompletionbox.cpp b/src/kcompletionbox.cpp index 3e9d67c..fe7b208 100644 --- a/src/kcompletionbox.cpp +++ b/src/kcompletionbox.cpp @@ -1,572 +1,576 @@ /* This file is part of the KDE libraries Copyright (c) 2000,2001,2002 Carsten Pfeiffer Copyright (c) 2000 Stefan Schimanski <1Stein@gmx.de> Copyright (c) 2000,2001,2002,2003,2004 Dawit Alemayehu This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcompletionbox.h" #include "klineedit.h" #include #include -#include #include +#include +#include class KCompletionBoxPrivate { public: KCompletionBoxPrivate(KCompletionBox *parent): q_ptr(parent){} void init(); void cancelled(); void _k_itemClicked(QListWidgetItem *); QWidget *m_parent = nullptr; // necessary to set the focus back QString cancelText; bool tabHandling; bool upwardBox; bool emitSelected; KCompletionBox * const q_ptr; Q_DECLARE_PUBLIC(KCompletionBox) }; KCompletionBox::KCompletionBox(QWidget *parent) : QListWidget(parent), d_ptr(new KCompletionBoxPrivate(this)) { Q_D(KCompletionBox); d->m_parent = parent; d->init(); } void KCompletionBoxPrivate::init() { Q_Q(KCompletionBox); tabHandling = true; upwardBox = false; emitSelected = true; //we can't link to QXcbWindowFunctions::Combo //also, q->setAttribute(Qt::WA_X11NetWmWindowTypeCombo); is broken in Qt xcb q->setProperty("_q_xcb_wm_window_type", 0x001000); q->setAttribute(Qt::WA_ShowWithoutActivating); //on wayland, we need an xdg-popup but we don't want it to grab // calls setVisible, so must be done after initializations if (qGuiApp->platformName() == QLatin1String("wayland")) q->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint); else q->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint); q->setUniformItemSizes(true); q->setLineWidth(1); q->setFrameStyle(QFrame::Box | QFrame::Plain); q->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); q->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); q->connect(q, &QListWidget::itemDoubleClicked, q, &KCompletionBox::slotActivated); q->connect(q, SIGNAL(itemClicked(QListWidgetItem*)), SLOT(_k_itemClicked(QListWidgetItem*))); } KCompletionBox::~KCompletionBox() { Q_D(KCompletionBox); d->m_parent = nullptr; } QStringList KCompletionBox::items() const { QStringList list; list.reserve(count()); for (int i = 0; i < count(); i++) { const QListWidgetItem *currItem = item(i); list.append(currItem->text()); } return list; } void KCompletionBox::slotActivated(QListWidgetItem *item) { if (!item) { return; } hide(); emit activated(item->text()); } bool KCompletionBox::eventFilter(QObject *o, QEvent *e) { Q_D(KCompletionBox); int type = e->type(); QWidget *wid = qobject_cast(o); if (o == this) { return false; } if (wid && wid == d->m_parent && (type == QEvent::Move || type == QEvent::Resize)) { resizeAndReposition(); return false; } if (wid && (wid->windowFlags() & Qt::Window) && type == QEvent::Move && wid == d->m_parent->window()) { hide(); return false; } if (type == QEvent::MouseButtonPress && (wid && !isAncestorOf(wid))) { if (!d->emitSelected && currentItem() && !qobject_cast(o)) { Q_ASSERT(currentItem()); emit currentTextChanged(currentItem()->text()); } hide(); e->accept(); return true; } if (wid && wid->isAncestorOf(d->m_parent) && isVisible()) { if (type == QEvent::KeyPress) { QKeyEvent *ev = static_cast(e); switch (ev->key()) { case Qt::Key_Backtab: if (d->tabHandling && (ev->modifiers() == Qt::NoButton || (ev->modifiers() & Qt::ShiftModifier))) { up(); ev->accept(); return true; } break; case Qt::Key_Tab: if (d->tabHandling && (ev->modifiers() == Qt::NoButton)) { down(); // #65877: Key_Tab should complete using the first // (or selected) item, and then offer completions again if (count() == 1) { KLineEdit *parent = qobject_cast(d->m_parent); if (parent) { parent->doCompletion(currentItem()->text()); } else { hide(); } } ev->accept(); return true; } break; case Qt::Key_Down: down(); ev->accept(); return true; case Qt::Key_Up: // If there is no selected item and we've popped up above // our parent, select the first item when they press up. if (!selectedItems().isEmpty() || mapToGlobal(QPoint(0, 0)).y() > d->m_parent->mapToGlobal(QPoint(0, 0)).y()) { up(); } else { down(); } ev->accept(); return true; case Qt::Key_PageUp: pageUp(); ev->accept(); return true; case Qt::Key_PageDown: pageDown(); ev->accept(); return true; case Qt::Key_Escape: d->cancelled(); ev->accept(); return true; case Qt::Key_Enter: case Qt::Key_Return: if (ev->modifiers() & Qt::ShiftModifier) { hide(); ev->accept(); // Consume the Enter event return true; } break; case Qt::Key_End: if (ev->modifiers() & Qt::ControlModifier) { end(); ev->accept(); return true; } break; case Qt::Key_Home: if (ev->modifiers() & Qt::ControlModifier) { home(); ev->accept(); return true; } Q_FALLTHROUGH(); default: break; } } else if (type == QEvent::ShortcutOverride) { // Override any accelerators that match // the key sequences we use here... QKeyEvent *ev = static_cast(e); switch (ev->key()) { case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Escape: case Qt::Key_Enter: case Qt::Key_Return: ev->accept(); return true; case Qt::Key_Tab: case Qt::Key_Backtab: if (ev->modifiers() == Qt::NoButton || (ev->modifiers() & Qt::ShiftModifier)) { ev->accept(); return true; } break; case Qt::Key_Home: case Qt::Key_End: if (ev->modifiers() & Qt::ControlModifier) { ev->accept(); return true; } break; default: break; } } else if (type == QEvent::FocusOut) { QFocusEvent *event = static_cast(e); if (event->reason() != Qt::PopupFocusReason #ifdef Q_OS_WIN && (event->reason() != Qt::ActiveWindowFocusReason || QApplication::activeWindow() != this) #endif ) { hide(); } } } return QListWidget::eventFilter(o, e); } void KCompletionBox::popup() { if (count() == 0) { hide(); } else { bool block = signalsBlocked(); blockSignals(true); setCurrentRow(-1); blockSignals(block); clearSelection(); if (!isVisible()) { show(); } else if (size().height() != sizeHint().height()) { resizeAndReposition(); } } } void KCompletionBox::resizeAndReposition() { Q_D(KCompletionBox); int currentGeom = height(); QPoint currentPos = pos(); QRect geom = calculateGeometry(); resize(geom.size()); int x = currentPos.x(), y = currentPos.y(); if (d->m_parent) { if (!isVisible()) { - QPoint orig = globalPositionHint(); - QRect screenSize = QApplication::desktop()->screenGeometry(orig); + const QPoint orig = globalPositionHint(); + QScreen *screen = QGuiApplication::screenAt(orig); + if (screen) { + const QRect screenSize = screen->geometry(); - x = orig.x() + geom.x(); - y = orig.y() + geom.y(); + x = orig.x() + geom.x(); + y = orig.y() + geom.y(); - if (x + width() > screenSize.right()) { - x = screenSize.right() - width(); - } - if (y + height() > screenSize.bottom()) { - y = y - height() - d->m_parent->height(); - d->upwardBox = true; + if (x + width() > screenSize.right()) { + x = screenSize.right() - width(); + } + if (y + height() > screenSize.bottom()) { + y = y - height() - d->m_parent->height(); + d->upwardBox = true; + } } } else { // Are we above our parent? If so we must keep bottom edge anchored. if (d->upwardBox) { y += (currentGeom - height()); } } move(x, y); } } QPoint KCompletionBox::globalPositionHint() const { Q_D(const KCompletionBox); if (!d->m_parent) { return QPoint(); } return d->m_parent->mapToGlobal(QPoint(0, d->m_parent->height())); } void KCompletionBox::setVisible(bool visible) { Q_D(KCompletionBox); if (visible) { d->upwardBox = false; if (d->m_parent) { resizeAndReposition(); qApp->installEventFilter(this); } // FIXME: Is this comment still valid or can it be deleted? Is a patch already sent to Qt? // Following lines are a workaround for a bug (not sure whose this is): // If this KCompletionBox' parent is in a layout, that layout will detect the // insertion of a new child (posting a ChildInserted event). Then it will trigger relayout // (posting a LayoutHint event). // // QWidget::show() then sends also posted ChildInserted events for the parent, // and later all LayoutHint events, which cause layout updating. // The problem is that KCompletionBox::eventFilter() detects the resizing // of the parent, calls hide() and this hide() happens in the middle // of show(), causing inconsistent state. I'll try to submit a Qt patch too. qApp->sendPostedEvents(); } else { if (d->m_parent) { qApp->removeEventFilter(this); } d->cancelText.clear(); } QListWidget::setVisible(visible); } QRect KCompletionBox::calculateGeometry() const { Q_D(const KCompletionBox); QRect visualRect; if (count() == 0 || !(visualRect = visualItemRect(item(0))).isValid()) { return QRect(); } int x = 0, y = 0; int ih = visualRect.height(); int h = qMin(15 * ih, count() * ih) + 2 * frameWidth(); int w = (d->m_parent) ? d->m_parent->width() : QListWidget::minimumSizeHint().width(); w = qMax(QListWidget::minimumSizeHint().width(), w); return QRect(x, y, w, h); } QSize KCompletionBox::sizeHint() const { return calculateGeometry().size(); } void KCompletionBox::down() { const int row = currentRow(); const int lastRow = count() - 1; if (row < lastRow) { setCurrentRow(row + 1); return; } if (lastRow > -1) { setCurrentRow(0); } } void KCompletionBox::up() { const int row = currentRow(); if (row > 0) { setCurrentRow(row - 1); return; } const int lastRow = count() - 1; if (lastRow > 0) { setCurrentRow(lastRow); } } void KCompletionBox::pageDown() { selectionModel()->setCurrentIndex(moveCursor(QAbstractItemView::MovePageDown, Qt::NoModifier), QItemSelectionModel::SelectCurrent); } void KCompletionBox::pageUp() { selectionModel()->setCurrentIndex(moveCursor(QAbstractItemView::MovePageUp, Qt::NoModifier), QItemSelectionModel::SelectCurrent); } void KCompletionBox::home() { setCurrentRow(0); } void KCompletionBox::end() { setCurrentRow(count() - 1); } void KCompletionBox::setTabHandling(bool enable) { Q_D(KCompletionBox); d->tabHandling = enable; } bool KCompletionBox::isTabHandling() const { Q_D(const KCompletionBox); return d->tabHandling; } void KCompletionBox::setCancelledText(const QString &text) { Q_D(KCompletionBox); d->cancelText = text; } QString KCompletionBox::cancelledText() const { Q_D(const KCompletionBox); return d->cancelText; } void KCompletionBoxPrivate::cancelled() { Q_Q(KCompletionBox); if (!cancelText.isNull()) { emit q->userCancelled(cancelText); } if (q->isVisible()) { q->hide(); } } class KCompletionBoxItem : public QListWidgetItem { public: //Returns true if dirty. bool reuse(const QString &newText) { if (text() == newText) { return false; } setText(newText); return true; } }; void KCompletionBox::insertItems(const QStringList &items, int index) { bool block = signalsBlocked(); blockSignals(true); QListWidget::insertItems(index, items); blockSignals(block); setCurrentRow(-1); } void KCompletionBox::setItems(const QStringList &items) { bool block = signalsBlocked(); blockSignals(true); int rowIndex = 0; if (!count()) { addItems(items); } else { // Keep track of whether we need to change anything, // so we can avoid a repaint for identical updates, // to reduce flicker bool dirty = false; QStringList::ConstIterator it = items.constBegin(); const QStringList::ConstIterator itEnd = items.constEnd(); for (; it != itEnd; ++it) { if (rowIndex < count()) { const bool changed = ((KCompletionBoxItem *)item(rowIndex))->reuse(*it); dirty = dirty || changed; } else { dirty = true; // Inserting an item is a way of making this dirty addItem(*it); } rowIndex++; } // If there is an unused item, mark as dirty -> less items now if (rowIndex < count()) { dirty = true; } // remove unused items with an index >= rowIndex for (; rowIndex < count();) { QListWidgetItem *item = takeItem(rowIndex); Q_ASSERT(item); delete item; } } if (isVisible() && size().height() != sizeHint().height()) { resizeAndReposition(); } blockSignals(block); } void KCompletionBoxPrivate::_k_itemClicked(QListWidgetItem *item) { Q_Q(KCompletionBox); if (item) { q->hide(); emit q->currentTextChanged(item->text()); emit q->activated(item->text()); } } void KCompletionBox::setActivateOnSelect(bool doEmit) { Q_D(KCompletionBox); d->emitSelected = doEmit; } bool KCompletionBox::activateOnSelect() const { Q_D(const KCompletionBox); return d->emitSelected; } #include "moc_kcompletionbox.cpp" diff --git a/src/khistorycombobox.cpp b/src/khistorycombobox.cpp index 9eb9abc..14fafb8 100644 --- a/src/khistorycombobox.cpp +++ b/src/khistorycombobox.cpp @@ -1,491 +1,501 @@ /* This file is part of the KDE libraries Copyright (c) 2000,2001 Dawit Alemayehu Copyright (c) 2000,2001 Carsten Pfeiffer Copyright (c) 2000 Stefan Schimanski <1Stein@gmx.de> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "khistorycombobox.h" #include #include #include #include #include #include #include class KHistoryComboBoxPrivate { public: KHistoryComboBoxPrivate(KHistoryComboBox *parent): q_ptr(parent) {} void init(bool useCompletion); void rotateUp(); void rotateDown(); /** * Called from the popupmenu, * calls clearHistory() and emits cleared() */ void _k_clear(); /** * Appends our own context menu entry. */ void _k_addContextMenuItems(QMenu *); /** * Used to emit the activated(QString) signal when enter is pressed */ void _k_simulateActivated(const QString &); /** * The text typed before Up or Down was pressed. */ QString typedText; KPixmapProvider *pixmapProvider = nullptr; KHistoryComboBox * const q_ptr; /** * The current index in the combobox, used for Up and Down */ int currentIndex; /** * Indicates that the user at least once rotated Up through the entire list * Needed to allow going back after rotation. */ bool rotated = false; Q_DECLARE_PUBLIC(KHistoryComboBox) }; void KHistoryComboBoxPrivate::init(bool useCompletion) { Q_Q(KHistoryComboBox); // Set a default history size to something reasonable, Qt sets it to INT_MAX by default q->setMaxCount(50); if (useCompletion) { q->completionObject()->setOrder(KCompletion::Weighted); } q->setInsertPolicy(KHistoryComboBox::NoInsert); currentIndex = -1; rotated = false; pixmapProvider = nullptr; // obey HISTCONTROL setting QByteArray histControl = qgetenv("HISTCONTROL"); if (histControl == "ignoredups" || histControl == "ignoreboth") { q->setDuplicatesEnabled(false); } q->connect(q, SIGNAL(aboutToShowContextMenu(QMenu*)), SLOT(_k_addContextMenuItems(QMenu*))); QObject::connect(q, QOverload::of(&QComboBox::activated), q, &KHistoryComboBox::reset); QObject::connect(q, QOverload::of(&KComboBox::returnPressed), q, &KHistoryComboBox::reset); // We want _k_simulateActivated to be called _after_ QComboBoxPrivate::_q_returnPressed // otherwise there's a risk of emitting activated twice (_k_simulateActivated will find // the item, after some app's slotActivated inserted the item into the combo). q->connect(q, SIGNAL(returnPressed(QString)), SLOT(_k_simulateActivated(QString)), Qt::QueuedConnection); } // we are always read-write KHistoryComboBox::KHistoryComboBox(QWidget *parent) : KComboBox(true, parent), d_ptr(new KHistoryComboBoxPrivate(this)) { Q_D(KHistoryComboBox); d->init(true); // using completion } // we are always read-write KHistoryComboBox::KHistoryComboBox(bool useCompletion, QWidget *parent) : KComboBox(true, parent), d_ptr(new KHistoryComboBoxPrivate(this)) { Q_D(KHistoryComboBox); d->init(useCompletion); } KHistoryComboBox::~KHistoryComboBox() { Q_D(KHistoryComboBox); delete d->pixmapProvider; } void KHistoryComboBox::setHistoryItems(const QStringList &items) { setHistoryItems(items, false); } void KHistoryComboBox::setHistoryItems(const QStringList &items, bool setCompletionList) { QStringList insertingItems = items; KComboBox::clear(); // limit to maxCount() const int itemCount = insertingItems.count(); const int toRemove = itemCount - maxCount(); if (toRemove >= itemCount) { insertingItems.clear(); } else { for (int i = 0; i < toRemove; ++i) { insertingItems.pop_front(); } } insertItems(insertingItems); if (setCompletionList && useCompletion()) { // we don't have any weighting information here ;( KCompletion *comp = completionObject(); comp->setOrder(KCompletion::Insertion); comp->setItems(insertingItems); comp->setOrder(KCompletion::Weighted); } clearEditText(); } QStringList KHistoryComboBox::historyItems() const { QStringList list; const int itemCount = count(); list.reserve(itemCount); for (int i = 0; i < itemCount; ++i) { list.append(itemText(i)); } return list; } bool KHistoryComboBox::useCompletion() const { return compObj(); } void KHistoryComboBox::clearHistory() { const QString temp = currentText(); KComboBox::clear(); if (useCompletion()) { completionObject()->clear(); } setEditText(temp); } void KHistoryComboBoxPrivate::_k_addContextMenuItems(QMenu *menu) { Q_Q(KHistoryComboBox); if (menu) { menu->addSeparator(); QAction *clearHistory = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), KHistoryComboBox::tr("Clear &History"), q, SLOT(_k_clear())); if (!q->count()) { clearHistory->setEnabled(false); } } } void KHistoryComboBox::addToHistory(const QString &item) { Q_D(KHistoryComboBox); if (item.isEmpty() || (count() > 0 && item == itemText(0))) { return; } bool wasCurrent = false; // remove all existing items before adding if (!duplicatesEnabled()) { int i = 0; int itemCount = count(); while (i < itemCount) { if (itemText(i) == item) { if (!wasCurrent) { wasCurrent = (i == currentIndex()); } removeItem(i); --itemCount; } else { ++i; } } } // now add the item if (d->pixmapProvider) { insertItem(0, d->pixmapProvider->pixmapFor(item, iconSize().height()), item); } else { insertItem(0, item); } if (wasCurrent) { setCurrentIndex(0); } const bool useComp = useCompletion(); const int last = count() - 1; // last valid index const int mc = maxCount(); const int stopAt = qMax(mc, 0); for (int rmIndex = last; rmIndex >= stopAt; --rmIndex) { // remove the last item, as long as we are longer than maxCount() // remove the removed item from the completionObject if it isn't // anymore available at all in the combobox. const QString rmItem = itemText(rmIndex); removeItem(rmIndex); if (useComp && !contains(rmItem)) { completionObject()->removeItem(rmItem); } } if (useComp) { completionObject()->addItem(item); } } bool KHistoryComboBox::removeFromHistory(const QString &item) { if (item.isEmpty()) { return false; } bool removed = false; const QString temp = currentText(); int i = 0; int itemCount = count(); while (i < itemCount) { if (item == itemText(i)) { removed = true; removeItem(i); --itemCount; } else { ++i; } } if (removed && useCompletion()) { completionObject()->removeItem(item); } setEditText(temp); return removed; } // going up in the history, rotating when reaching QListBox::count() // // Note: this differs from QComboBox because "up" means ++index here, // to simulate the way shell history works (up goes to the most // recent item). In QComboBox "down" means ++index, to match the popup... // void KHistoryComboBoxPrivate::rotateUp() { Q_Q(KHistoryComboBox); // save the current text in the lineedit // (This is also where this differs from standard up/down in QComboBox, // where a single keypress can make you lose your typed text) if (currentIndex == -1) { typedText = q->currentText(); } ++currentIndex; // skip duplicates/empty items const int last = q->count() - 1; // last valid index const QString currText = q->currentText(); while (currentIndex < last && (currText == q->itemText(currentIndex) || q->itemText(currentIndex).isEmpty())) { ++currentIndex; } if (currentIndex >= q->count()) { rotated = true; currentIndex = -1; // if the typed text is the same as the first item, skip the first if (q->count() > 0 && typedText == q->itemText(0)) { currentIndex = 0; } q->setEditText(typedText); } else { q->setCurrentIndex(currentIndex); } } // going down in the history, no rotation possible. Last item will be // the text that was in the lineedit before Up was called. void KHistoryComboBoxPrivate::rotateDown() { Q_Q(KHistoryComboBox); // save the current text in the lineedit if (currentIndex == -1) { typedText = q->currentText(); } --currentIndex; const QString currText = q->currentText(); // skip duplicates/empty items while (currentIndex >= 0 && (currText == q->itemText(currentIndex) || q->itemText(currentIndex).isEmpty())) { --currentIndex; } if (currentIndex < 0) { if (rotated && currentIndex == -2) { rotated = false; currentIndex = q->count() - 1; q->setEditText(q->itemText(currentIndex)); } else { // bottom of history currentIndex = -1; if (q->currentText() != typedText) { q->setEditText(typedText); } } } else { q->setCurrentIndex(currentIndex); } } void KHistoryComboBox::keyPressEvent(QKeyEvent *e) { Q_D(KHistoryComboBox); int event_key = e->key() | e->modifiers(); if (KStandardShortcut::rotateUp().contains(event_key)) { d->rotateUp(); } else if (KStandardShortcut::rotateDown().contains(event_key)) { d->rotateDown(); } else { KComboBox::keyPressEvent(e); } } void KHistoryComboBox::wheelEvent(QWheelEvent *ev) { Q_D(KHistoryComboBox); // Pass to poppable listbox if it's up QAbstractItemView *const iv = view(); if (iv && iv->isVisible()) { QApplication::sendEvent(iv, ev); return; } // Otherwise make it change the text without emitting activated if (ev->angleDelta().y() > 0) { d->rotateUp(); } else { d->rotateDown(); } ev->accept(); } void KHistoryComboBox::setPixmapProvider(KPixmapProvider *provider) { Q_D(KHistoryComboBox); if (d->pixmapProvider == provider) { return; } delete d->pixmapProvider; d->pixmapProvider = provider; // re-insert all the items with/without pixmap // I would prefer to use changeItem(), but that doesn't honor the pixmap // when using an editable combobox (what we do) if (count() > 0) { QStringList items(historyItems()); clear(); insertItems(items); } } void KHistoryComboBox::insertItems(const QStringList &items) { Q_D(KHistoryComboBox); QStringList::ConstIterator it = items.constBegin(); const QStringList::ConstIterator itEnd = items.constEnd(); while (it != itEnd) { const QString item = *it; if (!item.isEmpty()) { // only insert non-empty items if (d->pixmapProvider) addItem(d->pixmapProvider->pixmapFor(item, iconSize().height()), item); else { addItem(item); } } ++it; } } void KHistoryComboBoxPrivate::_k_clear() { Q_Q(KHistoryComboBox); q->clearHistory(); emit q->cleared(); } void KHistoryComboBoxPrivate::_k_simulateActivated(const QString &text) { Q_Q(KHistoryComboBox); /* With the insertion policy NoInsert, which we use by default, Qt doesn't emit activated on typed text if the item is not already there, which is perhaps reasonable. Generate the signal ourselves if that's the case. */ if ((q->insertPolicy() == q->NoInsert && q->findText(text, Qt::MatchFixedString | Qt::MatchCaseSensitive) == -1)) { +#if QT_DEPRECATED_SINCE(5, 15) || QT_VERSION < QT_VERSION_CHECK(5, 14, 0) emit q->activated(text); +#endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + emit q->textActivated(text); +#endif } /* Qt also doesn't emit it if the box is full, and policy is not InsertAtCurrent */ else if (q->insertPolicy() != q->InsertAtCurrent && q->count() >= q->maxCount()) { +#if QT_DEPRECATED_SINCE(5, 15) || QT_VERSION < QT_VERSION_CHECK(5, 14, 0) emit q->activated(text); +#endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + emit q->textActivated(text); +#endif } } KPixmapProvider *KHistoryComboBox::pixmapProvider() const { Q_D(const KHistoryComboBox); return d->pixmapProvider; } void KHistoryComboBox::reset() { Q_D(KHistoryComboBox); d->currentIndex = -1; d->rotated = false; } #include "moc_khistorycombobox.cpp" diff --git a/src/klineedit.cpp b/src/klineedit.cpp index 7f8d690..47897ce 100644 --- a/src/klineedit.cpp +++ b/src/klineedit.cpp @@ -1,1484 +1,1484 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Sven Radej (sven.radej@iname.com) Copyright (c) 1999 Patrick Ward Copyright (c) 1999 Preston Brown Re-designed for KDE 2.x by Copyright (c) 2000, 2001 Dawit Alemayehu Copyright (c) 2000, 2001 Carsten Pfeiffer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "klineedit.h" #include "klineedit_p.h" #include #include #include #include #include #include #include #include #include #include #include #include KLineEditPrivate::~KLineEditPrivate() { // causes a weird crash in KWord at least, so let Qt delete it for us. // delete completionBox; } void KLineEditPrivate::_k_textChanged(const QString &text) { Q_Q(KLineEdit); // COMPAT (as documented): emit userTextChanged whenever textChanged is emitted if (!completionRunning && (text != userText)) { userText = text; #ifndef KCOMPLETION_NO_DEPRECATED emit q->userTextChanged(text); #endif } } // Call this when a completion operation changes the lineedit text // "as if it had been edited by the user". void KLineEditPrivate::updateUserText(const QString &text) { Q_Q(KLineEdit); if (!completionRunning && (text != userText)) { userText = text; q->setModified(true); #ifndef KCOMPLETION_NO_DEPRECATED emit q->userTextChanged(text); #endif emit q->textEdited(text); emit q->textChanged(text); } } bool KLineEditPrivate::s_backspacePerformsCompletion = false; bool KLineEditPrivate::s_initialized = false; void KLineEditPrivate::init() { Q_Q(KLineEdit); //--- completionBox = nullptr; handleURLDrops = true; trapReturnKeyEvents = false; userSelection = true; autoSuggest = false; disableRestoreSelection = false; enableSqueezedText = false; completionRunning = false; if (!s_initialized) { KConfigGroup config(KSharedConfig::openConfig(), "General"); s_backspacePerformsCompletion = config.readEntry("Backspace performs completion", false); s_initialized = true; } urlDropEventFilter = new LineEditUrlDropEventFilter(q); // i18n: Placeholder text in line edit widgets is the text appearing // before any user input, briefly explaining to the user what to type // (e.g. "Enter search pattern"). // By default the text is set in italic, which may not be appropriate // for some languages and scripts (e.g. for CJK ideographs). QString metaMsg = KLineEdit::tr("1", "Italic placeholder text in line edits: 0 no, 1 yes"); italicizePlaceholder = (metaMsg.trimmed() != QLatin1Char('0')); //--- possibleTripleClick = false; bgRole = q->backgroundRole(); // Enable the context menu by default. q->QLineEdit::setContextMenuPolicy(Qt::DefaultContextMenu); KCursor::setAutoHideCursor(q, true, true); KCompletion::CompletionMode mode = q->completionMode(); autoSuggest = (mode == KCompletion::CompletionMan || mode == KCompletion::CompletionPopupAuto || mode == KCompletion::CompletionAuto); q->connect(q, SIGNAL(selectionChanged()), q, SLOT(_k_restoreSelectionColors())); if (handleURLDrops) { q->installEventFilter(urlDropEventFilter); } const QPalette p = q->palette(); if (!previousHighlightedTextColor.isValid()) { previousHighlightedTextColor = p.color(QPalette::Normal, QPalette::HighlightedText); } if (!previousHighlightColor.isValid()) { previousHighlightColor = p.color(QPalette::Normal, QPalette::Highlight); } q->connect(q, SIGNAL(textChanged(QString)), q, SLOT(_k_textChanged(QString))); } KLineEdit::KLineEdit(const QString &string, QWidget *parent) : QLineEdit(string, parent), d_ptr(new KLineEditPrivate(this)) { Q_D(KLineEdit); d->init(); } KLineEdit::KLineEdit(QWidget *parent) : QLineEdit(parent), d_ptr(new KLineEditPrivate(this)) { Q_D(KLineEdit); d->init(); } KLineEdit::~KLineEdit() { } #ifndef KCOMPLETION_NO_DEPRECATED QString KLineEdit::clickMessage() const { return placeholderText(); } #endif void KLineEdit::setClearButtonShown(bool show) { setClearButtonEnabled(show); } bool KLineEdit::isClearButtonShown() const { return isClearButtonEnabled(); } QSize KLineEdit::clearButtonUsedSize() const { QSize s; if (isClearButtonEnabled()) { // from qlineedit_p.cpp const int iconSize = height() < 34 ? 16 : 32; const int buttonWidth = iconSize + 6; const int buttonHeight = iconSize + 2; s = QSize(buttonWidth, buttonHeight); } return s; } void KLineEdit::setCompletionMode(KCompletion::CompletionMode mode) { Q_D(KLineEdit); KCompletion::CompletionMode oldMode = completionMode(); if (oldMode != mode && (oldMode == KCompletion::CompletionPopup || oldMode == KCompletion::CompletionPopupAuto) && d->completionBox && d->completionBox->isVisible()) { d->completionBox->hide(); } // If the widgets echo mode is not Normal, no completion // feature will be enabled even if one is requested. if (echoMode() != QLineEdit::Normal) { mode = KCompletion::CompletionNone; // Override the request. } if (!KAuthorized::authorize(QStringLiteral("lineedit_text_completion"))) { mode = KCompletion::CompletionNone; } if (mode == KCompletion::CompletionPopupAuto || mode == KCompletion::CompletionAuto || mode == KCompletion::CompletionMan) { d->autoSuggest = true; } else { d->autoSuggest = false; } KCompletionBase::setCompletionMode(mode); } void KLineEdit::setCompletionModeDisabled(KCompletion::CompletionMode mode, bool disable) { Q_D(KLineEdit); d->disableCompletionMap[ mode ] = disable; } void KLineEdit::setCompletedText(const QString &t, bool marked) { Q_D(KLineEdit); if (!d->autoSuggest) { return; } const QString txt = text(); if (t != txt) { setText(t); if (marked) { setSelection(t.length(), txt.length() - t.length()); } setUserSelection(false); } else { setUserSelection(true); } } void KLineEdit::setCompletedText(const QString &text) { KCompletion::CompletionMode mode = completionMode(); const bool marked = (mode == KCompletion::CompletionAuto || mode == KCompletion::CompletionMan || mode == KCompletion::CompletionPopup || mode == KCompletion::CompletionPopupAuto); setCompletedText(text, marked); } void KLineEdit::rotateText(KCompletionBase::KeyBindingType type) { KCompletion *comp = compObj(); if (comp && (type == KCompletionBase::PrevCompletionMatch || type == KCompletionBase::NextCompletionMatch)) { QString input; if (type == KCompletionBase::PrevCompletionMatch) { input = comp->previousMatch(); } else { input = comp->nextMatch(); } // Skip rotation if previous/next match is null or the same text if (input.isEmpty() || input == displayText()) { return; } setCompletedText(input, hasSelectedText()); } } void KLineEdit::makeCompletion(const QString &text) { Q_D(KLineEdit); KCompletion *comp = compObj(); KCompletion::CompletionMode mode = completionMode(); if (!comp || mode == KCompletion::CompletionNone) { return; // No completion object... } const QString match = comp->makeCompletion(text); if (mode == KCompletion::CompletionPopup || mode == KCompletion::CompletionPopupAuto) { if (match.isEmpty()) { if (d->completionBox) { d->completionBox->hide(); d->completionBox->clear(); } } else { setCompletedItems(comp->allMatches()); } } else { // Auto, ShortAuto (Man) and Shell // all other completion modes // If no match or the same match, simply return without completing. if (match.isEmpty() || match == text) { return; } if (mode != KCompletion::CompletionShell) { setUserSelection(false); } if (d->autoSuggest) { setCompletedText(match); } } } void KLineEdit::setReadOnly(bool readOnly) { Q_D(KLineEdit); // Do not do anything if nothing changed... if (readOnly == isReadOnly()) { return; } QLineEdit::setReadOnly(readOnly); if (readOnly) { d->bgRole = backgroundRole(); setBackgroundRole(QPalette::Window); if (d->enableSqueezedText && d->squeezedText.isEmpty()) { d->squeezedText = text(); d->setSqueezedText(); } } else { if (!d->squeezedText.isEmpty()) { setText(d->squeezedText); d->squeezedText.clear(); } setBackgroundRole(d->bgRole); } } void KLineEdit::setSqueezedText(const QString &text) { setSqueezedTextEnabled(true); setText(text); } void KLineEdit::setSqueezedTextEnabled(bool enable) { Q_D(KLineEdit); d->enableSqueezedText = enable; } bool KLineEdit::isSqueezedTextEnabled() const { Q_D(const KLineEdit); return d->enableSqueezedText; } void KLineEdit::setText(const QString &text) { Q_D(KLineEdit); if (d->enableSqueezedText && isReadOnly()) { d->squeezedText = text; d->setSqueezedText(); return; } QLineEdit::setText(text); } void KLineEditPrivate::setSqueezedText() { Q_Q(KLineEdit); squeezedStart = 0; squeezedEnd = 0; const QString fullText = squeezedText; const int fullLength = fullText.length(); const QFontMetrics fm(q->fontMetrics()); const int labelWidth = q->size().width() - 2 * q->style()->pixelMetric(QStyle::PM_DefaultFrameWidth) - 2; - const int textWidth = fm.width(fullText); + const int textWidth = fm.boundingRect(fullText).width(); // TODO: investigate use of QFontMetrics::elidedText for this if (textWidth > labelWidth) { // TODO: better would be "…" char (0x2026), but for that one would need to ensure it's from the main font, // otherwise if resulting in use of a new fallback font this can affect the metrics of the complete text, // resulting in shifted characters const QString ellipsisText = QStringLiteral("..."); // start with the dots only QString squeezedText = ellipsisText; - int squeezedWidth = fm.width(squeezedText); + int squeezedWidth = fm.boundingRect(squeezedText).width(); // estimate how many letters we can add to the dots on both sides int letters = fullText.length() * (labelWidth - squeezedWidth) / textWidth / 2; squeezedText = fullText.leftRef(letters) + ellipsisText + fullText.rightRef(letters); - squeezedWidth = fm.width(squeezedText); + squeezedWidth = fm.boundingRect(squeezedText).width(); if (squeezedWidth < labelWidth) { // we estimated too short // add letters while text < label do { letters++; squeezedText = fullText.leftRef(letters) + ellipsisText + fullText.rightRef(letters); - squeezedWidth = fm.width(squeezedText); + squeezedWidth = fm.boundingRect(squeezedText).width(); } while (squeezedWidth < labelWidth && letters <= fullLength / 2); letters--; squeezedText = fullText.leftRef(letters) + ellipsisText + fullText.rightRef(letters); } else if (squeezedWidth > labelWidth) { // we estimated too long // remove letters while text > label do { letters--; squeezedText = fullText.leftRef(letters) + ellipsisText + fullText.rightRef(letters); - squeezedWidth = fm.width(squeezedText); + squeezedWidth = fm.boundingRect(squeezedText).width(); } while (squeezedWidth > labelWidth && letters >= 5); } if (letters < 5) { // too few letters added -> we give up squeezing q->QLineEdit::setText(fullText); } else { q->QLineEdit::setText(squeezedText); squeezedStart = letters; squeezedEnd = fullText.length() - letters; } q->setToolTip(fullText); } else { q->QLineEdit::setText(fullText); q->setToolTip(QString()); QToolTip::showText(q->pos(), QString()); // hide } q->setCursorPosition(0); } void KLineEdit::copy() const { Q_D(const KLineEdit); if (!d->copySqueezedText(true)) { QLineEdit::copy(); } } bool KLineEditPrivate::copySqueezedText(bool copy) const { Q_Q(const KLineEdit); if (!squeezedText.isEmpty() && squeezedStart) { KLineEdit *that = const_cast(q); if (!that->hasSelectedText()) { return false; } int start = q->selectionStart(), end = start + q->selectedText().length(); if (start >= squeezedStart + 3) { start = start - 3 - squeezedStart + squeezedEnd; } else if (start > squeezedStart) { start = squeezedStart; } if (end >= squeezedStart + 3) { end = end - 3 - squeezedStart + squeezedEnd; } else if (end > squeezedStart) { end = squeezedEnd; } if (start == end) { return false; } QString t = squeezedText; t = t.mid(start, end - start); q->disconnect(QApplication::clipboard(), SIGNAL(selectionChanged()), q, nullptr); QApplication::clipboard()->setText(t, copy ? QClipboard::Clipboard : QClipboard::Selection); q->connect(QApplication::clipboard(), SIGNAL(selectionChanged()), q, SLOT(_q_clipboardChanged())); return true; } return false; } void KLineEdit::resizeEvent(QResizeEvent *ev) { Q_D(KLineEdit); if (!d->squeezedText.isEmpty()) { d->setSqueezedText(); } QLineEdit::resizeEvent(ev); } void KLineEdit::keyPressEvent(QKeyEvent *e) { Q_D(KLineEdit); const int key = e->key() | e->modifiers(); if (KStandardShortcut::copy().contains(key)) { copy(); return; } else if (KStandardShortcut::paste().contains(key)) { // TODO: // we should restore the original text (not autocompleted), otherwise the paste // will get into troubles Bug: 134691 if (!isReadOnly()) { paste(); } return; } else if (KStandardShortcut::pasteSelection().contains(key)) { QString text = QApplication::clipboard()->text(QClipboard::Selection); insert(text); deselect(); return; } else if (KStandardShortcut::cut().contains(key)) { if (!isReadOnly()) { cut(); } return; } else if (KStandardShortcut::undo().contains(key)) { if (!isReadOnly()) { undo(); } return; } else if (KStandardShortcut::redo().contains(key)) { if (!isReadOnly()) { redo(); } return; } else if (KStandardShortcut::deleteWordBack().contains(key)) { cursorWordBackward(true); if (hasSelectedText() && !isReadOnly()) { del(); } e->accept(); return; } else if (KStandardShortcut::deleteWordForward().contains(key)) { // Workaround for QT bug where cursorWordForward(true); if (hasSelectedText() && !isReadOnly()) { del(); } e->accept(); return; } else if (KStandardShortcut::backwardWord().contains(key)) { cursorWordBackward(false); e->accept(); return; } else if (KStandardShortcut::forwardWord().contains(key)) { cursorWordForward(false); e->accept(); return; } else if (KStandardShortcut::beginningOfLine().contains(key)) { home(false); e->accept(); return; } else if (KStandardShortcut::endOfLine().contains(key)) { end(false); e->accept(); return; } // Filter key-events if EchoMode is normal and // completion mode is not set to CompletionNone if (echoMode() == QLineEdit::Normal && completionMode() != KCompletion::CompletionNone) { if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { const bool trap = (d->completionBox && d->completionBox->isVisible()); const bool stopEvent = (trap || (d->trapReturnKeyEvents && (e->modifiers() == Qt::NoButton || e->modifiers() == Qt::KeypadModifier))); if (stopEvent) { emit QLineEdit::returnPressed(); e->accept(); } emit returnPressed(displayText()); if (trap) { d->completionBox->hide(); deselect(); setCursorPosition(text().length()); } // Eat the event if the user asked for it, or if a completionbox was visible if (stopEvent) { return; } } const KeyBindingMap keys = keyBindingMap(); const KCompletion::CompletionMode mode = completionMode(); const bool noModifier = (e->modifiers() == Qt::NoButton || e->modifiers() == Qt::ShiftModifier || e->modifiers() == Qt::KeypadModifier); if ((mode == KCompletion::CompletionAuto || mode == KCompletion::CompletionPopupAuto || mode == KCompletion::CompletionMan) && noModifier) { if (!d->userSelection && hasSelectedText() && (e->key() == Qt::Key_Right || e->key() == Qt::Key_Left) && e->modifiers() == Qt::NoButton) { const QString old_txt = text(); d->disableRestoreSelection = true; const int start = selectionStart(); deselect(); QLineEdit::keyPressEvent(e); const int cPosition = cursorPosition(); setText(old_txt); // keep cursor at cPosition setSelection(old_txt.length(), cPosition - old_txt.length()); if (e->key() == Qt::Key_Right && cPosition > start) { //the user explicitly accepted the autocompletion d->updateUserText(text()); } d->disableRestoreSelection = false; return; } if (e->key() == Qt::Key_Escape) { if (hasSelectedText() && !d->userSelection) { del(); setUserSelection(true); } // Don't swallow the Escape press event for the case // of dialogs, which have Escape associated to Cancel e->ignore(); return; } } if ((mode == KCompletion::CompletionAuto || mode == KCompletion::CompletionMan) && noModifier) { const QString keycode = e->text(); if (!keycode.isEmpty() && (keycode.unicode()->isPrint() || e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete)) { const bool hasUserSelection = d->userSelection; const bool hadSelection = hasSelectedText(); bool cursorNotAtEnd = false; const int start = selectionStart(); const int cPos = cursorPosition(); // When moving the cursor, we want to keep the autocompletion as an // autocompletion, so we want to process events at the cursor position // as if there was no selection. After processing the key event, we // can set the new autocompletion again. if (hadSelection && !hasUserSelection && start > cPos) { del(); setCursorPosition(cPos); cursorNotAtEnd = true; } d->disableRestoreSelection = true; QLineEdit::keyPressEvent(e); d->disableRestoreSelection = false; QString txt = text(); int len = txt.length(); if (!hasSelectedText() && len /*&& cursorPosition() == len */) { if (e->key() == Qt::Key_Backspace) { if (hadSelection && !hasUserSelection && !cursorNotAtEnd) { backspace(); txt = text(); len = txt.length(); } if (!d->s_backspacePerformsCompletion || !len) { d->autoSuggest = false; } } if (e->key() == Qt::Key_Delete) { d->autoSuggest = false; } doCompletion(txt); if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete)) { d->autoSuggest = true; } e->accept(); } return; } } else if ((mode == KCompletion::CompletionPopup || mode == KCompletion::CompletionPopupAuto) && noModifier && !e->text().isEmpty()) { const QString old_txt = text(); const bool hasUserSelection = d->userSelection; const bool hadSelection = hasSelectedText(); bool cursorNotAtEnd = false; const int start = selectionStart(); const int cPos = cursorPosition(); const QString keycode = e->text(); // When moving the cursor, we want to keep the autocompletion as an // autocompletion, so we want to process events at the cursor position // as if there was no selection. After processing the key event, we // can set the new autocompletion again. if (hadSelection && !hasUserSelection && start > cPos && ((!keycode.isEmpty() && keycode.unicode()->isPrint()) || e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete)) { del(); setCursorPosition(cPos); cursorNotAtEnd = true; } const int selectedLength = selectedText().length(); d->disableRestoreSelection = true; QLineEdit::keyPressEvent(e); d->disableRestoreSelection = false; if ((selectedLength != selectedText().length()) && !hasUserSelection) { d->_k_restoreSelectionColors(); // and set userSelection to true } QString txt = text(); int len = txt.length(); if ((txt != old_txt || txt != e->text()) && len/* && ( cursorPosition() == len || force )*/ && ((!keycode.isEmpty() && keycode.unicode()->isPrint()) || e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete)) { if (e->key() == Qt::Key_Backspace) { if (hadSelection && !hasUserSelection && !cursorNotAtEnd) { backspace(); txt = text(); len = txt.length(); } if (!d->s_backspacePerformsCompletion) { d->autoSuggest = false; } } if (e->key() == Qt::Key_Delete) { d->autoSuggest = false; } if (d->completionBox) { d->completionBox->setCancelledText(txt); } doCompletion(txt); if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete) && mode == KCompletion::CompletionPopupAuto) { d->autoSuggest = true; } e->accept(); } else if (!len && d->completionBox && d->completionBox->isVisible()) { d->completionBox->hide(); } return; } else if (mode == KCompletion::CompletionShell) { // Handles completion. QList cut; if (keys[TextCompletion].isEmpty()) { cut = KStandardShortcut::shortcut(KStandardShortcut::TextCompletion); } else { cut = keys[TextCompletion]; } if (cut.contains(key)) { // Emit completion if the completion mode is CompletionShell // and the cursor is at the end of the string. const QString txt = text(); const int len = txt.length(); if (cursorPosition() == len && len != 0) { doCompletion(txt); return; } } else if (d->completionBox) { d->completionBox->hide(); } } // handle rotation // Handles previous match QList cut; if (keys[PrevCompletionMatch].isEmpty()) { cut = KStandardShortcut::shortcut(KStandardShortcut::PrevCompletion); } else { cut = keys[PrevCompletionMatch]; } if (cut.contains(key)) { if (emitSignals()) { emit textRotation(KCompletionBase::PrevCompletionMatch); } if (handleSignals()) { rotateText(KCompletionBase::PrevCompletionMatch); } return; } // Handles next match if (keys[NextCompletionMatch].isEmpty()) { cut = KStandardShortcut::shortcut(KStandardShortcut::NextCompletion); } else { cut = keys[NextCompletionMatch]; } if (cut.contains(key)) { if (emitSignals()) { emit textRotation(KCompletionBase::NextCompletionMatch); } if (handleSignals()) { rotateText(KCompletionBase::NextCompletionMatch); } return; } // substring completion if (compObj()) { QList cut; if (keys[SubstringCompletion].isEmpty()) { cut = KStandardShortcut::shortcut(KStandardShortcut::SubstringCompletion); } else { cut = keys[SubstringCompletion]; } if (cut.contains(key)) { if (emitSignals()) { emit substringCompletion(text()); } if (handleSignals()) { setCompletedItems(compObj()->substringCompletion(text())); e->accept(); } return; } } } const int selectedLength = selectedText().length(); // Let QLineEdit handle any other keys events. QLineEdit::keyPressEvent(e); if (selectedLength != selectedText().length()) { d->_k_restoreSelectionColors(); // and set userSelection to true } } void KLineEdit::mouseDoubleClickEvent(QMouseEvent *e) { Q_D(KLineEdit); if (e->button() == Qt::LeftButton) { d->possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(_k_tripleClickTimeout())); } QLineEdit::mouseDoubleClickEvent(e); } void KLineEdit::mousePressEvent(QMouseEvent *e) { Q_D(KLineEdit); if (e->button() == Qt::LeftButton && d->possibleTripleClick) { selectAll(); e->accept(); return; } // if middle clicking and if text is present in the clipboard then clear the selection // to prepare paste operation if (e->button() == Qt::MidButton) { if (hasSelectedText() && !isReadOnly()) { if (QApplication::clipboard()->text(QClipboard::Selection).length() > 0) { backspace(); } } } QLineEdit::mousePressEvent(e); } void KLineEdit::mouseReleaseEvent(QMouseEvent *e) { Q_D(KLineEdit); QLineEdit::mouseReleaseEvent(e); if (QApplication::clipboard()->supportsSelection()) { if (e->button() == Qt::LeftButton) { // Fix copying of squeezed text if needed d->copySqueezedText(false); } } } void KLineEditPrivate::_k_tripleClickTimeout() { possibleTripleClick = false; } QMenu *KLineEdit::createStandardContextMenu() { Q_D(KLineEdit); QMenu *popup = QLineEdit::createStandardContextMenu(); if (!isReadOnly()) { // FIXME: This code depends on Qt's action ordering. const QList actionList = popup->actions(); enum { UndoAct, RedoAct, Separator1, CutAct, CopyAct, PasteAct, DeleteAct, ClearAct, Separator2, SelectAllAct, NCountActs }; QAction *separatorAction = nullptr; // separator we want is right after Delete right now. const int idx = actionList.indexOf(actionList[DeleteAct]) + 1; if (idx < actionList.count()) { separatorAction = actionList.at(idx); } if (separatorAction) { QAction *clearAllAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), tr("C&lear"), this); clearAllAction->setShortcuts(QKeySequence::keyBindings(QKeySequence::DeleteCompleteLine)); connect(clearAllAction, &QAction::triggered, this, &QLineEdit::clear); if (text().isEmpty()) { clearAllAction->setEnabled(false); } popup->insertAction(separatorAction, clearAllAction); } } // If a completion object is present and the input // widget is not read-only, show the Text Completion // menu item. if (compObj() && !isReadOnly() && KAuthorized::authorize(QStringLiteral("lineedit_text_completion"))) { QMenu *subMenu = popup->addMenu(QIcon::fromTheme(QStringLiteral("text-completion")), tr("Text Completion", "@title:menu")); connect(subMenu, SIGNAL(triggered(QAction*)), this, SLOT(_k_completionMenuActivated(QAction*))); popup->addSeparator(); QActionGroup *ag = new QActionGroup(this); d->noCompletionAction = ag->addAction(tr("None", "@item:inmenu Text Completion")); d->shellCompletionAction = ag->addAction(tr("Manual", "@item:inmenu Text Completion")); d->autoCompletionAction = ag->addAction(tr("Automatic", "@item:inmenu Text Completion")); d->popupCompletionAction = ag->addAction(tr("Dropdown List", "@item:inmenu Text Completion")); d->shortAutoCompletionAction = ag->addAction(tr("Short Automatic", "@item:inmenu Text Completion")); d->popupAutoCompletionAction = ag->addAction(tr("Dropdown List && Automatic", "@item:inmenu Text Completion")); subMenu->addActions(ag->actions()); //subMenu->setAccel( KStandardShortcut::completion(), ShellCompletion ); d->shellCompletionAction->setCheckable(true); d->noCompletionAction->setCheckable(true); d->popupCompletionAction->setCheckable(true); d->autoCompletionAction->setCheckable(true); d->shortAutoCompletionAction->setCheckable(true); d->popupAutoCompletionAction->setCheckable(true); d->shellCompletionAction->setEnabled(!d->disableCompletionMap[ KCompletion::CompletionShell ]); d->noCompletionAction->setEnabled(!d->disableCompletionMap[ KCompletion::CompletionNone ]); d->popupCompletionAction->setEnabled(!d->disableCompletionMap[ KCompletion::CompletionPopup ]); d->autoCompletionAction->setEnabled(!d->disableCompletionMap[ KCompletion::CompletionAuto ]); d->shortAutoCompletionAction->setEnabled(!d->disableCompletionMap[ KCompletion::CompletionMan ]); d->popupAutoCompletionAction->setEnabled(!d->disableCompletionMap[ KCompletion::CompletionPopupAuto ]); const KCompletion::CompletionMode mode = completionMode(); d->noCompletionAction->setChecked(mode == KCompletion::CompletionNone); d->shellCompletionAction->setChecked(mode == KCompletion::CompletionShell); d->popupCompletionAction->setChecked(mode == KCompletion::CompletionPopup); d->autoCompletionAction->setChecked(mode == KCompletion::CompletionAuto); d->shortAutoCompletionAction->setChecked(mode == KCompletion::CompletionMan); d->popupAutoCompletionAction->setChecked(mode == KCompletion::CompletionPopupAuto); const KCompletion::CompletionMode defaultMode = KCompletion::CompletionPopup; if (mode != defaultMode && !d->disableCompletionMap[ defaultMode ]) { subMenu->addSeparator(); d->defaultAction = subMenu->addAction(tr("Default", "@item:inmenu Text Completion")); } } return popup; } void KLineEdit::contextMenuEvent(QContextMenuEvent *e) { if (QLineEdit::contextMenuPolicy() != Qt::DefaultContextMenu) { return; } QMenu *popup = createStandardContextMenu(); // ### do we really need this? Yes, Please do not remove! This // allows applications to extend the popup menu without having to // inherit from this class! (DA) emit aboutToShowContextMenu(popup); popup->exec(e->globalPos()); delete popup; } void KLineEditPrivate::_k_completionMenuActivated(QAction *act) { Q_Q(KLineEdit); KCompletion::CompletionMode oldMode = q->completionMode(); if (act == noCompletionAction) { q->setCompletionMode(KCompletion::CompletionNone); } else if (act == shellCompletionAction) { q->setCompletionMode(KCompletion::CompletionShell); } else if (act == autoCompletionAction) { q->setCompletionMode(KCompletion::CompletionAuto); } else if (act == popupCompletionAction) { q->setCompletionMode(KCompletion::CompletionPopup); } else if (act == shortAutoCompletionAction) { q->setCompletionMode(KCompletion::CompletionMan); } else if (act == popupAutoCompletionAction) { q->setCompletionMode(KCompletion::CompletionPopupAuto); } else if (act == defaultAction) { q->setCompletionMode(KCompletion::CompletionPopup); } else { return; } if (oldMode != q->completionMode()) { if ((oldMode == KCompletion::CompletionPopup || oldMode == KCompletion::CompletionPopupAuto) && completionBox && completionBox->isVisible()) { completionBox->hide(); } emit q->completionModeChanged(q->completionMode()); } } bool KLineEdit::event(QEvent *ev) { Q_D(KLineEdit); KCursor::autoHideEventFilter(this, ev); if (ev->type() == QEvent::ShortcutOverride) { QKeyEvent *e = static_cast(ev); if (d->overrideShortcut(e)) { ev->accept(); } } else if (ev->type() == QEvent::ApplicationPaletteChange || ev->type() == QEvent::PaletteChange) { // Assume the widget uses the application's palette QPalette p = QApplication::palette(); d->previousHighlightedTextColor = p.color(QPalette::Normal, QPalette::HighlightedText); d->previousHighlightColor = p.color(QPalette::Normal, QPalette::Highlight); setUserSelection(d->userSelection); } else if (ev->type() == QEvent::ChildAdded) { QObject *obj = static_cast(ev)->child(); if (obj) { connect(obj, &QObject::objectNameChanged, this, [this, obj] { if (obj->objectName() == QLatin1String("_q_qlineeditclearaction")) { QAction *action = qobject_cast(obj); connect(action, &QAction::triggered, this, &KLineEdit::clearButtonClicked); } }); } } return QLineEdit::event(ev); } #ifndef KCOMPLETION_NO_DEPRECATED void KLineEdit::setUrlDropsEnabled(bool enable) { Q_D(KLineEdit); if (enable && !d->handleURLDrops) { installEventFilter(d->urlDropEventFilter); d->handleURLDrops = true; } else if (!enable && d->handleURLDrops) { removeEventFilter(d->urlDropEventFilter); d->handleURLDrops = false; } } #endif bool KLineEdit::urlDropsEnabled() const { Q_D(const KLineEdit); return d->handleURLDrops; } void KLineEdit::setTrapReturnKey(bool trap) { Q_D(KLineEdit); d->trapReturnKeyEvents = trap; } bool KLineEdit::trapReturnKey() const { Q_D(const KLineEdit); return d->trapReturnKeyEvents; } void KLineEdit::setUrl(const QUrl &url) { setText(url.toDisplayString()); } void KLineEdit::setCompletionBox(KCompletionBox *box) { Q_D(KLineEdit); if (d->completionBox) { return; } d->completionBox = box; if (handleSignals()) { connect(d->completionBox, SIGNAL(currentTextChanged(QString)), SLOT(_k_completionBoxTextChanged(QString))); connect(d->completionBox, &KCompletionBox::userCancelled, this, &KLineEdit::userCancelled); connect(d->completionBox, SIGNAL(activated(QString)), SIGNAL(completionBoxActivated(QString))); connect(d->completionBox, SIGNAL(activated(QString)), SIGNAL(textEdited(QString))); } } /* * Set the line edit text without changing the modified flag. By default * calling setText resets the modified flag to false. */ static void setEditText(KLineEdit *edit, const QString &text) { if (!edit) { return; } const bool wasModified = edit->isModified(); edit->setText(text); edit->setModified(wasModified); } void KLineEdit::userCancelled(const QString &cancelText) { Q_D(KLineEdit); if (completionMode() != KCompletion::CompletionPopupAuto) { setEditText(this, cancelText); } else if (hasSelectedText()) { if (d->userSelection) { deselect(); } else { d->autoSuggest = false; const int start = selectionStart(); const QString s = text().remove(selectionStart(), selectedText().length()); setEditText(this, s); setCursorPosition(start); d->autoSuggest = true; } } } bool KLineEditPrivate::overrideShortcut(const QKeyEvent *e) { Q_Q(KLineEdit); QList scKey; const int key = e->key() | e->modifiers(); const KLineEdit::KeyBindingMap keys = q->keyBindingMap(); if (keys[KLineEdit::TextCompletion].isEmpty()) { scKey = KStandardShortcut::shortcut(KStandardShortcut::TextCompletion); } else { scKey = keys[KLineEdit::TextCompletion]; } if (scKey.contains(key)) { return true; } if (keys[KLineEdit::NextCompletionMatch].isEmpty()) { scKey = KStandardShortcut::shortcut(KStandardShortcut::NextCompletion); } else { scKey = keys[KLineEdit::NextCompletionMatch]; } if (scKey.contains(key)) { return true; } if (keys[KLineEdit::PrevCompletionMatch].isEmpty()) { scKey = KStandardShortcut::shortcut(KStandardShortcut::PrevCompletion); } else { scKey = keys[KLineEdit::PrevCompletionMatch]; } if (scKey.contains(key)) { return true; } // Override all the text manupilation accelerators... if (KStandardShortcut::copy().contains(key)) { return true; } else if (KStandardShortcut::paste().contains(key)) { return true; } else if (KStandardShortcut::cut().contains(key)) { return true; } else if (KStandardShortcut::undo().contains(key)) { return true; } else if (KStandardShortcut::redo().contains(key)) { return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { return true; } else if (KStandardShortcut::forwardWord().contains(key)) { return true; } else if (KStandardShortcut::backwardWord().contains(key)) { return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { return true; } else if (KStandardShortcut::endOfLine().contains(key)) { return true; } // Shortcut overrides for shortcuts that QLineEdit handles // but doesn't dare force as "stronger than kaction shortcuts"... else if (e->matches(QKeySequence::SelectAll)) { return true; } else if (qApp->platformName() == QLatin1String("xcb") && (key == Qt::CTRL + Qt::Key_E || key == Qt::CTRL + Qt::Key_U)) { return true; } if (completionBox && completionBox->isVisible()) { const int key = e->key(); const Qt::KeyboardModifiers modifiers = e->modifiers(); if ((key == Qt::Key_Backtab || key == Qt::Key_Tab) && (modifiers == Qt::NoModifier || (modifiers & Qt::ShiftModifier))) { return true; } } return false; } void KLineEdit::setCompletedItems(const QStringList &items, bool autoSuggest) { Q_D(KLineEdit); QString txt; if (d->completionBox && d->completionBox->isVisible()) { // The popup is visible already - do the matching on the initial string, // not on the currently selected one. txt = completionBox()->cancelledText(); } else { txt = text(); } if (!items.isEmpty() && !(items.count() == 1 && txt == items.first())) { // create completion box if non-existent completionBox(); if (d->completionBox->isVisible()) { QListWidgetItem *currentItem = d->completionBox->currentItem(); QString currentSelection; if (currentItem != nullptr) { currentSelection = currentItem->text(); } d->completionBox->setItems(items); const QList matchedItems = d->completionBox->findItems(currentSelection, Qt::MatchExactly); QListWidgetItem *matchedItem = matchedItems.isEmpty() ? nullptr : matchedItems.first(); if (matchedItem) { const bool blocked = d->completionBox->blockSignals(true); d->completionBox->setCurrentItem(matchedItem); d->completionBox->blockSignals(blocked); } else { d->completionBox->setCurrentRow(-1); } } else { // completion box not visible yet -> show it if (!txt.isEmpty()) { d->completionBox->setCancelledText(txt); } d->completionBox->setItems(items); d->completionBox->popup(); } if (d->autoSuggest && autoSuggest) { const int index = items.first().indexOf(txt); const QString newText = items.first().mid(index); setUserSelection(false); // can be removed? setCompletedText sets it anyway setCompletedText(newText, true); } } else { if (d->completionBox && d->completionBox->isVisible()) { d->completionBox->hide(); } } } KCompletionBox *KLineEdit::completionBox(bool create) { Q_D(KLineEdit); if (create && !d->completionBox) { setCompletionBox(new KCompletionBox(this)); d->completionBox->setObjectName(QStringLiteral("completion box")); d->completionBox->setFont(font()); } return d->completionBox; } void KLineEdit::setCompletionObject(KCompletion *comp, bool handle) { KCompletion *oldComp = compObj(); if (oldComp && handleSignals()) disconnect(oldComp, SIGNAL(matches(QStringList)), this, SLOT(setCompletedItems(QStringList))); if (comp && handle) connect(comp, SIGNAL(matches(QStringList)), this, SLOT(setCompletedItems(QStringList))); KCompletionBase::setCompletionObject(comp, handle); } void KLineEdit::setUserSelection(bool userSelection) { Q_D(KLineEdit); //if !d->userSelection && userSelection we are accepting a completion, //so trigger an update if (!d->userSelection && userSelection) { d->updateUserText(text()); } QPalette p = palette(); if (userSelection) { p.setColor(QPalette::Highlight, d->previousHighlightColor); p.setColor(QPalette::HighlightedText, d->previousHighlightedTextColor); } else { QColor color = p.color(QPalette::Disabled, QPalette::Text); p.setColor(QPalette::HighlightedText, color); color = p.color(QPalette::Active, QPalette::Base); p.setColor(QPalette::Highlight, color); } d->userSelection = userSelection; setPalette(p); } void KLineEditPrivate::_k_restoreSelectionColors() { Q_Q(KLineEdit); if (disableRestoreSelection) { return; } q->setUserSelection(true); } void KLineEditPrivate::_k_completionBoxTextChanged(const QString &text) { Q_Q(KLineEdit); if (!text.isEmpty()) { q->setText(text); q->setModified(true); q->end(false); // force cursor at end } } QString KLineEdit::originalText() const { Q_D(const KLineEdit); if (d->enableSqueezedText && isReadOnly()) { return d->squeezedText; } return text(); } QString KLineEdit::userText() const { Q_D(const KLineEdit); return d->userText; } bool KLineEdit::autoSuggest() const { Q_D(const KLineEdit); return d->autoSuggest; } void KLineEdit::paintEvent(QPaintEvent *ev) { Q_D(KLineEdit); if (echoMode() == Password && d->threeStars) { // ### hack alert! // QLineEdit has currently no hooks to modify the displayed string. // When we call setText(), an update() is triggered and we get // into an infinite recursion. // Qt offers the setUpdatesEnabled() method, but when we re-enable // them, update() is triggered, and we get into the same recursion. // To work around this problem, we set/clear the internal Qt flag which // marks the updatesDisabled state manually. setAttribute(Qt::WA_UpdatesDisabled, true); blockSignals(true); const QString oldText = text(); const bool isModifiedState = isModified(); // save modified state because setText resets it setText(oldText + oldText + oldText); QLineEdit::paintEvent(ev); setText(oldText); setModified(isModifiedState); blockSignals(false); setAttribute(Qt::WA_UpdatesDisabled, false); } else { QLineEdit::paintEvent(ev); } } #ifndef KCOMPLETION_NO_DEPRECATED void KLineEdit::setClickMessage(const QString &msg) { setPlaceholderText(msg); } #endif #ifndef KCOMPLETION_NO_DEPRECATED void KLineEdit::setContextMenuEnabled(bool showMenu) { QLineEdit::setContextMenuPolicy(showMenu ? Qt::DefaultContextMenu : Qt::NoContextMenu); } #endif #ifndef KCOMPLETION_NO_DEPRECATED bool KLineEdit::isContextMenuEnabled() const { return (contextMenuPolicy() == Qt::DefaultContextMenu); } #endif void KLineEdit::setPasswordMode(bool passwordMode) { Q_D(KLineEdit); if (passwordMode) { KConfigGroup cg(KSharedConfig::openConfig(), "Passwords"); const QString val = cg.readEntry("EchoMode", "OneStar"); if (val == QLatin1String("NoEcho")) { setEchoMode(NoEcho); } else { d->threeStars = (val == QLatin1String("ThreeStars")); setEchoMode(Password); } } else { setEchoMode(Normal); } } bool KLineEdit::passwordMode() const { return echoMode() == NoEcho || echoMode() == Password; } void KLineEdit::doCompletion(const QString &text) { Q_D(KLineEdit); if (emitSignals()) { emit completion(text); // emit when requested... } d->completionRunning = true; if (handleSignals()) { makeCompletion(text); // handle when requested... } d->completionRunning = false; } #include "moc_klineedit.cpp" diff --git a/src/ksortablelist.h b/src/ksortablelist.h index dfe6a1f..c1c2ead 100644 --- a/src/ksortablelist.h +++ b/src/ksortablelist.h @@ -1,212 +1,213 @@ /* This file is part of the KDE libraries Copyright (C) 2001 Carsten Pfeiffer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KSORTABLELIST_H #define KSORTABLELIST_H #include #include #include +#include /** * \class KSortableItem ksortablelist.h * * KSortableItem is a QPair that provides several operators * for sorting. * @see KSortableList */ template class KSortableItem : public QPair { public: /** * Creates a new KSortableItem with the given values. * @param i the first value (the key) * @param t the second value (the item) */ KSortableItem(Key i, const T &t) : QPair(i, t) {} /** * Creates a new KSortableItem that copies another one. * @param rhs the other item to copy */ KSortableItem(const KSortableItem &rhs) : QPair(rhs.first, rhs.second) {} /** * Creates a new KSortableItem with uninitialized values. */ KSortableItem() {} /** * Assignment operator, just copies the item. */ KSortableItem &operator=(const KSortableItem &i) { this->first = i.first; this->second = i.second; return *this; } // operators for sorting /** * Compares the two items. This implementation only compares * the first value. */ bool operator> (const KSortableItem &i2) const { return (i2.first < this->first); } /** * Compares the two items. This implementation only compares * the first value. */ bool operator< (const KSortableItem &i2) const { return (this->first < i2.first); } /** * Compares the two items. This implementation only compares * the first value. */ bool operator>= (const KSortableItem &i2) const { return (this->first >= i2.first); } /** * Compares the two items. This implementation only compares * the first value. */ bool operator<= (const KSortableItem &i2) const { return !(i2.first < this->first); } /** * Compares the two items. This implementation only compares * the first value. */ bool operator== (const KSortableItem &i2) const { return (this->first == i2.first); } /** * Compares the two items. This implementation only compares * the first value. */ bool operator!= (const KSortableItem &i2) const { return (this->first != i2.first); } /** * @return the second value (the item) */ T &value() { return this->second; } /** * @return the second value (the item) */ const T &value() const { return this->second; } /** * @return the first value (the key) * @deprecated use key() */ #ifndef KCOMPLETION_NO_DEPRECATED KCOMPLETION_DEPRECATED Key index() const { return this->first; } #endif /** * @return the first value. */ Key key() const { return this->first; } }; /** * \class KSortableList ksortablelist.h * * KSortableList is a QList which associates a key with each item in the list. * This key is used for sorting when calling sort(). * * This allows to temporarily calculate a key and use it for sorting, without having * to store that key in the items, or calculate that key many times for the same item * during sorting if that calculation is expensive. */ template class KSortableList : public QList > { public: /** * Insert a KSortableItem with the given values. * @param i the first value * @param t the second value */ void insert(Key i, const T &t) { QList >::append(KSortableItem(i, t)); } // add more as you please... /** * Returns the first value of the KSortableItem at the given position. * @return the first value of the KSortableItem */ T &operator[](Key i) { return QList >::operator[](i).value(); } /** * Returns the first value of the KSortableItem at the given position. * @return the first value of the KSortableItem */ const T &operator[](Key i) const { return QList >::operator[](i).value(); } /** * Sorts the KSortableItems. */ void sort() { - qSort(*this); + std::sort(this->begin(), this->end()); } }; #ifdef Q_CC_MSVC template inline uint qHash(const KSortableItem &) { Q_ASSERT(0); return 0; } #endif #endif // KSORTABLELIST_H diff --git a/tests/kcompletionuitest.cpp b/tests/kcompletionuitest.cpp index e33a493..457f172 100644 --- a/tests/kcompletionuitest.cpp +++ b/tests/kcompletionuitest.cpp @@ -1,204 +1,197 @@ #include "kcompletionuitest.h" #include #include #include #include #include #include #include #include -/* - * Constructs a Form1 which is a child of 'parent', with the - * widget flags set to 'f' - */ Form1::Form1(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_DeleteOnClose); - setObjectName(QStringLiteral("Form1")); resize(559, 465); setWindowTitle(QStringLiteral("Form1")); Form1Layout = new QVBoxLayout(this); GroupBox1 = new QGroupBox(this); GroupBox1->setLayout(new QVBoxLayout()); GroupBox1->setTitle(QStringLiteral("Completion Test")); GroupBox1->layout()->setSpacing(0); GroupBox1->layout()->setContentsMargins(0, 0, 0, 0); GroupBox1Layout = new QVBoxLayout; GroupBox1Layout->setAlignment(Qt::AlignTop); - GroupBox1Layout->setSpacing(6); - GroupBox1Layout->setMargin(11); GroupBox1->layout()->addItem(GroupBox1Layout); GroupBox1Layout->setParent(GroupBox1->layout()); Layout9 = new QVBoxLayout; Layout9->setSpacing(6); Layout9->setContentsMargins(0, 0, 0, 0); Layout1 = new QHBoxLayout; Layout1->setSpacing(6); Layout1->setContentsMargins(0, 0, 0, 0); TextLabel1 = new QLabel(GroupBox1); TextLabel1->setObjectName(QStringLiteral("TextLabel1")); TextLabel1->setText(QStringLiteral("Completion")); Layout1->addWidget(TextLabel1); edit = new KLineEdit(GroupBox1); edit->setObjectName(QStringLiteral("edit")); Layout1->addWidget(edit); Layout9->addLayout(Layout1); edit->completionObject()->setItems(defaultItems()); edit->completionObject()->setIgnoreCase(true); edit->setFocus(); edit->setToolTip(QStringLiteral("right-click to change completion mode")); Layout2 = new QHBoxLayout; Layout2->setSpacing(6); Layout2->setContentsMargins(0, 0, 0, 0); combo = new KHistoryComboBox(GroupBox1); combo->setObjectName(QStringLiteral("history combo")); combo->setCompletionObject(edit->completionObject()); // combo->setMaxCount( 5 ); combo->setHistoryItems(defaultItems(), true); connect(combo, SIGNAL(activated(QString)), combo, SLOT(addToHistory(QString))); combo->setToolTip(QStringLiteral("KHistoryComboBox")); Layout2->addWidget(combo); LineEdit1 = new KLineEdit(GroupBox1); LineEdit1->setObjectName(QStringLiteral("LineEdit1")); Layout2->addWidget(LineEdit1); PushButton1 = new QPushButton(GroupBox1); PushButton1->setObjectName(QStringLiteral("PushButton1")); PushButton1->setText(QStringLiteral("Add")); connect(PushButton1, &QAbstractButton::clicked, this, &Form1::slotAdd); Layout2->addWidget(PushButton1); Layout9->addLayout(Layout2); Layout3 = new QHBoxLayout; Layout3->setSpacing(6); Layout3->setContentsMargins(0, 0, 0, 0); QSpacerItem *spacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); Layout3->addItem(spacer); PushButton1_4 = new QPushButton(GroupBox1); PushButton1_4->setObjectName(QStringLiteral("PushButton1_4")); PushButton1_4->setText(QStringLiteral("Remove")); connect(PushButton1_4, &QAbstractButton::clicked, this, &Form1::slotRemove); Layout3->addWidget(PushButton1_4); Layout9->addLayout(Layout3); Layout8 = new QHBoxLayout; Layout8->setSpacing(6); Layout8->setContentsMargins(0, 0, 0, 0); ListBox1 = new QListWidget(GroupBox1); Layout8->addWidget(ListBox1); connect(ListBox1, &QListWidget::currentRowChanged, this, &Form1::slotHighlighted); ListBox1->setToolTip(QStringLiteral("Contains the contents of the completion object.\n:x is the weighting, i.e. how often an item has been inserted")); Layout7 = new QVBoxLayout; Layout7->setSpacing(6); Layout7->setContentsMargins(0, 0, 0, 0); PushButton1_3 = new QPushButton(GroupBox1); PushButton1_3->setObjectName(QStringLiteral("PushButton1_3")); PushButton1_3->setText(QStringLiteral("Completion items")); connect(PushButton1_3, &QAbstractButton::clicked, this, &Form1::slotList); Layout7->addWidget(PushButton1_3); PushButton1_2 = new QPushButton(GroupBox1); PushButton1_2->setObjectName(QStringLiteral("PushButton1_2")); PushButton1_2->setText(QStringLiteral("Clear")); connect(PushButton1_2, &QAbstractButton::clicked, edit->completionObject(), &KCompletion::clear); Layout7->addWidget(PushButton1_2); Layout8->addLayout(Layout7); Layout9->addLayout(Layout8); GroupBox1Layout->addLayout(Layout9); Form1Layout->addWidget(GroupBox1); slotList(); } /* * Destroys the object and frees any allocated resources */ Form1::~Form1() { // no need to delete child widgets, Qt does it all for us } void Form1::slotAdd() { qDebug("** adding: %s", LineEdit1->text().toLatin1().constData()); edit->completionObject()->addItem(LineEdit1->text()); QStringList matches = edit->completionObject()->allMatches(QStringLiteral("S")); QStringList::ConstIterator it = matches.constBegin(); for (; it != matches.constEnd(); ++it) { qDebug("-- %s", (*it).toLatin1().constData()); } } void Form1::slotRemove() { edit->completionObject()->removeItem(LineEdit1->text()); } void Form1::slotList() { ListBox1->clear(); QStringList items = edit->completionObject()->items(); ListBox1->addItems(items); } void Form1::slotHighlighted(int row) { if (row == -1) { return; } QListWidgetItem *i = ListBox1->item(row); Q_ASSERT(i != nullptr); QString text = i->text(); // remove any "weighting" int index = text.lastIndexOf(QLatin1Char(':')); if (index > 0) { LineEdit1->setText(text.left(index)); } else { LineEdit1->setText(text); } } QStringList Form1::defaultItems() const { QStringList items; items << QStringLiteral("Super") << QStringLiteral("Sushi") << QStringLiteral("Samson") << QStringLiteral("Sucks") << QStringLiteral("Sumo") << QStringLiteral("Schumi"); items << QStringLiteral("Slashdot") << QStringLiteral("sUpEr") << QStringLiteral("SUshi") << QStringLiteral("sUshi") << QStringLiteral("sUShi"); items << QStringLiteral("sushI") << QStringLiteral("SushI"); return items; } int main(int argc, char **argv) { QApplication::setApplicationName(QStringLiteral("kcompletiontest")); QApplication app(argc, argv); Form1 *form = new Form1(); form->show(); return app.exec(); }