settingsdialog.cpp 19.3 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
hjk's avatar
hjk committed
3
4
** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8
9
10
11
12
13
14
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
18
19
20
21
22
** 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.
23
**
hjk's avatar
hjk committed
24
25
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
26
27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
28
****************************************************************************/
hjk's avatar
hjk committed
29

con's avatar
con committed
30
31
#include "settingsdialog.h"

32
#include "icore.h"
33

hjk's avatar
hjk committed
34
#include <extensionsystem/pluginmanager.h>
35
#include <utils/hostosinfo.h>
Friedemann Kleint's avatar
Friedemann Kleint committed
36
#include <utils/filterlineedit.h>
37

hjk's avatar
hjk committed
38
39
40
41
42
#include <QApplication>
#include <QDialogButtonBox>
#include <QFrame>
#include <QGridLayout>
#include <QGroupBox>
43
44
#include <QHBoxLayout>
#include <QIcon>
hjk's avatar
hjk committed
45
#include <QItemSelectionModel>
46
#include <QLabel>
hjk's avatar
hjk committed
47
48
49
#include <QLineEdit>
#include <QListView>
#include <QPointer>
50
51
#include <QPushButton>
#include <QScrollBar>
hjk's avatar
hjk committed
52
53
#include <QSettings>
#include <QSortFilterProxyModel>
54
55
#include <QSpacerItem>
#include <QStackedLayout>
hjk's avatar
hjk committed
56
#include <QStyle>
57
#include <QStyledItemDelegate>
hjk's avatar
hjk committed
58
59
#include <QToolBar>
#include <QToolButton>
60
61
62

static const char categoryKeyC[] = "General/LastPreferenceCategory";
static const char pageKeyC[] = "General/LastPreferencePage";
63
const int categoryIconSize = 24;
64
65
66

namespace Core {
namespace Internal {
con's avatar
con committed
67

hjk's avatar
hjk committed
68
static QPointer<SettingsDialog> m_instance = 0;
69

70
71
// ----------- Category model

hjk's avatar
hjk committed
72
73
class Category
{
Tobias Hunger's avatar
Tobias Hunger committed
74
public:
hjk's avatar
hjk committed
75
    Id id;
76
    QString displayName;
77
    QIcon icon;
78
79
    QList<IOptionsPage *> pages;
    QList<IOptionsPageProvider *> providers;
80
81
82
83
84
85
86
87
88
89
90
91
92
    int index;
    QTabWidget *tabWidget;
};

class CategoryModel : public QAbstractListModel
{
public:
    CategoryModel(QObject *parent = 0);
    ~CategoryModel();

    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;

93
94
    void setPages(const QList<IOptionsPage*> &pages,
                  const QList<IOptionsPageProvider *> &providers);
95
96
97
    const QList<Category*> &categories() const { return m_categories; }

private:
hjk's avatar
hjk committed
98
    Category *findCategoryById(Id id);
99
100

