From 9a7729ebdcb3933c35e264f58727f941780152ce Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Thu, 19 Nov 2009 17:47:13 +0100 Subject: [PATCH] Preferences: Set up infrastructure for a filter widget. Add filter widget, use a special QSortFilterProxyModel + StandardItemModel, add a slot selecting the page. Add matches() to IOptionsPage. --- .../coreplugin/dialogs/ioptionspage.cpp | 1 + src/plugins/coreplugin/dialogs/ioptionspage.h | 1 + .../coreplugin/dialogs/settingsdialog.cpp | 316 +++++++++++++----- .../coreplugin/dialogs/settingsdialog.h | 16 +- .../coreplugin/dialogs/settingsdialog.ui | 50 +-- src/plugins/vcsbase/vcsbasesettingspage.cpp | 17 + src/plugins/vcsbase/vcsbasesettingspage.h | 5 + 7 files changed, 304 insertions(+), 102 deletions(-) diff --git a/src/plugins/coreplugin/dialogs/ioptionspage.cpp b/src/plugins/coreplugin/dialogs/ioptionspage.cpp index e6395e937df..ade65ea3688 100644 --- a/src/plugins/coreplugin/dialogs/ioptionspage.cpp +++ b/src/plugins/coreplugin/dialogs/ioptionspage.cpp @@ -42,5 +42,6 @@ \o trCategory() is the translated category \o apply() is called to store the settings. It should detect if any changes have been made and store those. + \o matches() is used for the options dialog search filter. \endlist */ diff --git a/src/plugins/coreplugin/dialogs/ioptionspage.h b/src/plugins/coreplugin/dialogs/ioptionspage.h index 0a1ebfc21c0..f7c0c10a951 100644 --- a/src/plugins/coreplugin/dialogs/ioptionspage.h +++ b/src/plugins/coreplugin/dialogs/ioptionspage.h @@ -51,6 +51,7 @@ public: virtual QString trName() const = 0; virtual QString category() const = 0; virtual QString trCategory() const = 0; + virtual bool matches(const QString & /* searchKeyWord*/) const { return false; } virtual QWidget *createPage(QWidget *parent) = 0; virtual void apply() = 0; diff --git a/src/plugins/coreplugin/dialogs/settingsdialog.cpp b/src/plugins/coreplugin/dialogs/settingsdialog.cpp index 0b42742a621..f2794d31949 100644 --- a/src/plugins/coreplugin/dialogs/settingsdialog.cpp +++ b/src/plugins/coreplugin/dialogs/settingsdialog.cpp @@ -32,26 +32,152 @@ #include #include "icore.h" +#include + #include #include #include +#include +#include +#include +#include +#include -namespace { - struct PageData { - int index; - QString category; - QString id; - }; +enum ItemType { CategoryItem, PageItem }; + +enum { TypeRole = Qt::UserRole + 1, + IndexRole = Qt::UserRole + 2, + PageRole = Qt::UserRole + 3 }; + +Q_DECLARE_METATYPE(Core::IOptionsPage*) + +static const char categoryKeyC[] = "General/LastPreferenceCategory"; +static const char pageKeyC[] = "General/LastPreferencePage"; + +namespace Core { +namespace Internal { + +// Create item on either model or parent item +template + inline QStandardItem *createStandardItem(Parent *parent, + const QString &text, + ItemType type = CategoryItem, + int index = -1, + IOptionsPage *page = 0) +{ + QStandardItem *rc = new QStandardItem(text); + rc->setData(QVariant(int(type)), TypeRole); + rc->setData(QVariant(index), IndexRole); + rc->setData(qVariantFromValue(page), PageRole); + parent->appendRow(rc); + return rc; } -Q_DECLARE_METATYPE(::PageData); +static inline ItemType itemTypeOfItem(const QStandardItem *item) +{ + return static_cast(item->data(TypeRole).toInt()); +} -using namespace Core; -using namespace Core::Internal; +static inline ItemType itemTypeOfItem(const QAbstractItemModel *model, const QModelIndex &index) +{ + return static_cast(model->data(index, TypeRole).toInt()); +} + +static inline int indexOfItem(const QStandardItem *item) +{ + return item->data(IndexRole).toInt(); +} + +static inline IOptionsPage *pageOfItem(const QStandardItem *item) +{ + return qvariant_cast(item->data(PageRole)); +} + +static inline IOptionsPage *pageOfItem(const QAbstractItemModel *model, const QModelIndex &index) +{ + return qvariant_cast(model->data(index, PageRole)); +} + +// A filter model that returns true for the parent (category) nodes +// (which by default do not match the search string and are thus collapsed) +// and additionally checks IOptionsPage::matches(). +class PageFilterModel : public QSortFilterProxyModel { +public: + explicit PageFilterModel(QObject *parent = 0) : QSortFilterProxyModel(parent) {} +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; +}; + +bool PageFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!source_parent.isValid()) + return true; // Always true for parents/categories. + // Regular contents check, then check page-filter. + if (QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent)) + return true; + if (const IOptionsPage *page = pageOfItem(sourceModel(), source_parent.child(source_row, 0))) + return page->matches(filterRegExp().pattern()); + return false; +} + +// Populate a model with pages. +static QStandardItemModel *pageModel(const QList &pages, + QObject *parent, + const QString &initialCategory, + const QString &initialPageId, + QModelIndex *initialIndex) +{ + QStandardItemModel *model = new QStandardItemModel(0, 1, parent); + const QChar hierarchySeparator = QLatin1Char('|'); + int index = 0; + QMap categories; + foreach (IOptionsPage *page, pages) { + const QStringList categoriesId = page->category().split(hierarchySeparator); + const QStringList trCategories = page->trCategory().split(hierarchySeparator); + const int categoryDepth = categoriesId.size(); + if (categoryDepth != trCategories.size()) { + qWarning("Internal error: Hierarchy mismatch in settings page %s.", qPrintable(page->id())); + continue; + } + + // Retrieve/Create parent items for nested categories "Cat1|Cat2|Cat3" + QString currentCategory = categoriesId.at(0); + QStandardItem *treeItem = categories.value(currentCategory, 0); + if (!treeItem) { + treeItem = createStandardItem(model, trCategories.at(0), CategoryItem); + categories.insert(currentCategory, treeItem); + } + + for (int cat = 1; cat < categoryDepth; cat++) { + const QString fullCategory = currentCategory + hierarchySeparator + categoriesId.at(cat); + treeItem = categories.value(fullCategory, 0); + if (!treeItem) { + QStandardItem *parentItem = categories.value(currentCategory); + QTC_ASSERT(parentItem, return model) + treeItem = createStandardItem(parentItem, trCategories.at(cat), CategoryItem); + categories.insert(fullCategory, treeItem); + } + currentCategory = fullCategory; + } + + // Append page item + QTC_ASSERT(treeItem, return model) + QStandardItem *item = createStandardItem(treeItem, page->trName(), PageItem, index, page); + if (currentCategory == initialCategory && page->id() == initialPageId) { + *initialIndex = model->indexFromItem(item); + } + index++; + } + return model; +} SettingsDialog::SettingsDialog(QWidget *parent, const QString &categoryId, - const QString &pageId) - : QDialog(parent), m_applied(false) + const QString &pageId) : + QDialog(parent), + m_pages(ExtensionSystem::PluginManager::instance()->getObjects()), + m_proxyModel(new PageFilterModel), + m_model(0), + m_applied(false) { setupUi(this); #ifdef Q_OS_MAC @@ -63,72 +189,31 @@ SettingsDialog::SettingsDialog(QWidget *parent, const QString &categoryId, QString initialPage = pageId; if (initialCategory.isEmpty() && initialPage.isEmpty()) { QSettings *settings = ICore::instance()->settings(); - initialCategory = settings->value("General/LastPreferenceCategory", QVariant(QString())).toString(); - initialPage = settings->value("General/LastPreferencePage", QVariant(QString())).toString(); + initialCategory = settings->value(QLatin1String(categoryKeyC), QVariant(QString())).toString(); + initialPage = settings->value(QLatin1String(pageKeyC), QVariant(QString())).toString(); } buttonBox->button(QDialogButtonBox::Ok)->setDefault(true); connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(apply())); + foreach(IOptionsPage *page, m_pages) + stackedPages->addWidget(page->createPage(0)); + splitter->setCollapsible(1, false); pageTree->header()->setVisible(false); - connect(pageTree, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)), - this, SLOT(pageSelected())); - - QMap categories; - - QList pages = - ExtensionSystem::PluginManager::instance()->getObjects(); - - int index = 0; - foreach (IOptionsPage *page, pages) { - PageData pageData; - pageData.index = index; - pageData.category = page->category(); - pageData.id = page->id(); - - QTreeWidgetItem *item = new QTreeWidgetItem; - item->setText(0, page->trName()); - item->setData(0, Qt::UserRole, qVariantFromValue(pageData)); - - QStringList categoriesId = page->category().split(QLatin1Char('|')); - QStringList trCategories = page->trCategory().split(QLatin1Char('|')); - QString currentCategory = categoriesId.at(0); - - QTreeWidgetItem *treeitem; - if (!categories.contains(currentCategory)) { - treeitem = new QTreeWidgetItem(pageTree); - treeitem->setText(0, trCategories.at(0)); - treeitem->setData(0, Qt::UserRole, qVariantFromValue(pageData)); - categories.insert(currentCategory, treeitem); - } - - int catCount = 1; - while (catCount < categoriesId.count()) { - if (!categories.contains(currentCategory + QLatin1Char('|') + categoriesId.at(catCount))) { - treeitem = new QTreeWidgetItem(categories.value(currentCategory)); - currentCategory += QLatin1Char('|') + categoriesId.at(catCount); - treeitem->setText(0, trCategories.at(catCount)); - treeitem->setData(0, Qt::UserRole, qVariantFromValue(pageData)); - categories.insert(currentCategory, treeitem); - } else { - currentCategory += QLatin1Char('|') + categoriesId.at(catCount); - } - ++catCount; - } - - categories.value(currentCategory)->addChild(item); - - m_pages.append(page); - stackedPages->addWidget(page->createPage(stackedPages)); - - if (page->id() == initialPage && currentCategory == initialCategory) { - stackedPages->setCurrentIndex(stackedPages->count()); - pageTree->setCurrentItem(item); - } - - index++; + QModelIndex initialIndex; + m_model = pageModel(m_pages, 0, initialCategory, initialPage, &initialIndex); + m_proxyModel->setFilterKeyColumn(0); + m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_proxyModel->setSourceModel(m_model); + pageTree->setModel(m_proxyModel); + pageTree->setSelectionMode(QAbstractItemView::SingleSelection); + connect(pageTree->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), + this, SLOT(currentChanged(QModelIndex,QModelIndex))); + if (initialIndex.isValid()) { + const QModelIndex proxyIndex = m_proxyModel->mapFromSource(initialIndex); + pageTree->selectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect); } QList sizes; @@ -137,20 +222,92 @@ SettingsDialog::SettingsDialog(QWidget *parent, const QString &categoryId, splitter->setStretchFactor(splitter->indexOf(pageTree), 0); splitter->setStretchFactor(splitter->indexOf(layoutWidget), 1); + + filterClearButton->setIcon(QIcon(QLatin1String(":/debugger/images/delete.png"))); + connect(filterClearButton, SIGNAL(clicked()), filterLineEdit, SLOT(clear())); + // The order of the slot connection matters here, the filter slot + // opens the matching page after the model has filtered. + connect(filterLineEdit, SIGNAL(textChanged(QString)), + m_proxyModel, SLOT(setFilterFixedString(QString))); + connect(filterLineEdit, SIGNAL(textChanged(QString)), this, SLOT(filter(QString))); } SettingsDialog::~SettingsDialog() { } -void SettingsDialog::pageSelected() +void SettingsDialog::showPage(const QStandardItem *item) { - QTreeWidgetItem *item = pageTree->currentItem(); - PageData data = item->data(0, Qt::UserRole).value(); - int index = data.index; - m_currentCategory = data.category; - m_currentPage = data.id; - stackedPages->setCurrentIndex(index); + // Show a page item or recurse to the first page of a category + // if a category was hit. + switch (itemTypeOfItem(item)) { + case PageItem: { + const IOptionsPage *page = pageOfItem(item); + m_currentCategory = page->category(); + m_currentPage = page->id(); + stackedPages->setCurrentIndex(indexOfItem(item)); + } + break; + case CategoryItem: + if (const QStandardItem *child = item->child(0, 0)) + showPage(child); + break; + } +} + +void SettingsDialog::currentChanged(const QModelIndex & current, const QModelIndex & /*previous */) +{ + if (current.isValid()) + if (const QStandardItem *item = m_model->itemFromIndex(m_proxyModel->mapToSource(current))) + showPage(item); +} + +// Helpers that recurse down the model to find a page. +static QModelIndex findPage(const QModelIndex &root) +{ + QTC_ASSERT(root.isValid(), return root) + const QAbstractItemModel *model = root.model(); + // Found a page! + if (itemTypeOfItem(model, root) == PageItem) + return root; + // Recurse down category. + const int childCount = model->rowCount(root); + for (int c = 0; c < childCount; c++) { + const QModelIndex page = findPage(root.child(c, 0)); + if (page.isValid()) + return page; + } + return QModelIndex(); +} + +static QModelIndex findPage(const QAbstractItemModel *model) +{ + const QModelIndex invalid; + // Traverse top categories + const int rootItemCount = model->rowCount(invalid); + for (int c = 0; c < rootItemCount; c++) { + const QModelIndex page = findPage(model->index(c, 0, invalid)); + if (page.isValid()) + return page; + } + return invalid; +} + +void SettingsDialog::filter(const QString &text) +{ + // Filter cleared, collapse all. + if (text.isEmpty()) { + pageTree->collapseAll(); + return; + } + // Expand match and select the first page. Note: This depends + // on the order of slot invocation, needs to be done after filtering + if (!m_proxyModel->rowCount(QModelIndex())) + return; + pageTree->expandAll(); + const QModelIndex firstVisiblePage = findPage(m_proxyModel); + if (firstVisiblePage.isValid()) + pageTree->selectionModel()->setCurrentIndex(firstVisiblePage, QItemSelectionModel::ClearAndSelect); } void SettingsDialog::accept() @@ -187,7 +344,10 @@ bool SettingsDialog::execDialog() void SettingsDialog::done(int val) { QSettings *settings = ICore::instance()->settings(); - settings->setValue("General/LastPreferenceCategory", m_currentCategory); - settings->setValue("General/LastPreferencePage", m_currentPage); + settings->setValue(QLatin1String(categoryKeyC), m_currentCategory); + settings->setValue(QLatin1String(pageKeyC), m_currentPage); QDialog::done(val); } + +} +} diff --git a/src/plugins/coreplugin/dialogs/settingsdialog.h b/src/plugins/coreplugin/dialogs/settingsdialog.h index 31cc41fdee5..f007bfd83a7 100644 --- a/src/plugins/coreplugin/dialogs/settingsdialog.h +++ b/src/plugins/coreplugin/dialogs/settingsdialog.h @@ -36,6 +36,13 @@ #include "coreplugin/dialogs/ioptionspage.h" +QT_BEGIN_NAMESPACE +class QModelIndex; +class QStandardItemModel; +class QStandardItem; +class QSortFilterProxyModel; +QT_END_NAMESPACE + namespace Core { namespace Internal { @@ -57,13 +64,18 @@ public slots: void done(int); private slots: - void pageSelected(); void accept(); void reject(); void apply(); + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void filter(const QString &text); private: - QList m_pages; + void showPage(const QStandardItem *item); + + const QList m_pages; + QSortFilterProxyModel *m_proxyModel; + QStandardItemModel *m_model; bool m_applied; QString m_currentCategory; QString m_currentPage; diff --git a/src/plugins/coreplugin/dialogs/settingsdialog.ui b/src/plugins/coreplugin/dialogs/settingsdialog.ui index aad114f848c..f133bf0fca0 100644 --- a/src/plugins/coreplugin/dialogs/settingsdialog.ui +++ b/src/plugins/coreplugin/dialogs/settingsdialog.ui @@ -13,33 +13,39 @@ Options - - - 6 - - - 9 - + Qt::Horizontal - - - - 0 - 0 - - - - 1 - - - - 0 - - + + + + + + + + + + + Clear + + + + + + + + + + 0 + 0 + + + + + diff --git a/src/plugins/vcsbase/vcsbasesettingspage.cpp b/src/plugins/vcsbase/vcsbasesettingspage.cpp index baa088a7ada..ac5ef4c2372 100644 --- a/src/plugins/vcsbase/vcsbasesettingspage.cpp +++ b/src/plugins/vcsbase/vcsbasesettingspage.cpp @@ -80,6 +80,17 @@ void VCSBaseSettingsWidget::setSettings(const VCSBaseSettings &s) m_ui->lineWrapSpinBox->setValue(s.lineWrapWidth); } +QString VCSBaseSettingsWidget::searchKeyWordMatchString() const +{ + const QChar blank = QLatin1Char(' '); + QString rc = m_ui->submitMessageCheckScriptLabel->text(); + rc += blank; + rc += m_ui->nickNameMailMapLabel->text(); + rc += blank; + rc += m_ui->nickNameFieldsFileLabel->text(); + return rc; +} + // --------------- VCSBaseSettingsPage VCSBaseSettingsPage::VCSBaseSettingsPage(QObject *parent) : Core::IOptionsPage(parent) @@ -119,6 +130,7 @@ QWidget *VCSBaseSettingsPage::createPage(QWidget *parent) { m_widget = new VCSBaseSettingsWidget(parent); m_widget->setSettings(m_settings); + m_searchKeyWords = m_widget->searchKeyWordMatchString(); return m_widget; } @@ -134,5 +146,10 @@ void VCSBaseSettingsPage::apply() } } +bool VCSBaseSettingsPage::matches(const QString &key) const +{ + return m_searchKeyWords.contains(key, Qt::CaseInsensitive); +} + } } diff --git a/src/plugins/vcsbase/vcsbasesettingspage.h b/src/plugins/vcsbase/vcsbasesettingspage.h index e157bd7c8aa..9e94e00b756 100644 --- a/src/plugins/vcsbase/vcsbasesettingspage.h +++ b/src/plugins/vcsbase/vcsbasesettingspage.h @@ -53,6 +53,8 @@ public: VCSBaseSettings settings() const; void setSettings(const VCSBaseSettings &s); + QString searchKeyWordMatchString() const; + private: Ui::VCSBaseSettingsPage *m_ui; }; @@ -72,6 +74,7 @@ public: virtual QWidget *createPage(QWidget *parent); virtual void apply(); virtual void finish() { } + virtual bool matches(const QString &key) const; VCSBaseSettings settings() const { return m_settings; } @@ -80,8 +83,10 @@ signals: private: void updateNickNames(); + VCSBaseSettingsWidget* m_widget; VCSBaseSettings m_settings; + QString m_searchKeyWords; }; } // namespace Internal