From 5dda8b7361aba8cc892b09125d69daed8e13ec50 Mon Sep 17 00:00:00 2001 From: Flavio Tordini Date: Sat, 11 Oct 2014 21:22:50 +0200 Subject: [PATCH] Better autocomplete --- src/Suggester.h | 40 ++++++++-- src/autocomplete.cpp | 170 ++++++++++++++++++++++------------------- src/autocomplete.h | 33 ++++---- src/channelsuggest.cpp | 7 +- src/channelsuggest.h | 4 +- src/searchlineedit.cpp | 52 +------------ src/searchlineedit.h | 44 +---------- src/suggester.h | 40 ++++++++-- src/ytsuggester.cpp | 10 +-- src/ytsuggester.h | 4 +- 10 files changed, 193 insertions(+), 211 deletions(-) diff --git a/src/Suggester.h b/src/Suggester.h index a647ccc..cca7f5f 100644 --- a/src/Suggester.h +++ b/src/Suggester.h @@ -21,22 +21,48 @@ $END_LICENSE */ #ifndef SUGGESTER_H #define SUGGESTER_H -#include -#if QT_VERSION >= 0x050000 -#include -#endif +#include + +class Suggestion { + +public: + Suggestion(QString value = QString(), + QString type = QString()) : + value(value), type(type) { } + QString value; + QString type; + + bool operator==(const Suggestion &other) const { + return (value == other.value) && (type == other.type); + } + + bool operator!=(const Suggestion &other) const { + return !(*this == other); + } + + bool operator==(Suggestion *other) const { + qDebug() << "Comparing" << this << other; + return (value == other->value) && (type == other->type); + } + + bool operator!=(Suggestion *other) const { + return !(this == other); + } + +}; class Suggester : public QObject { Q_OBJECT public: - Suggester(QObject *parent = 0) : QObject(parent) { } - virtual void suggest(QString query) = 0; + Suggester(QObject *parent) : QObject(parent) { } + virtual void suggest(const QString &query) = 0; signals: - void ready(QStringList); + void ready(const QList &suggestions); }; #endif // SUGGESTER_H + diff --git a/src/autocomplete.cpp b/src/autocomplete.cpp index 1678073..e6e0d42 100644 --- a/src/autocomplete.cpp +++ b/src/autocomplete.cpp @@ -1,7 +1,7 @@ /* $BEGIN_LICENSE This file is part of Minitube. -Copyright 2009, Flavio Tordini +Copyright 2013, Flavio Tordini Minitube is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License along with Minitube. If not, see . $END_LICENSE */ - #include "autocomplete.h" #include "suggester.h" #ifdef APP_MAC @@ -26,40 +25,37 @@ $END_LICENSE */ #include "searchlineedit.h" #endif -AutoComplete::AutoComplete(SearchLineEdit *parent, QLineEdit *editor): - QObject(parent), editor(editor), suggester(0) { +AutoComplete::AutoComplete(SearchLineEdit *buddy, QLineEdit *lineEdit): + QObject(buddy), buddy(buddy), lineEdit(lineEdit), suggester(0) { - buddy = parent; enabled = true; - popup = new QListWidget; - popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + popup = new QListWidget(); popup->setMouseTracking(true); - popup->setWindowOpacity(.9); - popup->installEventFilter(this); popup->setWindowFlags(Qt::Popup); + popup->setAttribute(Qt::WA_ShowWithoutActivating); popup->setFocusPolicy(Qt::NoFocus); popup->setFocusProxy(buddy); + popup->installEventFilter(this); + popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + popup->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + popup->setWindowOpacity(.9); + popup->setProperty("suggest", true); popup->setFrameShape(QFrame::NoFrame); popup->setAttribute(Qt::WA_TranslucentBackground); popup->viewport()->setStyleSheet("border:0; border-radius:5px; background:palette(base)"); - connect(popup, SIGNAL(itemClicked(QListWidgetItem*)), SLOT(doneCompletion())); - - // connect(popup, SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem *)), - // SLOT(currentItemChanged(QListWidgetItem *))); - - // mouse hover - // connect(popup, SIGNAL(itemEntered(QListWidgetItem*)), - // SLOT(currentItemChanged(QListWidgetItem *))); + connect(popup, SIGNAL(itemClicked(QListWidgetItem*)), SLOT(acceptSuggestion())); + connect(popup, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), + SLOT(currentItemChanged(QListWidgetItem*))); + connect(popup, SIGNAL(itemEntered(QListWidgetItem*)), SLOT(itemEntered(QListWidgetItem *))); timer = new QTimer(this); timer->setSingleShot(true); - timer->setInterval(600); - connect(timer, SIGNAL(timeout()), SLOT(autoSuggest())); - connect(buddy, SIGNAL(textChanged(QString)), timer, SLOT(start())); - + timer->setInterval(500); + connect(timer, SIGNAL(timeout()), SLOT(suggest())); + connect(buddy, SIGNAL(textEdited(QString)), timer, SLOT(start())); } AutoComplete::~AutoComplete() { @@ -67,62 +63,69 @@ AutoComplete::~AutoComplete() { } bool AutoComplete::eventFilter(QObject *obj, QEvent *ev) { - if (obj != popup) - return false; + if (obj != popup) return false; - if (ev->type() == QEvent::FocusOut) { - popup->hide(); - buddy->setFocus(); + if (ev->type() == QEvent::Leave) { + popup->setCurrentItem(0); + popup->clearSelection(); + if (!originalText.isEmpty()) buddy->setText(originalText); return true; } - if (ev->type() == QEvent::MouseButtonPress) { + if (ev->type() == QEvent::FocusOut) { popup->hide(); - buddy->setFocus(); buddy->setText(originalText); + buddy->setFocus(); return true; } if (ev->type() == QEvent::KeyPress) { - bool consumed = false; - QKeyEvent *keyEvent = static_cast(ev); - int key = keyEvent->key(); - // qDebug() << keyEvent->text(); - switch (key) { + // qWarning() << keyEvent->text(); + switch (keyEvent->key()) { case Qt::Key_Enter: case Qt::Key_Return: if (popup->currentItem()) { - doneCompletion(); + acceptSuggestion(); consumed = true; } else { buddy->setFocus(); - editor->event(ev); + lineEdit->event(ev); popup->hide(); } break; case Qt::Key_Escape: - buddy->setFocus(); - editor->setText(originalText); popup->hide(); + popup->clear(); + buddy->setText(originalText); + buddy->setFocus(); consumed = true; break; case Qt::Key_Up: + if (popup->currentRow() == 0) { + popup->setCurrentItem(0); + popup->clearSelection(); + buddy->setText(originalText); + buddy->setFocus(); + consumed = true; + } + break; + case Qt::Key_Down: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_PageUp: case Qt::Key_PageDown: + // qDebug() << key; break; default: // qDebug() << keyEvent->text(); - buddy->setFocus(); - editor->event(ev); - popup->hide(); + lineEdit->event(ev); + consumed = true; break; } @@ -132,46 +135,50 @@ bool AutoComplete::eventFilter(QObject *obj, QEvent *ev) { return false; } -void AutoComplete::showCompletion(const QStringList &choices) { - - if (choices.isEmpty()) +void AutoComplete::showCompletion(const QList &suggestions) { + if (suggestions.isEmpty()) { + popup->clear(); + popup->hide(); return; - + } popup->setUpdatesEnabled(false); popup->clear(); - for (int i = 0; i < choices.count(); ++i) { + for (int i = 0; i < suggestions.count(); ++i) { QListWidgetItem * item; item = new QListWidgetItem(popup); - item->setText(choices[i]); + Suggestion *s = suggestions[i]; + item->setText(s->value); + if (!s->type.isEmpty()) + item->setIcon(QIcon(":/images/" + s->type + ".png")); } popup->setCurrentItem(0); - popup->adjustSize(); - popup->setUpdatesEnabled(true); - - int h = popup->sizeHintForRow(0) * choices.count() + 4; + int h = 0; + for (int i = 0; i < suggestions.count(); ++i) + h += popup->sizeHintForRow(i); popup->resize(buddy->width(), h); - popup->move(buddy->mapToGlobal(QPoint(0, buddy->height()))); - - popup->setFrameShape(QFrame::NoFrame); - popup->setFocus(); - popup->show(); + + if (popup->isHidden()) popup->show(); + popup->setUpdatesEnabled(true); } -void AutoComplete::doneCompletion() { +void AutoComplete::acceptSuggestion() { timer->stop(); + originalText.clear(); popup->hide(); buddy->setFocus(); - QListWidgetItem *item = popup->currentItem(); - if (item) { - buddy->setText(item->text()); - emit suggestionAccepted(item->text()); - } + int index = popup->currentIndex().row(); + if (index >= 0 && index < suggestions.size()) { + Suggestion* suggestion = suggestions.at(index); + buddy->setText(suggestion->value); + emit suggestionAccepted(suggestion); + emit suggestionAccepted(suggestion->value); + popup->clear(); + } else qWarning() << "No suggestion for index" << index; } void AutoComplete::preventSuggest() { - // qDebug() << "preventSuggest"; timer->stop(); enabled = false; popup->hide(); @@ -179,43 +186,48 @@ void AutoComplete::preventSuggest() { } void AutoComplete::enableSuggest() { - // qDebug() << "enableSuggest"; enabled = true; } void AutoComplete::setSuggester(Suggester* suggester) { if (this->suggester) this->suggester->disconnect(); this->suggester = suggester; - connect(suggester, SIGNAL(ready(QStringList)), SLOT(suggestionsReady(QStringList))); + connect(suggester, SIGNAL(ready(QList)), SLOT(suggestionsReady(QList))); } -void AutoComplete::autoSuggest() { +void AutoComplete::suggest() { if (!enabled) return; if (!buddy->hasFocus()) return; - QString query = editor->text(); - originalText = query; - // qDebug() << "originalText" << originalText; - if (query.isEmpty()) { + popup->setCurrentItem(0); + popup->clearSelection(); + + originalText = buddy->text(); + if (originalText.isEmpty()) { popup->hide(); buddy->setFocus(); return; } - if (suggester) - suggester->suggest(query); + if (suggester) suggester->suggest(originalText); } -void AutoComplete::suggestionsReady(QStringList suggestions) { +void AutoComplete::suggestionsReady(const QList &suggestions) { + qDeleteAll(this->suggestions); + this->suggestions = suggestions; if (!enabled) return; + if (!buddy->hasFocus()) return; showCompletion(suggestions); } -void AutoComplete::currentItemChanged(QListWidgetItem *current) { - if (current) { - // qDebug() << "current" << current->text(); - current->setSelected(true); - buddy->setText(current->text()); - editor->setSelection(originalText.length(), editor->text().length()); - } +void AutoComplete::itemEntered(QListWidgetItem *item) { + if (!item) return; + item->setSelected(true); + popup->setCurrentItem(item); +} + +void AutoComplete::currentItemChanged(QListWidgetItem *item) { + if (!item) return; + buddy->setText(item->text()); + // lineEdit->setSelection(originalText.length(), editor->text().length()); } diff --git a/src/autocomplete.h b/src/autocomplete.h index 65673f9..6894d7e 100644 --- a/src/autocomplete.h +++ b/src/autocomplete.h @@ -17,49 +17,50 @@ You should have received a copy of the GNU General Public License along with Minitube. If not, see . $END_LICENSE */ - -#ifndef SUGGESTCOMPLETION_H -#define SUGGESTCOMPLETION_H +#ifndef AUTOCOMPLETE_H +#define AUTOCOMPLETE_H #include -#if QT_VERSION >= 0x050000 -#include -#endif class Suggester; +class Suggestion; class SearchLineEdit; class AutoComplete : public QObject { + Q_OBJECT public: - AutoComplete(SearchLineEdit *parent, QLineEdit *editor); + AutoComplete(SearchLineEdit *buddy, QLineEdit *lineEdit); ~AutoComplete(); bool eventFilter(QObject *obj, QEvent *ev); - void showCompletion(const QStringList &choices); + void showCompletion(const QList &suggestions); void setSuggester(Suggester* suggester); QListWidget* getPopup() { return popup; } public slots: - void doneCompletion(); + void acceptSuggestion(); void preventSuggest(); void enableSuggest(); - void autoSuggest(); - void currentItemChanged(QListWidgetItem *current); - void suggestionsReady(QStringList suggestions); + void suggest(); + void itemEntered(QListWidgetItem *item); + void currentItemChanged(QListWidgetItem *item); + void suggestionsReady(const QList &suggestions); signals: - void suggestionAccepted(const QString &suggestion); + void suggestionAccepted(Suggestion *suggestion); + void suggestionAccepted(const QString &value); private: SearchLineEdit *buddy; - QLineEdit *editor; + QLineEdit *lineEdit; QString originalText; QListWidget *popup; QTimer *timer; bool enabled; - Suggester* suggester; + Suggester *suggester; + QList suggestions; }; -#endif // SUGGESTCOMPLETION_H +#endif // AUTOCOMPLETE_H diff --git a/src/channelsuggest.cpp b/src/channelsuggest.cpp index 00f6e90..c252802 100644 --- a/src/channelsuggest.cpp +++ b/src/channelsuggest.cpp @@ -29,7 +29,7 @@ ChannelSuggest::ChannelSuggest(QObject *parent) : Suggester(parent) { } -void ChannelSuggest::suggest(QString query) { +void ChannelSuggest::suggest(const QString &query) { QUrl url("http://www.youtube.com/results"); #if QT_VERSION >= 0x050000 { @@ -48,6 +48,8 @@ void ChannelSuggest::suggest(QString query) { void ChannelSuggest::handleNetworkData(QByteArray data) { QStringList choices; + QList suggestions; + QString html = QString::fromUtf8(data); QRegExp re("/user/([a-zA-Z0-9]+)"); @@ -56,11 +58,12 @@ void ChannelSuggest::handleNetworkData(QByteArray data) { // qDebug() << re.cap(0) << re.cap(1); QString choice = re.cap(1); if (!choices.contains(choice, Qt::CaseInsensitive)) { + suggestions << new Suggestion(choice); choices << choice; if (choices.size() == 10) break; } pos += re.matchedLength(); } - emit ready(choices); + emit ready(suggestions); } diff --git a/src/channelsuggest.h b/src/channelsuggest.h index c4bdec9..bde29c5 100644 --- a/src/channelsuggest.h +++ b/src/channelsuggest.h @@ -31,10 +31,10 @@ class ChannelSuggest : public Suggester { public: ChannelSuggest(QObject *parent = 0); - void suggest(QString query); + void suggest(const QString &query); signals: - void ready(QStringList); + void ready(const QList &suggestions); private slots: void handleNetworkData(QByteArray response); diff --git a/src/searchlineedit.cpp b/src/searchlineedit.cpp index 93008d0..20b50d2 100644 --- a/src/searchlineedit.cpp +++ b/src/searchlineedit.cpp @@ -1,44 +1,3 @@ -/**************************************************************************** -** -** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** This file is part of the demonstration applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial Usage -** Licensees holding valid Qt Commercial licenses may use this file in -** accordance with the Qt Commercial License Agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Nokia. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain -** additional rights. These rights are described in the Nokia Qt LGPL -** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this -** package. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** If you are unsure which license is appropriate for your use, please -** contact the sales department at http://www.qtsoftware.com/contact. -** $QT_END_LICENSE$ -** -****************************************************************************/ - #include "searchlineedit.h" #include @@ -167,11 +126,9 @@ void SearchButton::paintEvent(QPaintEvent *event) SearchLineEdit::SearchLineEdit(QWidget *parent) : ExLineEdit(parent), m_searchButton(new SearchButton(this)) { - connect(lineEdit(), SIGNAL(textChanged(const QString &)), - this, SIGNAL(textChanged(const QString &))); - - connect(lineEdit(), SIGNAL(returnPressed()), - this, SLOT(returnPressed())); + connect(lineEdit(), SIGNAL(textChanged(const QString &)), SIGNAL(textChanged(const QString &))); + connect(lineEdit(), SIGNAL(textEdited(const QString &)), SIGNAL(textEdited(const QString &))); + connect(lineEdit(), SIGNAL(returnPressed()), SLOT(returnPressed())); setLeftWidget(m_searchButton); m_inactiveText = tr("Search"); @@ -181,8 +138,7 @@ m_searchButton(new SearchButton(this)) // completion completion = new AutoComplete(this, m_lineEdit); - connect(completion, SIGNAL(suggestionAccepted(const QString &)), SIGNAL(suggestionAccepted(const QString &))); - + connect(completion, SIGNAL(suggestionAccepted(Suggestion*)), SIGNAL(suggestionAccepted(Suggestion*))); } void SearchLineEdit::paintEvent(QPaintEvent *event) diff --git a/src/searchlineedit.h b/src/searchlineedit.h index 3403df4..2124565 100644 --- a/src/searchlineedit.h +++ b/src/searchlineedit.h @@ -1,44 +1,3 @@ -/**************************************************************************** -** -** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** This file is part of the demonstration applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial Usage -** Licensees holding valid Qt Commercial licenses may use this file in -** accordance with the Qt Commercial License Agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Nokia. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain -** additional rights. These rights are described in the Nokia Qt LGPL -** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this -** package. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** If you are unsure which license is appropriate for your use, please -** contact the sales department at http://www.qtsoftware.com/contact. -** $QT_END_LICENSE$ -** -****************************************************************************/ - #ifndef SEARCHLINEEDIT_H #define SEARCHLINEEDIT_H @@ -80,6 +39,7 @@ class SearchLineEdit : public ExLineEdit signals: void textChanged(const QString &text); + void textEdited(const QString &text); void search(const QString &text); void suggestionAccepted(const QString &suggestion); @@ -107,10 +67,8 @@ private slots: void returnPressed(); private: - SearchButton *m_searchButton; QString m_inactiveText; - AutoComplete *completion; }; diff --git a/src/suggester.h b/src/suggester.h index a647ccc..cca7f5f 100644 --- a/src/suggester.h +++ b/src/suggester.h @@ -21,22 +21,48 @@ $END_LICENSE */ #ifndef SUGGESTER_H #define SUGGESTER_H -#include -#if QT_VERSION >= 0x050000 -#include -#endif +#include + +class Suggestion { + +public: + Suggestion(QString value = QString(), + QString type = QString()) : + value(value), type(type) { } + QString value; + QString type; + + bool operator==(const Suggestion &other) const { + return (value == other.value) && (type == other.type); + } + + bool operator!=(const Suggestion &other) const { + return !(*this == other); + } + + bool operator==(Suggestion *other) const { + qDebug() << "Comparing" << this << other; + return (value == other->value) && (type == other->type); + } + + bool operator!=(Suggestion *other) const { + return !(this == other); + } + +}; class Suggester : public QObject { Q_OBJECT public: - Suggester(QObject *parent = 0) : QObject(parent) { } - virtual void suggest(QString query) = 0; + Suggester(QObject *parent) : QObject(parent) { } + virtual void suggest(const QString &query) = 0; signals: - void ready(QStringList); + void ready(const QList &suggestions); }; #endif // SUGGESTER_H + diff --git a/src/ytsuggester.cpp b/src/ytsuggester.cpp index c526b30..a11824e 100644 --- a/src/ytsuggester.cpp +++ b/src/ytsuggester.cpp @@ -32,7 +32,7 @@ YTSuggester::YTSuggester(QObject *parent) : Suggester(parent) { } -void YTSuggester::suggest(QString query) { +void YTSuggester::suggest(const QString &query) { if (query.startsWith("http")) return; #if QT_VERSION >= 0x040800 @@ -53,17 +53,17 @@ void YTSuggester::suggest(QString query) { } void YTSuggester::handleNetworkData(QByteArray response) { - QStringList choices; - + QList suggestions; QXmlStreamReader xml(response); while (!xml.atEnd()) { xml.readNext(); if (xml.tokenType() == QXmlStreamReader::StartElement) { if (xml.name() == QLatin1String("suggestion")) { QStringRef str = xml.attributes().value("data"); - choices << str.toString(); + QString value = str.toString(); + suggestions << new Suggestion(value); } } } - emit ready(choices); + emit ready(suggestions); } diff --git a/src/ytsuggester.h b/src/ytsuggester.h index 0599ff8..fc27153 100644 --- a/src/ytsuggester.h +++ b/src/ytsuggester.h @@ -30,10 +30,10 @@ class YTSuggester : public Suggester { public: YTSuggester(QObject *parent = 0); - void suggest(QString query); + void suggest(const QString &query); signals: - void ready(QStringList); + void ready(const QList &suggestions); private slots: void handleNetworkData(QByteArray response); -- 2.39.5