    QList<Category*> m_categories;
101
    QIcon m_emptyIcon;
102
103
104
105
};

CategoryModel::CategoryModel(QObject *parent)
    : QAbstractListModel(parent)
106
{
107
108
109
    QPixmap empty(categoryIconSize, categoryIconSize);
    empty.fill(Qt::transparent);
    m_emptyIcon = QIcon(empty);
110
111
}

112
CategoryModel::~CategoryModel()
113
{
114
    qDeleteAll(m_categories);
115
116
}

117
int CategoryModel::rowCount(const QModelIndex &parent) const
118
{
119
    return parent.isValid() ? 0 : m_categories.size();
120
121
}

122
QVariant CategoryModel::data(const QModelIndex &index, int role) const
123
{
124
125
126
    switch (role) {
    case Qt::DisplayRole:
        return m_categories.at(index.row())->displayName;
127
128
129
130
131
132
    case Qt::DecorationRole: {
            QIcon icon = m_categories.at(index.row())->icon;
            if (icon.isNull())
                icon = m_emptyIcon;
            return icon;
        }
133
134
135
    }

    return QVariant();
136
137
}

138
139
void CategoryModel::setPages(const QList<IOptionsPage*> &pages,
                             const QList<IOptionsPageProvider *> &providers)
140
{
141
142
    beginResetModel();

143
144
145
146
147
148
    // Clear any previous categories
    qDeleteAll(m_categories);
    m_categories.clear();

    // Put the pages in categories
    foreach (IOptionsPage *page, pages) {
hjk's avatar
hjk committed
149
        const Id categoryId = page->category();
150
151
152
153
        Category *category = findCategoryById(categoryId);
        if (!category) {
            category = new Category;
            category->id = categoryId;
154
155
156
157
158
            category->tabWidget = 0;
            category->index = -1;
            m_categories.append(category);
        }
        if (category->displayName.isEmpty())
159
            category->displayName = page->displayCategory();
160
        if (category->icon.isNull())
161
            category->icon = page->categoryIcon();
162
163
164
165
        category->pages.append(page);
    }

    foreach (IOptionsPageProvider *provider, providers) {
hjk's avatar
hjk committed
166
        const Id categoryId = provider->category();
167
168
169
170
171
172
        Category *category = findCategoryById(categoryId);
        if (!category) {
            category = new Category;
            category->id = categoryId;
            category->tabWidget = 0;
            category->index = -1;
173
174
            m_categories.append(category);
        }
175
176
177
178
179
        if (category->displayName.isEmpty())
            category->displayName = provider->displayCategory();
        if (category->icon.isNull())
            category->icon = provider->categoryIcon();
        category->providers.append(provider);
180
181
    }

182
    endResetModel();
183
184
}

hjk's avatar
hjk committed
185
Category *CategoryModel::findCategoryById(Id id)
186
{
187
188
189
190
191
192
193
    for (int i = 0; i < m_categories.size(); ++i) {
        Category *category = m_categories.at(i);
        if (category->id == id)
            return category;
    }

    return 0;
194
}
195

196
197
198
199
200
201
202
203
// ----------- Category filter model

/**
 * A filter model that returns true for each category node that has pages that
 * match the search string.
 */
class CategoryFilterModel : public QSortFilterProxyModel
{
204
public:
205
206
207
208
    explicit CategoryFilterModel(QObject *parent = 0)
        : QSortFilterProxyModel(parent)
    {}

209
protected:
210
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
211
212
};

213
bool CategoryFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
214
215
{
    // Regular contents check, then check page-filter.
216
    if (QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent))
217
        return true;
218
219
220

    const CategoryModel *cm = static_cast<CategoryModel*>(sourceModel());
    foreach (const IOptionsPage *page, cm->categories().at(sourceRow)->pages) {
221
        const QString pattern = filterRegExp().pattern();
222
223
224
225
        if (page->displayCategory().contains(pattern, Qt::CaseInsensitive)
            || page->displayName().contains(pattern, Qt::CaseInsensitive)
            || page->matches(pattern))
            return true;
226
    }
227

228
    return false;
229
}
con's avatar
con committed
230

231
// ----------- Category list view
232

233
234
235
236
237
238
239
240
241
242
243
244
245

class CategoryListViewDelegate : public QStyledItemDelegate
{
public:
    CategoryListViewDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        QSize size = QStyledItemDelegate::sizeHint(option, index);
        size.setHeight(qMax(size.height(), 32));
        return size;
    }
};

246
/**
247
 * Special version of a QListView that has the width of the first column as
248
249
 * minimum size.
 */
250
class CategoryListView : public QListView
251
252
{
public:
253
    CategoryListView(QWidget *parent = 0) : QListView(parent)
254
255
    {
        setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding);
256
        setItemDelegate(new CategoryListViewDelegate(this));
257
258
259
260
    }

    virtual QSize sizeHint() const
    {
261
262
263
264
        int width = sizeHintForColumn(0) + frameWidth() * 2 + 5;
        if (verticalScrollBar()->isVisible())
            width += verticalScrollBar()->width();
        return QSize(width, 100);
265
266
267
    }
};

268
269
270
271
272
// ----------- SettingsDialog

// Helpers to sort by category. id
bool optionsPageLessThan(const IOptionsPage *p1, const IOptionsPage *p2)
{
hjk's avatar
hjk committed
273
    if (const int cc = p1->category().toString().compare(p2->category().toString()))
274
        return cc < 0;
hjk's avatar
hjk committed
275
    return p1->id().toString().compare(p2->id().toString()) < 0;
276
277
278
279
}

static inline QList<Core::IOptionsPage*> sortedOptionsPages()
{
280
    QList<Core::IOptionsPage*> rc = ExtensionSystem::PluginManager::getObjects<IOptionsPage>();
281
282
283
284
    qStableSort(rc.begin(), rc.end(), optionsPageLessThan);
    return rc;
}

285
SettingsDialog::SettingsDialog(QWidget *parent) :
286
    QDialog(parent),
