diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index 39e4077f5..1005d36db 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -761,11 +761,6 @@ void MainWindow::showProjectSaveError(RzProjectErr err) tr("Failed to save project: %1").arg(QString::fromUtf8(s))); } -void MainWindow::refreshOmniBar(const QStringList &flags) -{ - omnibar->refresh(flags); -} - void MainWindow::setFilename(const QString &fn) { // Add file name to window title diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index 72a265b8d..3b49a9cd4 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -86,7 +86,6 @@ class CUTTER_EXPORT MainWindow : public QMainWindow void readSettings(); void saveSettings(); void setFilename(const QString &fn); - void refreshOmniBar(const QStringList &flags); void addWidget(CutterDockWidget *widget); void addMemoryDockWidget(MemoryDockWidget *widget); diff --git a/src/shortcuts/DefaultShortcuts.cpp b/src/shortcuts/DefaultShortcuts.cpp index 0b7032ca2..5e42a1fc0 100644 --- a/src/shortcuts/DefaultShortcuts.cpp +++ b/src/shortcuts/DefaultShortcuts.cpp @@ -288,6 +288,14 @@ const QHash &getDefaultShortcuts() { { QKeySequence(Qt::Key_Escape) }, QT_TRANSLATE_NOOP("Omnibar", "Clear Omnibar"), "Omnibar" } }, + { "Omnibar.showMore", + { { QKeySequence(Qt::ControlModifier | Qt::Key_Return) }, + QT_TRANSLATE_NOOP("Omnibar", "Show More Completions"), + "Omnibar" } }, + { "Omnibar.showAll", + { { QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Return) }, + QT_TRANSLATE_NOOP("Omnibar", "Show All Completions"), + "Omnibar" } }, // Graph Overview { "Overview.zoomIn", diff --git a/src/widgets/FlagsWidget.cpp b/src/widgets/FlagsWidget.cpp index 25859a26a..b7abf02d3 100644 --- a/src/widgets/FlagsWidget.cpp +++ b/src/widgets/FlagsWidget.cpp @@ -268,12 +268,6 @@ void FlagsWidget::refreshFlags() flags_model->endResetModel(); tree->showItemsNumber(flags_proxy_model->rowCount()); - - // TODO: this is not a very good place for the following: - QStringList flagNames; - for (const FlagDescription &i : flags_model->flags) - flagNames.append(i.name); - main->refreshOmniBar(flagNames); } void FlagsWidget::setScrollMode() diff --git a/src/widgets/Omnibar.cpp b/src/widgets/Omnibar.cpp index 584f49a3f..bfa14c176 100644 --- a/src/widgets/Omnibar.cpp +++ b/src/widgets/Omnibar.cpp @@ -7,8 +7,39 @@ #include #include #include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr int DEFAULT_ITEM_COUNT = 100; +} + +OmnibarCompleter::OmnibarCompleter(QObject *parent) : QCompleter(parent) {} -Omnibar::Omnibar(MainWindow *main, QWidget *parent) : QLineEdit(parent), main(main) +QStringList OmnibarCompleter::splitPath(const QString &) const +{ + return QStringList(""); +} + +QString OmnibarCompleter::pathFromIndex(const QModelIndex &index) const +{ + int type = index.data(Qt::UserRole).toInt(); + + if (type == Omnibar::ShowMore || type == Omnibar::ShowAll) { + if (auto edit = qobject_cast(widget())) { + return edit->text(); + } + } + + return QCompleter::pathFromIndex(index); +} + +Omnibar::Omnibar(MainWindow *main, QWidget *parent) + : QLineEdit(parent), main(main), m_completerModel(new QStandardItemModel(this)) { // QLineEdit basic features this->setMinimumHeight(16); @@ -18,53 +49,108 @@ Omnibar::Omnibar(MainWindow *main, QWidget *parent) : QLineEdit(parent), main(ma this->setTextMargins(10, 0, 0, 0); this->setClearButtonEnabled(true); - connect(this, &QLineEdit::returnPressed, this, &Omnibar::on_gotoEntry_returnPressed); + connect(this, &QLineEdit::textEdited, this, [this](const QString &text) { + m_maxShownItems = DEFAULT_ITEM_COUNT; // reset the entries count to 100 + handleSearch(text); + }); // Esc clears omnibar QShortcut *clear_shortcut = Shortcuts()->makeQShortcut("Omnibar.clear", this); connect(clear_shortcut, &QShortcut::activated, this, &Omnibar::clear); clear_shortcut->setContext(Qt::WidgetWithChildrenShortcut); -} -void Omnibar::setupCompleter() -{ // Set gotoEntry completer for jump history - QCompleter *completer = new QCompleter(flags, this); - completer->setMaxVisibleItems(20); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - - this->setCompleter(completer); + m_completer = new OmnibarCompleter(this); + m_completer->setModel(m_completerModel); + m_completer->setMaxVisibleItems(20); + m_completer->setFilterMode(Qt::MatchContains); + m_completer->setCompletionMode(QCompleter::PopupCompletion); + m_completer->popup()->installEventFilter(this); + m_completer->popup()->viewport()->installEventFilter(this); + + this->setCompleter(m_completer); + this->installEventFilter(this); + + connect(Core(), &CutterCore::flagsChanged, this, &Omnibar::refresh); + connect(Core(), &CutterCore::codeRebased, this, &Omnibar::refresh); + connect(Core(), &CutterCore::refreshAll, this, &Omnibar::refresh); } -void Omnibar::refresh(const QStringList &flagList) +bool Omnibar::eventFilter(QObject *obj, QEvent *event) { - flags = flagList; + QAbstractItemView *popup = m_completer->popup(); + + if (((obj == popup || obj == this) && (event->type() == QEvent::KeyPress)) + || (obj == popup->viewport() && event->type() == QEvent::MouseButtonPress)) { + + QModelIndex index; + + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + index = popup->indexAt(mouseEvent->pos()); + } + } else if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + int keyInt = keyEvent->modifiers() | keyEvent->key(); + QKeySequence eventSeq(keyInt); + if (eventSeq == Shortcuts()->getKeySequence("Omnibar.showMore")) { + handleShowMore(popup->currentIndex().row()); + return true; + } else if (eventSeq == Shortcuts()->getKeySequence("Omnibar.showAll")) { + handleShowAll(popup->currentIndex().row()); + return true; + } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { + index = popup->currentIndex(); + } + } + + if (index.isValid()) { + int row = index.row(); + int type = index.data(Qt::UserRole).toInt(); - setupCompleter(); + if (type == ItemType::ShowMore) { + handleShowMore(row); + } else if (type == ItemType::ShowAll) { + handleShowAll(row); + } else { + goToEntry(); + popup->hide(); + } + return true; + } + } + return QObject::eventFilter(obj, event); } -void Omnibar::restoreCompleter() +void Omnibar::refresh() { - QCompleter *completer = this->completer(); - if (!completer) { - return; + m_flags.clear(); + const auto flags = Core()->getAllFlags(); + for (const auto &f : flags) { + m_flags.append(f.name); } - completer->setFilterMode(Qt::MatchContains); + + QCollator collator; + collator.setNumericMode(true); + collator.setCaseSensitivity(Qt::CaseInsensitive); + std::sort(m_flags.begin(), m_flags.end(), [&collator](const QString &a, const QString &b) { + return collator.compare(a, b) < 0; + }); + + handleSearch(this->text()); } void Omnibar::clear() { QLineEdit::clear(); - // Close the potential shown completer popup - clearFocus(); - setFocus(); + m_completerModel->clear(); + m_completer->popup()->hide(); } -void Omnibar::on_gotoEntry_returnPressed() +void Omnibar::goToEntry() { QString str = this->text(); if (!str.isEmpty()) { @@ -79,5 +165,92 @@ void Omnibar::on_gotoEntry_returnPressed() this->setText(""); this->clearFocus(); - this->restoreCompleter(); +} + +void Omnibar::handleSearch(const QString &Text, bool append) +{ + m_searchedText = Text; + m_matchCount = 0; + + if (!append) { + m_completerModel->clear(); + m_lastIndex = 0; + } else { + // remove "show more" and "show all" rows + m_completerModel->removeRows(m_completerModel->rowCount() - 2, 2); + } + + if (Text.isEmpty()) { + return; + } + + bool hasMore = false; + int itemsInModel = m_completerModel->rowCount(); + + for (int i = 0; i < m_flags.size(); ++i) { + const QString &flag = m_flags[i]; + if (flag.contains(Text, Qt::CaseInsensitive)) { + if (itemsInModel < m_maxShownItems) { + if (!append || i > m_lastIndex) { + QStandardItem *item = new QStandardItem(flag); + item->setData(ItemType::Standard, Qt::UserRole); + m_completerModel->appendRow(item); + ++itemsInModel; + m_lastIndex = i; + } + } else { + hasMore = true; + } + ++m_matchCount; + } + } + + if (hasMore) { + auto addActionRow = [&](const QString &text, ItemType type) { + QStandardItem *actionItem = new QStandardItem(text); + actionItem->setData(type, Qt::UserRole); + + QPalette palette = this->palette(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + QColor placeholderColor = palette.color(QPalette::PlaceholderText); +#else + QColor placeholderColor = palette.color(QPalette::Text); + placeholderColor.setAlpha(128); +#endif + actionItem->setForeground(placeholderColor); + + QFont font = actionItem->font(); + font.setItalic(true); + actionItem->setFont(font); + + m_completerModel->appendRow(actionItem); + }; + + addActionRow(tr("Show More"), ItemType::ShowMore); + addActionRow(tr("Show All (%1 of %2)").arg(m_maxShownItems).arg(m_matchCount), + ItemType::ShowAll); + } + + m_completer->setCompletionPrefix(""); + m_completer->complete(); +} + +void Omnibar::handleShowMore(int currentRow) +{ + if (m_maxShownItems >= m_matchCount) { + return; + } + m_maxShownItems = std::min(m_maxShownItems + DEFAULT_ITEM_COUNT, m_matchCount); + handleSearch(m_searchedText, true); + m_completer->popup()->setCurrentIndex(m_completerModel->index(currentRow, 0)); +} + +void Omnibar::handleShowAll(int currentRow) +{ + if (m_maxShownItems >= m_matchCount) { + return; + } + m_maxShownItems = m_matchCount; + handleSearch(m_searchedText, true); + m_completer->popup()->setCurrentIndex(m_completerModel->index(currentRow, 0)); } diff --git a/src/widgets/Omnibar.h b/src/widgets/Omnibar.h index b3dc05e34..ccab0795f 100644 --- a/src/widgets/Omnibar.h +++ b/src/widgets/Omnibar.h @@ -2,8 +2,28 @@ #define OMNIBAR_H #include +#include class MainWindow; +class QStandardItemModel; + +class OmnibarCompleter : public QCompleter +{ +public: + explicit OmnibarCompleter(QObject *parent = nullptr); + + /** + * @brief Force the completer to match everything, since we are handling filtering on our own + */ + QStringList splitPath(const QString &) const override; + + /** + * @brief Do not auto-complete when selecting "Show More" or "Show All" entries + * + * Keeps the text the same for "Show More" and "Show All", auto-completes for other entries + */ + QString pathFromIndex(const QModelIndex &index) const override; +}; class Omnibar : public QLineEdit { @@ -11,21 +31,50 @@ class Omnibar : public QLineEdit public: explicit Omnibar(MainWindow *main, QWidget *parent = nullptr); - void refresh(const QStringList &flagList); + void refresh(); + + enum ItemType { Standard = 0, ShowMore = 1, ShowAll = 2 }; private slots: - void on_gotoEntry_returnPressed(); + /** + * @brief Seeks to the address of selected entry/flag and shows it in the last memory widget + */ + void goToEntry(); + + /** + * @brief Filters or searches entries "containing" the text + * + * Appends "Show More" and "Show All" entries at the end if there are more found matches then + * currently shown + * + * @param text The string to match against + * @param append If false, resets the search to the beginning. If true, continues from the last + * found position + */ + void handleSearch(const QString &text, bool append = false); - void restoreCompleter(); +protected: + bool eventFilter(QObject *obj, QEvent *event) override; public slots: void clear(); private: void setupCompleter(); + void handleShowMore(int currentRow); + void handleShowAll(int currentRow); MainWindow *main; - QStringList flags; + QStringList m_flags; + + QStringList m_filteredFlags; + QStandardItemModel *m_completerModel; + QCompleter *m_completer; + + QString m_searchedText; + int m_maxShownItems; + int m_lastIndex; + int m_matchCount; }; #endif // OMNIBAR_H