|
@@ -0,0 +1,233 @@
|
|
|
+#include "quickselector.h"
|
|
|
+
|
|
|
+#include <QVBoxLayout>
|
|
|
+#include <QWidgetAction>
|
|
|
+#include <QMenu>
|
|
|
+#include <QLabel>
|
|
|
+#include <QListWidgetItem>
|
|
|
+#include <QKeyEvent>
|
|
|
+#include <QDebug>
|
|
|
+#include <QRegularExpression>
|
|
|
+
|
|
|
+#include <utils/widgetutils.h>
|
|
|
+#include <utils/iconutils.h>
|
|
|
+
|
|
|
+#include "lineedit.h"
|
|
|
+#include "listwidget.h"
|
|
|
+#include "widgetsfactory.h"
|
|
|
+
|
|
|
+using namespace vnotex;
|
|
|
+
|
|
|
+QuickSelectorItem::QuickSelectorItem(const QVariant &p_key,
|
|
|
+ const QString &p_name,
|
|
|
+ const QString &p_tip,
|
|
|
+ const QString &p_shortcut)
|
|
|
+ : m_key(p_key),
|
|
|
+ m_name(p_name),
|
|
|
+ m_tip(p_tip),
|
|
|
+ m_shortcut(p_shortcut)
|
|
|
+{
|
|
|
+ Q_ASSERT(m_shortcut.size() < 3);
|
|
|
+}
|
|
|
+
|
|
|
+static bool selectorItemCmp(const QuickSelectorItem &p_a, const QuickSelectorItem &p_b)
|
|
|
+{
|
|
|
+ if (p_a.m_shortcut.isEmpty()) {
|
|
|
+ if (p_b.m_shortcut.isEmpty()) {
|
|
|
+ return p_a.m_name < p_b.m_name;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ if (p_b.m_shortcut.isEmpty()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return p_a.m_shortcut < p_b.m_shortcut;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+QuickSelector::QuickSelector(const QString &p_title,
|
|
|
+ const QVector<QuickSelectorItem> &p_items,
|
|
|
+ bool p_sortByShortcut,
|
|
|
+ QWidget *p_parent)
|
|
|
+ : FloatingWidget(p_parent),
|
|
|
+ m_items(p_items)
|
|
|
+{
|
|
|
+ if (p_sortByShortcut) {
|
|
|
+ std::sort(m_items.begin(), m_items.end(), selectorItemCmp);
|
|
|
+ }
|
|
|
+
|
|
|
+ setupUI(p_title);
|
|
|
+
|
|
|
+ updateItemList();
|
|
|
+}
|
|
|
+
|
|
|
+void QuickSelector::setupUI(const QString &p_title)
|
|
|
+{
|
|
|
+ auto mainLayout = new QVBoxLayout(this);
|
|
|
+
|
|
|
+ if (!p_title.isEmpty()) {
|
|
|
+ mainLayout->addWidget(new QLabel(p_title, this));
|
|
|
+ }
|
|
|
+
|
|
|
+ m_searchLineEdit = WidgetsFactory::createLineEdit(this);
|
|
|
+ connect(m_searchLineEdit, &QLineEdit::textEdited,
|
|
|
+ this, &QuickSelector::searchAndFilter);
|
|
|
+ mainLayout->addWidget(m_searchLineEdit);
|
|
|
+
|
|
|
+ setFocusProxy(m_searchLineEdit);
|
|
|
+ m_searchLineEdit->installEventFilter(this);
|
|
|
+
|
|
|
+ m_itemList = new ListWidget(this);
|
|
|
+ m_itemList->setWrapping(true);
|
|
|
+ m_itemList->setFlow(QListView::LeftToRight);
|
|
|
+ m_itemList->setIconSize(QSize(18, 18));
|
|
|
+ connect(m_itemList, &QListWidget::itemActivated,
|
|
|
+ this, &QuickSelector::activateItem);
|
|
|
+ mainLayout->addWidget(m_itemList);
|
|
|
+
|
|
|
+ m_itemList->installEventFilter(this);
|
|
|
+}
|
|
|
+
|
|
|
+void QuickSelector::updateItemList()
|
|
|
+{
|
|
|
+ m_itemList->clear();
|
|
|
+
|
|
|
+ for (int i = 0; i < m_items.size(); ++i) {
|
|
|
+ const auto &item = m_items[i];
|
|
|
+
|
|
|
+ auto listItem = new QListWidgetItem(m_itemList);
|
|
|
+ auto icon = IconUtils::drawTextIcon(item.m_shortcut, "blue", "darkgreen");
|
|
|
+ listItem->setIcon(icon);
|
|
|
+
|
|
|
+ listItem->setText(item.m_name);
|
|
|
+ listItem->setToolTip(item.m_tip);
|
|
|
+ listItem->setData(Qt::UserRole, i);
|
|
|
+ }
|
|
|
+
|
|
|
+ Q_ASSERT(!m_items.isEmpty());
|
|
|
+ m_itemList->setCurrentRow(0);
|
|
|
+}
|
|
|
+
|
|
|
+void QuickSelector::activateItem(const QListWidgetItem *p_item)
|
|
|
+{
|
|
|
+ if (p_item) {
|
|
|
+ m_selectedKey = getSelectorItem(p_item).m_key;
|
|
|
+ }
|
|
|
+ finish();
|
|
|
+}
|
|
|
+
|
|
|
+void QuickSelector::activate(const QuickSelectorItem *p_item)
|
|
|
+{
|
|
|
+ m_selectedKey = p_item->m_key;
|
|
|
+ finish();
|
|
|
+}
|
|
|
+
|
|
|
+QuickSelectorItem &QuickSelector::getSelectorItem(const QListWidgetItem *p_item)
|
|
|
+{
|
|
|
+ Q_ASSERT(p_item);
|
|
|
+ return m_items[p_item->data(Qt::UserRole).toInt()];
|
|
|
+}
|
|
|
+
|
|
|
+QVariant QuickSelector::result() const
|
|
|
+{
|
|
|
+ return m_selectedKey;
|
|
|
+}
|
|
|
+
|
|
|
+bool QuickSelector::eventFilter(QObject *p_obj, QEvent *p_event)
|
|
|
+{
|
|
|
+ if ((p_obj == m_searchLineEdit || p_obj == m_itemList)
|
|
|
+ && p_event->type() == QEvent::KeyPress) {
|
|
|
+ auto keyEve = static_cast<QKeyEvent *>(p_event);
|
|
|
+ const auto key = keyEve->key();
|
|
|
+ if (key == Qt::Key_Tab || key == Qt::Key_Backtab) {
|
|
|
+ // Change focus.
|
|
|
+ if (p_obj == m_searchLineEdit) {
|
|
|
+ m_itemList->setFocus();
|
|
|
+ } else {
|
|
|
+ m_searchLineEdit->setFocus();
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ } else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
|
|
+ if (p_obj == m_searchLineEdit) {
|
|
|
+ activateItem(m_itemList->currentItem());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return FloatingWidget::eventFilter(p_obj, p_event);
|
|
|
+}
|
|
|
+
|
|
|
+void QuickSelector::searchAndFilter(const QString &p_text)
|
|
|
+{
|
|
|
+ auto text = p_text.trimmed();
|
|
|
+ if (text.isEmpty()) {
|
|
|
+ // Show all items.
|
|
|
+ filterItems([](const QuickSelectorItem &) {
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ } else if (text.size() < 3) {
|
|
|
+ // Check shortcut first.
|
|
|
+ const QuickSelectorItem *hitItem = nullptr;
|
|
|
+ int ret = filterItems([&text, &hitItem](const QuickSelectorItem &p_item) {
|
|
|
+ if (p_item.m_shortcut == text) {
|
|
|
+ hitItem = &p_item;
|
|
|
+ return true;
|
|
|
+ } else if (p_item.m_shortcut.startsWith(text)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (hitItem) {
|
|
|
+ activate(hitItem);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ret > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check name.
|
|
|
+ auto parts = text.split(QLatin1Char(' '), QString::SkipEmptyParts);
|
|
|
+ Q_ASSERT(!parts.isEmpty());
|
|
|
+ QRegularExpression regExp;
|
|
|
+ regExp.setPatternOptions(regExp.patternOptions() | QRegularExpression::CaseInsensitiveOption);
|
|
|
+ if (parts.size() == 1) {
|
|
|
+ regExp.setPattern(QRegularExpression::escape(parts[0]));
|
|
|
+ } else {
|
|
|
+ QString pattern = QRegularExpression::escape(parts[0]);
|
|
|
+ for (int i = 1; i < parts.size(); ++i) {
|
|
|
+ pattern += ".*" + QRegularExpression::escape(parts[i]);
|
|
|
+ }
|
|
|
+ regExp.setPattern(pattern);
|
|
|
+ }
|
|
|
+ filterItems([®Exp](const QuickSelectorItem &p_item) {
|
|
|
+ if (p_item.m_name.indexOf(regExp) != -1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+int QuickSelector::filterItems(const std::function<bool(const QuickSelectorItem &)> &p_judge)
|
|
|
+{
|
|
|
+ const int cnt = m_itemList->count();
|
|
|
+ int matchedCnt = 0;
|
|
|
+ int firstHit = -1;
|
|
|
+ for (int i = 0; i < cnt; ++i) {
|
|
|
+ auto item = m_itemList->item(i);
|
|
|
+ bool hit = p_judge(getSelectorItem(item));
|
|
|
+ if (hit) {
|
|
|
+ if (matchedCnt == 0) {
|
|
|
+ firstHit = i;
|
|
|
+ }
|
|
|
+ ++matchedCnt;
|
|
|
+ }
|
|
|
+ item->setHidden(!hit);
|
|
|
+ }
|
|
|
+ m_itemList->setCurrentRow(firstHit);
|
|
|
+ return matchedCnt;
|
|
|
+}
|