287
    m_pages(sortedOptionsPages()),
288
289
    m_proxyModel(new CategoryFilterModel(this)),
    m_model(new CategoryModel(this)),
Friedemann Kleint's avatar
Friedemann Kleint committed
290
291
    m_stackedLayout(new QStackedLayout),
    m_filterLineEdit(new Utils::FilterLineEdit),
292
    m_categoryList(new CategoryListView),
293
294
    m_headerLabel(new QLabel),
    m_running(false),
295
296
    m_applied(false),
    m_finished(false)
con's avatar
con committed
297
{
298
299
    m_applied = false;

Friedemann Kleint's avatar
Friedemann Kleint committed
300
    createGui();
301
    setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
302
303
304
305
    if (Utils::HostOsInfo::isMacHost())
        setWindowTitle(tr("Preferences"));
    else
        setWindowTitle(tr("Options"));
306

307
    m_model->setPages(m_pages,
308
        ExtensionSystem::PluginManager::getObjects<IOptionsPageProvider>());
con's avatar
con committed
309

310
    m_proxyModel->setSourceModel(m_model);
311
    m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
312
    m_categoryList->setIconSize(QSize(categoryIconSize, categoryIconSize));
313
314
    m_categoryList->setModel(m_proxyModel);
    m_categoryList->setSelectionMode(QAbstractItemView::SingleSelection);
315
    m_categoryList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
316
317
318
319

    connect(m_categoryList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
            this, SLOT(currentChanged(QModelIndex)));

320
321
    // The order of the slot connection matters here, the filter slot
    // opens the matching page after the model has filtered.
322
323
    connect(m_filterLineEdit, SIGNAL(filterChanged(QString)),
            this, SLOT(ensureAllCategoryWidgets()));
324
325
326
327
328
329
    connect(m_filterLineEdit, SIGNAL(filterChanged(QString)),
                m_proxyModel, SLOT(setFilterFixedString(QString)));
    connect(m_filterLineEdit, SIGNAL(filterChanged(QString)), this, SLOT(filter(QString)));
    m_categoryList->setFocus();
}

hjk's avatar
hjk committed
330
void SettingsDialog::showPage(Id categoryId, Id pageId)
331
332
{
    // handle the case of "show last page"
hjk's avatar
hjk committed
333
    Id initialCategory = categoryId;
hjk's avatar
hjk committed
334
335
    Id initialPage = pageId;
    if (!initialCategory.isValid() && !initialPage.isValid()) {
hjk's avatar
hjk committed
336
        QSettings *settings = ICore::settings();
337
        initialCategory = Id::fromSetting(settings->value(QLatin1String(categoryKeyC)));
hjk's avatar
hjk committed
338
        initialPage = Id::fromSetting(settings->value(QLatin1String(pageKeyC)));
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
    }

    int initialCategoryIndex = -1;
    int initialPageIndex = -1;
    const QList<Category*> &categories = m_model->categories();
    for (int i = 0; i < categories.size(); ++i) {
        Category *category = categories.at(i);
        if (category->id == initialCategory) {
            initialCategoryIndex = i;
            for (int j = 0; j < category->pages.size(); ++j) {
                IOptionsPage *page = category->pages.at(j);
                if (page->id() == initialPage)
                    initialPageIndex = j;
            }
        }
    }
355
356
357
358
359
    if (initialCategoryIndex != -1) {
        const QModelIndex modelIndex = m_proxyModel->mapFromSource(m_model->index(initialCategoryIndex));
        m_categoryList->setCurrentIndex(modelIndex);
        if (initialPageIndex != -1)
            categories.at(initialCategoryIndex)->tabWidget->setCurrentIndex(initialPageIndex);
con's avatar
con committed
360
    }
Friedemann Kleint's avatar
Friedemann Kleint committed
361
362
363
364
365
366
367
368
369
370
371
372
373
374
}

void SettingsDialog::createGui()
{
    // Header label with large font and a bit of spacing (align with group boxes)
    QFont headerLabelFont = m_headerLabel->font();
    headerLabelFont.setBold(true);
    // Paranoia: Should a font be set in pixels...
    const int pointSize = headerLabelFont.pointSize();
    if (pointSize > 0)
        headerLabelFont.setPointSize(pointSize + 2);
    m_headerLabel->setFont(headerLabelFont);

    QHBoxLayout *headerHLayout = new QHBoxLayout;
375
    const int leftMargin = qApp->style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
Friedemann Kleint's avatar
Friedemann Kleint committed
376
377
378
379
380
    headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
    headerHLayout->addWidget(m_headerLabel);

    m_stackedLayout->setMargin(0);

381
382
383
    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok |
                                                       QDialogButtonBox::Apply |
                                                       QDialogButtonBox::Cancel);
