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.
This commit is contained in:
Friedemann Kleint
2009-11-19 17:47:13 +01:00
parent d49dc172ad
commit 9a7729ebdc
7 changed files with 304 additions and 102 deletions

View File

@@ -42,5 +42,6 @@
\o trCategory() is the translated category \o trCategory() is the translated category
\o apply() is called to store the settings. It should detect if any changes have been \o apply() is called to store the settings. It should detect if any changes have been
made and store those. made and store those.
\o matches() is used for the options dialog search filter.
\endlist \endlist
*/ */

View File

@@ -51,6 +51,7 @@ public:
virtual QString trName() const = 0; virtual QString trName() const = 0;
virtual QString category() const = 0; virtual QString category() const = 0;
virtual QString trCategory() const = 0; virtual QString trCategory() const = 0;
virtual bool matches(const QString & /* searchKeyWord*/) const { return false; }
virtual QWidget *createPage(QWidget *parent) = 0; virtual QWidget *createPage(QWidget *parent) = 0;
virtual void apply() = 0; virtual void apply() = 0;

View File

@@ -32,26 +32,152 @@
#include <extensionsystem/pluginmanager.h> #include <extensionsystem/pluginmanager.h>
#include "icore.h" #include "icore.h"
#include <utils/qtcassert.h>
#include <QtCore/QSettings> #include <QtCore/QSettings>
#include <QtGui/QHeaderView> #include <QtGui/QHeaderView>
#include <QtGui/QPushButton> #include <QtGui/QPushButton>
#include <QtGui/QStandardItemModel>
#include <QtGui/QStandardItem>
#include <QtGui/QSortFilterProxyModel>
#include <QtGui/QItemSelectionModel>
#include <QtGui/QIcon>
namespace { enum ItemType { CategoryItem, PageItem };
struct PageData {
int index; enum { TypeRole = Qt::UserRole + 1,
QString category; IndexRole = Qt::UserRole + 2,
QString id; 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<class Parent>
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<ItemType>(item->data(TypeRole).toInt());
}
using namespace Core; static inline ItemType itemTypeOfItem(const QAbstractItemModel *model, const QModelIndex &index)
using namespace Core::Internal; {
return static_cast<ItemType>(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<IOptionsPage *>(item->data(PageRole));
}
static inline IOptionsPage *pageOfItem(const QAbstractItemModel *model, const QModelIndex &index)
{
return qvariant_cast<IOptionsPage *>(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<IOptionsPage*> &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<QString, QStandardItem *> 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, SettingsDialog::SettingsDialog(QWidget *parent, const QString &categoryId,
const QString &pageId) const QString &pageId) :
: QDialog(parent), m_applied(false) QDialog(parent),
m_pages(ExtensionSystem::PluginManager::instance()->getObjects<IOptionsPage>()),
m_proxyModel(new PageFilterModel),
m_model(0),
m_applied(false)
{ {
setupUi(this); setupUi(this);
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
@@ -63,72 +189,31 @@ SettingsDialog::SettingsDialog(QWidget *parent, const QString &categoryId,
QString initialPage = pageId; QString initialPage = pageId;
if (initialCategory.isEmpty() && initialPage.isEmpty()) { if (initialCategory.isEmpty() && initialPage.isEmpty()) {
QSettings *settings = ICore::instance()->settings(); QSettings *settings = ICore::instance()->settings();
initialCategory = settings->value("General/LastPreferenceCategory", QVariant(QString())).toString(); initialCategory = settings->value(QLatin1String(categoryKeyC), QVariant(QString())).toString();
initialPage = settings->value("General/LastPreferencePage", QVariant(QString())).toString(); initialPage = settings->value(QLatin1String(pageKeyC), QVariant(QString())).toString();
} }
buttonBox->button(QDialogButtonBox::Ok)->setDefault(true); buttonBox->button(QDialogButtonBox::Ok)->setDefault(true);
connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(apply())); connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(apply()));
foreach(IOptionsPage *page, m_pages)
stackedPages->addWidget(page->createPage(0));
splitter->setCollapsible(1, false); splitter->setCollapsible(1, false);
pageTree->header()->setVisible(false); pageTree->header()->setVisible(false);
connect(pageTree, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)), QModelIndex initialIndex;
this, SLOT(pageSelected())); m_model = pageModel(m_pages, 0, initialCategory, initialPage, &initialIndex);
m_proxyModel->setFilterKeyColumn(0);
QMap<QString, QTreeWidgetItem *> categories; m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setSourceModel(m_model);
QList<IOptionsPage*> pages = pageTree->setModel(m_proxyModel);
ExtensionSystem::PluginManager::instance()->getObjects<IOptionsPage>(); pageTree->setSelectionMode(QAbstractItemView::SingleSelection);
connect(pageTree->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
int index = 0; this, SLOT(currentChanged(QModelIndex,QModelIndex)));
foreach (IOptionsPage *page, pages) { if (initialIndex.isValid()) {
PageData pageData; const QModelIndex proxyIndex = m_proxyModel->mapFromSource(initialIndex);
pageData.index = index; pageTree->selectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
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++;
} }
QList<int> sizes; QList<int> sizes;
@@ -137,20 +222,92 @@ SettingsDialog::SettingsDialog(QWidget *parent, const QString &categoryId,
splitter->setStretchFactor(splitter->indexOf(pageTree), 0); splitter->setStretchFactor(splitter->indexOf(pageTree), 0);
splitter->setStretchFactor(splitter->indexOf(layoutWidget), 1); 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() SettingsDialog::~SettingsDialog()
{ {
} }
void SettingsDialog::pageSelected() void SettingsDialog::showPage(const QStandardItem *item)
{ {
QTreeWidgetItem *item = pageTree->currentItem(); // Show a page item or recurse to the first page of a category
PageData data = item->data(0, Qt::UserRole).value<PageData>(); // if a category was hit.
int index = data.index; switch (itemTypeOfItem(item)) {
m_currentCategory = data.category; case PageItem: {
m_currentPage = data.id; const IOptionsPage *page = pageOfItem(item);
stackedPages->setCurrentIndex(index); 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() void SettingsDialog::accept()
@@ -187,7 +344,10 @@ bool SettingsDialog::execDialog()
void SettingsDialog::done(int val) void SettingsDialog::done(int val)
{ {
QSettings *settings = ICore::instance()->settings(); QSettings *settings = ICore::instance()->settings();
settings->setValue("General/LastPreferenceCategory", m_currentCategory); settings->setValue(QLatin1String(categoryKeyC), m_currentCategory);
settings->setValue("General/LastPreferencePage", m_currentPage); settings->setValue(QLatin1String(pageKeyC), m_currentPage);
QDialog::done(val); QDialog::done(val);
} }
}
}

View File

@@ -36,6 +36,13 @@
#include "coreplugin/dialogs/ioptionspage.h" #include "coreplugin/dialogs/ioptionspage.h"
QT_BEGIN_NAMESPACE
class QModelIndex;
class QStandardItemModel;
class QStandardItem;
class QSortFilterProxyModel;
QT_END_NAMESPACE
namespace Core { namespace Core {
namespace Internal { namespace Internal {
@@ -57,13 +64,18 @@ public slots:
void done(int); void done(int);
private slots: private slots:
void pageSelected();
void accept(); void accept();
void reject(); void reject();
void apply(); void apply();
void currentChanged(const QModelIndex &current, const QModelIndex &previous);
void filter(const QString &text);
private: private:
QList<Core::IOptionsPage*> m_pages; void showPage(const QStandardItem *item);
const QList<Core::IOptionsPage*> m_pages;
QSortFilterProxyModel *m_proxyModel;
QStandardItemModel *m_model;
bool m_applied; bool m_applied;
QString m_currentCategory; QString m_currentCategory;
QString m_currentPage; QString m_currentPage;

View File

@@ -13,33 +13,39 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Options</string> <string>Options</string>
</property> </property>
<layout class="QVBoxLayout"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>9</number>
</property>
<item> <item>
<widget class="QSplitter" name="splitter"> <widget class="QSplitter" name="splitter">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<widget class="QTreeWidget" name="pageTree"> <widget class="QWidget" name="">
<property name="sizePolicy"> <layout class="QVBoxLayout" name="verticalLayout">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <item>
<horstretch>0</horstretch> <layout class="QHBoxLayout" name="filterLayout">
<verstretch>0</verstretch> <item>
</sizepolicy> <widget class="QLineEdit" name="filterLineEdit"/>
</property> </item>
<property name="columnCount"> <item>
<number>1</number> <widget class="QToolButton" name="filterClearButton">
</property> <property name="text">
<column> <string>Clear</string>
<property name="text"> </property>
<string>0</string> </widget>
</property> </item>
</column> </layout>
</item>
<item>
<widget class="QTreeView" name="pageTree">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget> </widget>
<widget class="QWidget" name="layoutWidget"> <widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">

View File

@@ -80,6 +80,17 @@ void VCSBaseSettingsWidget::setSettings(const VCSBaseSettings &s)
m_ui->lineWrapSpinBox->setValue(s.lineWrapWidth); 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::VCSBaseSettingsPage(QObject *parent) : VCSBaseSettingsPage::VCSBaseSettingsPage(QObject *parent) :
Core::IOptionsPage(parent) Core::IOptionsPage(parent)
@@ -119,6 +130,7 @@ QWidget *VCSBaseSettingsPage::createPage(QWidget *parent)
{ {
m_widget = new VCSBaseSettingsWidget(parent); m_widget = new VCSBaseSettingsWidget(parent);
m_widget->setSettings(m_settings); m_widget->setSettings(m_settings);
m_searchKeyWords = m_widget->searchKeyWordMatchString();
return m_widget; return m_widget;
} }
@@ -134,5 +146,10 @@ void VCSBaseSettingsPage::apply()
} }
} }
bool VCSBaseSettingsPage::matches(const QString &key) const
{
return m_searchKeyWords.contains(key, Qt::CaseInsensitive);
}
} }
} }

View File

@@ -53,6 +53,8 @@ public:
VCSBaseSettings settings() const; VCSBaseSettings settings() const;
void setSettings(const VCSBaseSettings &s); void setSettings(const VCSBaseSettings &s);
QString searchKeyWordMatchString() const;
private: private:
Ui::VCSBaseSettingsPage *m_ui; Ui::VCSBaseSettingsPage *m_ui;
}; };
@@ -72,6 +74,7 @@ public:
virtual QWidget *createPage(QWidget *parent); virtual QWidget *createPage(QWidget *parent);
virtual void apply(); virtual void apply();
virtual void finish() { } virtual void finish() { }
virtual bool matches(const QString &key) const;
VCSBaseSettings settings() const { return m_settings; } VCSBaseSettings settings() const { return m_settings; }
@@ -80,8 +83,10 @@ signals:
private: private:
void updateNickNames(); void updateNickNames();
VCSBaseSettingsWidget* m_widget; VCSBaseSettingsWidget* m_widget;
VCSBaseSettings m_settings; VCSBaseSettings m_settings;
QString m_searchKeyWords;
}; };
} // namespace Internal } // namespace Internal