Friedemann Kleint's avatar
Friedemann Kleint committed
384
385
386
387
388
389
390
391
    buttonBox->button(QDialogButtonBox::Ok)->setDefault(true);
    connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(apply()));
    connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
    connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));

    QGridLayout *mainGridLayout = new QGridLayout;
    mainGridLayout->addWidget(m_filterLineEdit, 0, 0, 1, 1);
    mainGridLayout->addLayout(headerHLayout,    0, 1, 1, 1);
392
    mainGridLayout->addWidget(m_categoryList,   1, 0, 1, 1);
Friedemann Kleint's avatar
Friedemann Kleint committed
393
    mainGridLayout->addLayout(m_stackedLayout,  1, 1, 1, 1);
394
    mainGridLayout->addWidget(buttonBox,        2, 0, 1, 2);
Friedemann Kleint's avatar
Friedemann Kleint committed
395
396
    mainGridLayout->setColumnStretch(1, 4);
    setLayout(mainGridLayout);
397
    setMinimumSize(1000, 550);
con's avatar
con committed
398
399
400
401
402
403
}

SettingsDialog::~SettingsDialog()
{
}

404
void SettingsDialog::showCategory(int index)
con's avatar
con committed
405
{
406
    Category *category = m_model->categories().at(index);
407
    ensureCategoryWidget(category);
408
409
410
411
412
413
414
    // Update current category and page
    m_currentCategory = category->id;
    const int currentTabIndex = category->tabWidget->currentIndex();
    if (currentTabIndex != -1) {
        IOptionsPage *page = category->pages.at(currentTabIndex);
        m_currentPage = page->id();
        m_visitedPages.insert(page);
415
    }
416
417
418
419
420

    m_stackedLayout->setCurrentIndex(category->index);
    m_headerLabel->setText(category->displayName);

    updateEnabledTabs(category, m_filterLineEdit->text());
421
422
}

423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
void SettingsDialog::ensureCategoryWidget(Category *category)
{
    if (category->tabWidget != 0)
        return;
    foreach (const IOptionsPageProvider *provider, category->providers) {
        category->pages += provider->pages();
    }
    qStableSort(category->pages.begin(), category->pages.end(), optionsPageLessThan);

    QTabWidget *tabWidget = new QTabWidget;
    for (int j = 0; j < category->pages.size(); ++j) {
        IOptionsPage *page = category->pages.at(j);
        QWidget *widget = page->createPage(0);
        tabWidget->addTab(widget, page->displayName());
    }

    connect(tabWidget, SIGNAL(currentChanged(int)),
            this, SLOT(currentTabChanged(int)));

    category->tabWidget = tabWidget;
    category->index = m_stackedLayout->addWidget(tabWidget);
}

446
447
448
449
450
451
void SettingsDialog::ensureAllCategoryWidgets()
{
    foreach (Category *category, m_model->categories())
        ensureCategoryWidget(category);
}

452
453
454
455
456
457
458
459
460
void SettingsDialog::disconnectTabWidgets()
{
    foreach (Category *category, m_model->categories()) {
        if (category->tabWidget)
            disconnect(category->tabWidget, SIGNAL(currentChanged(int)),
                    this, SLOT(currentTabChanged(int)));
    }
}

461
void SettingsDialog::updateEnabledTabs(Category *category, const QString &searchText)
462
{
463
464
465
    for (int i = 0; i < category->pages.size(); ++i) {
        const IOptionsPage *page = category->pages.at(i);
        const bool enabled = searchText.isEmpty()
hjk's avatar
hjk committed
466
                             || page->category().toString().contains(searchText, Qt::CaseInsensitive)
467
468
469
470
                             || page->displayName().contains(searchText, Qt::CaseInsensitive)
                             || page->matches(searchText);
        category->tabWidget->setTabEnabled(i, enabled);
    }
471
472
}

473
void SettingsDialog::currentChanged(const QModelIndex &current)
474
{
475
476
    if (current.isValid())
        showCategory(m_proxyModel->mapToSource(current).row());
477
478
}

479
void SettingsDialog::currentTabChanged(int index)
480
{
481
482
483
484
485
486
487
488
489
490
491
492
    if (index == -1)
        return;

    const QModelIndex modelIndex = m_proxyModel->mapToSource(m_categoryList->currentIndex());
    if (!modelIndex.isValid())
        return;

    // Remember the current tab and mark it as visited
    const Category *category = m_model->categories().at(modelIndex.row());
    IOptionsPage *page = category->pages.at(index);
    m_currentPage = page->id();
    m_visitedPages.insert(page);
493
494
495
496
}

void SettingsDialog::filter(const QString &text)
{
497
    ensureAllCategoryWidgets();
498
499
500
501
502
503
    // When there is no current index, select the first one when possible
    if (!m_categoryList->currentIndex().isValid() && m_model->rowCount() > 0)
        m_categoryList->setCurrentIndex(m_proxyModel->index(0, 0));

    const QModelIndex currentIndex = m_proxyModel->mapToSource(m_categoryList->currentIndex());
    if (!currentIndex.isValid())
504
        return;
505
506
507

    Category *category = m_model->categories().at(currentIndex.row());
    updateEnabledTabs(category, text);
con's avatar
con committed
508
509
510
511
}

void SettingsDialog::accept()
{
512
513
514
    if (m_finished)
        return;
    m_finished = true;
515
    disconnectTabWidgets();
516
    m_applied = true;
517
    foreach (IOptionsPage *page, m_visitedPages)
518
        page->apply();
519
    foreach (IOptionsPage *page, m_pages)
520
        page->finish();
con's avatar
con committed
521
522
523
524
525
    done(QDialog::Accepted);
}

void SettingsDialog::reject()
{
526
527
528
    if (m_finished)
        return;
    m_finished = true;
529
    disconnectTabWidgets();
hjk's avatar
hjk committed
530
    foreach (IOptionsPage *page, m_pages)
531
        page->finish();
con's avatar
con committed
532
533
    done(QDialog::Rejected);
}
534
535
536

void SettingsDialog::apply()
{
537
    foreach (IOptionsPage *page, m_visitedPages)
538
        page->apply();
539
540
541
    m_applied = true;
}

542
543
void SettingsDialog::done(int val)
{
hjk's avatar
hjk committed
544
    QSettings *settings = ICore::settings();
hjk's avatar
hjk committed
545
546
    settings->setValue(QLatin1String(categoryKeyC), m_currentCategory.toSetting());
    settings->setValue(QLatin1String(pageKeyC), m_currentPage.toSetting());
547

Tobias Hunger's avatar
Tobias Hunger committed
548
549
    ICore::saveSettings(); // save all settings

550
551
552
553
554
555
556
557
    // exit all additional event loops, see comment in execDialog()
    QListIterator<QEventLoop *> it(m_eventLoops);
    it.toBack();
    while (it.hasPrevious()) {
        QEventLoop *loop = it.previous();
        loop->exit();
    }

558
559
    QDialog::done(val);
}
560

561
562
563
564
565
566
567
568
/**
 * Override to make sure the settings dialog starts up as small as possible.
 */
QSize SettingsDialog::sizeHint() const
{
    return minimumSize();
}

569
SettingsDialog *SettingsDialog::getSettingsDialog(QWidget *parent,
hjk's avatar
hjk committed
570
    Id initialCategory, Id initialPage)
571
{
hjk's avatar
hjk committed
572
    if (!m_instance)
573
574
575
576
577
578
579
580
581
        m_instance = new SettingsDialog(parent);
    m_instance->showPage(initialCategory, initialPage);
    return m_instance;
}

bool SettingsDialog::execDialog()
{
    if (!m_running) {
        m_running = true;
582
        m_finished = false;
583
        exec();
584
585
586
587
588
        m_running = false;
        m_instance = 0;
        // make sure that the current "single" instance is deleted
        // we can't delete right away, since we still access the m_applied member
        deleteLater();
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
    } else {
        // exec dialog is called while the instance is already running
        // this can happen when a event triggers a code path that wants to
        // show the settings dialog again
        // e.g. when starting the debugger (with non-built debugging helpers),
        // and manually opening the settings dialog, after the debugger hit
        // a break point it will complain about missing helper, and offer the
        // option to open the settings dialog.
        // Keep the UI running by creating another event loop.
        QEventLoop *loop = new QEventLoop(this);
        m_eventLoops.append(loop);
        loop->exec();
    }
    return m_applied;
}

605
606
} // namespace Internal
} // namespace Core