Revision control
++ // Copyright (C) 2020 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qhelpfilterdata.h"
++ #include "qfilternamedialog_p.h"
++ #include "qhelpfiltersettingswidget.h"
++ #include "ui_qhelpfiltersettingswidget.h"
++
++ #include <QtCore/qmap.h>
++ #include <QtCore/qversionnumber.h>
++ #include <QtHelp/qhelpfilterdata.h>
++ #include <QtHelp/qhelpfilterengine.h>
++ #include <QtWidgets/qmessagebox.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ class QHelpFilterSettings final
++ {
++ public:
++ void setFilter(const QString &filterName, const QHelpFilterData &filterData)
++ {
++ m_filterToData.insert(filterName, filterData);
++ }
++ void removeFilter(const QString &filterName) { m_filterToData.remove(filterName); }
++ QHelpFilterData filterData(const QString &filterName) const
++ {
++ return m_filterToData.value(filterName);
++ }
++ QMap<QString, QHelpFilterData> filters() const { return m_filterToData; }
++
++ void setCurrentFilter(const QString &filterName) { m_currentFilter = filterName; }
++ QString currentFilter() const { return m_currentFilter; }
++
++ private:
++ QMap<QString, QHelpFilterData> m_filterToData;
++ QString m_currentFilter;
++ };
++
++ static QHelpFilterSettings readSettingsHelper(const QHelpFilterEngine *filterEngine)
++ {
++ QHelpFilterSettings filterSettings;
++
++ const QStringList allFilters = filterEngine->filters();
++ for (const QString &filter : allFilters)
++ filterSettings.setFilter(filter, filterEngine->filterData(filter));
++
++ filterSettings.setCurrentFilter(filterEngine->activeFilter());
++ return filterSettings;
++ }
++
++ static QMap<QString, QHelpFilterData> subtract(const QMap<QString, QHelpFilterData> &minuend,
++ const QMap<QString, QHelpFilterData> &subtrahend)
++ {
++ QMap<QString, QHelpFilterData> result = minuend;
++
++ for (auto itSubtrahend = subtrahend.cbegin(); itSubtrahend != subtrahend.cend(); ++itSubtrahend) {
++ auto itResult = result.constFind(itSubtrahend.key());
++ if (itResult != result.constEnd() && itSubtrahend.value() == itResult.value())
++ result.erase(itResult);
++ }
++ return result;
++ }
++
++ static bool applySettingsHelper(QHelpFilterEngine *filterEngine, const QHelpFilterSettings &settings)
++ {
++ bool changed = false;
++ const QHelpFilterSettings oldSettings = readSettingsHelper(filterEngine);
++
++ const auto filtersToRemove = subtract(oldSettings.filters(), settings.filters());
++ const auto filtersToAdd = subtract(settings.filters(), oldSettings.filters());
++
++ const QString ¤tFilter = filterEngine->activeFilter();
++
++ for (auto it = filtersToRemove.cbegin(); it != filtersToRemove.cend(); ++it) {
++ filterEngine->removeFilter(it.key());
++ if (currentFilter == it.key() && !filtersToAdd.contains(it.key()))
++ filterEngine->setActiveFilter({});
++ changed = true;
++ }
++
++ for (auto it = filtersToAdd.cbegin(); it != filtersToAdd.cend(); ++it) {
++ filterEngine->setFilterData(it.key(), it.value());
++ changed = true;
++ }
++
++ if (changed)
++ filterEngine->setActiveFilter(settings.currentFilter());
++ return changed;
++ }
++
++ static QStringList versionsToStringList(const QList<QVersionNumber> &versions)
++ {
++ QStringList versionList;
++ for (const QVersionNumber &version : versions)
++ versionList.append(version.isNull() ? QString() : version.toString());
++ return versionList;
++ }
++
++ static QList<QVersionNumber> stringListToVersions(const QStringList &versionList)
++ {
++ QList<QVersionNumber> versions;
++ for (const QString &versionString : versionList)
++ versions.append(QVersionNumber::fromString(versionString));
++ return versions;
++ }
++
++ class QHelpFilterSettingsWidgetPrivate
++ {
++ QHelpFilterSettingsWidget *q_ptr;
++ Q_DECLARE_PUBLIC(QHelpFilterSettingsWidget) // TODO: remove Q_DECLARE_PUBLIC
++ public:
++ QHelpFilterSettingsWidgetPrivate() = default;
++
++ QHelpFilterSettings filterSettings() const { return m_filterSettings; }
++ void setFilterSettings(const QHelpFilterSettings &settings);
++
++ void updateCurrentFilter();
++ void componentsChanged(const QStringList &components);
++ void versionsChanged(const QStringList &versions);
++ void addFilterClicked();
++ void renameFilterClicked();
++ void removeFilterClicked();
++ void addFilter(const QString &filterName, const QHelpFilterData &filterData = {});
++ void removeFilter(const QString &filterName);
++ QString getUniqueFilterName(const QString &windowTitle, const QString &initialFilterName);
++ QString suggestedNewFilterName(const QString &initialFilterName) const;
++
++ QMap<QString, QListWidgetItem *> m_filterToItem;
++ QHash<QListWidgetItem *, QString> m_itemToFilter;
++
++ Ui::QHelpFilterSettingsWidget m_ui;
++ QStringList m_components;
++ QList<QVersionNumber> m_versions;
++ QHelpFilterSettings m_filterSettings;
++ };
++
++ void QHelpFilterSettingsWidgetPrivate::setFilterSettings(const QHelpFilterSettings &settings)
++ {
++ QString currentFilter = m_itemToFilter.value(m_ui.filterWidget->currentItem());
++ if (currentFilter.isEmpty()) {
++ if (!m_filterSettings.currentFilter().isEmpty())
++ currentFilter = m_filterSettings.currentFilter();
++ else
++ currentFilter = settings.currentFilter();
++ }
++
++ m_filterSettings = settings;
++
++ m_ui.filterWidget->clear();
++ m_ui.componentWidget->clear();
++ m_ui.versionWidget->clear();
++ m_itemToFilter.clear();
++ m_filterToItem.clear();
++
++ const auto filters = m_filterSettings.filters();
++ for (auto it = filters.cbegin(); it != filters.cend(); ++it) {
++ const QString &filterName = it.key();
++ QListWidgetItem *item = new QListWidgetItem(filterName);
++ m_ui.filterWidget->addItem(item);
++ m_itemToFilter.insert(item, filterName);
++ m_filterToItem.insert(filterName, item);
++ if (filterName == currentFilter)
++ m_ui.filterWidget->setCurrentItem(item);
++ }
++
++ if (!m_ui.filterWidget->currentItem() && !m_filterToItem.isEmpty())
++ m_ui.filterWidget->setCurrentItem(m_filterToItem.first());
++
++ updateCurrentFilter();
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::updateCurrentFilter()
++ {
++ const QString ¤tFilter = m_itemToFilter.value(m_ui.filterWidget->currentItem());
++
++ const bool filterSelected = !currentFilter.isEmpty();
++ m_ui.componentWidget->setEnabled(filterSelected);
++ m_ui.versionWidget->setEnabled(filterSelected);
++ m_ui.renameButton->setEnabled(filterSelected);
++ m_ui.removeButton->setEnabled(filterSelected);
++
++ m_ui.componentWidget->setOptions(m_components,
++ m_filterSettings.filterData(currentFilter).components());
++ m_ui.versionWidget->setOptions(versionsToStringList(m_versions),
++ versionsToStringList(m_filterSettings.filterData(currentFilter).versions()));
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::componentsChanged(const QStringList &components)
++ {
++ const QString ¤tFilter = m_itemToFilter.value(m_ui.filterWidget->currentItem());
++ if (currentFilter.isEmpty())
++ return;
++
++ QHelpFilterData filterData = m_filterSettings.filterData(currentFilter);
++ filterData.setComponents(components);
++ m_filterSettings.setFilter(currentFilter, filterData);
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::versionsChanged(const QStringList &versions)
++ {
++ const QString ¤tFilter = m_itemToFilter.value(m_ui.filterWidget->currentItem());
++ if (currentFilter.isEmpty())
++ return;
++
++ QHelpFilterData filterData = m_filterSettings.filterData(currentFilter);
++ filterData.setVersions(stringListToVersions(versions));
++ m_filterSettings.setFilter(currentFilter, filterData);
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::addFilterClicked()
++ {
++ const QString newFilterName = getUniqueFilterName(QHelpFilterSettingsWidget::tr("Add Filter"),
++ suggestedNewFilterName(QHelpFilterSettingsWidget::tr("New Filter")));
++ if (newFilterName.isEmpty())
++ return;
++
++ addFilter(newFilterName);
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::renameFilterClicked()
++ {
++ const QString ¤tFilter = m_itemToFilter.value(m_ui.filterWidget->currentItem());
++ if (currentFilter.isEmpty())
++ return;
++
++ const QString newFilterName = getUniqueFilterName(QHelpFilterSettingsWidget::tr("Rename Filter"), currentFilter);
++ if (newFilterName.isEmpty())
++ return;
++
++ const QHelpFilterData oldFilterData = m_filterSettings.filterData(currentFilter);
++ removeFilter(currentFilter);
++ addFilter(newFilterName, oldFilterData);
++
++ if (m_filterSettings.currentFilter() == currentFilter)
++ m_filterSettings.setCurrentFilter(newFilterName);
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::removeFilterClicked()
++ {
++ Q_Q(QHelpFilterSettingsWidget);
++
++ const QString ¤tFilter = m_itemToFilter.value(m_ui.filterWidget->currentItem());
++ if (currentFilter.isEmpty())
++ return;
++
++ if (QMessageBox::question(q, QHelpFilterSettingsWidget::tr("Remove Filter"),
++ QHelpFilterSettingsWidget::tr("Are you sure you want to remove the \"%1\" filter?")
++ .arg(currentFilter), QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
++ return;
++ }
++
++ removeFilter(currentFilter);
++
++ if (m_filterSettings.currentFilter() == currentFilter)
++ m_filterSettings.setCurrentFilter({});
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::addFilter(const QString &filterName,
++ const QHelpFilterData &filterData)
++ {
++ QListWidgetItem *item = new QListWidgetItem(filterName);
++ m_filterSettings.setFilter(filterName, filterData);
++ m_filterToItem.insert(filterName, item);
++ m_itemToFilter.insert(item, filterName);
++ m_ui.filterWidget->insertItem(m_filterToItem.keys().indexOf(filterName), item);
++
++ m_ui.filterWidget->setCurrentItem(item);
++ updateCurrentFilter();
++ }
++
++ void QHelpFilterSettingsWidgetPrivate::removeFilter(const QString &filterName)
++ {
++ QListWidgetItem *item = m_filterToItem.value(filterName);
++ m_itemToFilter.remove(item);
++ m_filterToItem.remove(filterName);
++ delete item;
++ m_filterSettings.removeFilter(filterName);
++ }
++
++ QString QHelpFilterSettingsWidgetPrivate::getUniqueFilterName(const QString &windowTitle,
++ const QString &initialFilterName)
++ {
++ Q_Q(QHelpFilterSettingsWidget);
++
++ QString newFilterName = initialFilterName;
++ while (true) {
++ QFilterNameDialog dialog(q);
++ dialog.setWindowTitle(windowTitle);
++ dialog.setFilterName(newFilterName);
++ if (dialog.exec() == QDialog::Rejected)
++ return {};
++
++ newFilterName = dialog.filterName();
++ if (!m_filterToItem.contains(newFilterName))
++ break;
++
++ if (QMessageBox::warning(q, QHelpFilterSettingsWidget::tr("Filter Exists"),
++ QHelpFilterSettingsWidget::tr("The filter \"%1\" already exists.").arg(newFilterName),
++ QMessageBox::Retry | QMessageBox::Cancel) == QMessageBox::Cancel) {
++ return {};
++ }
++ }
++ return newFilterName;
++ }
++
++ QString QHelpFilterSettingsWidgetPrivate::suggestedNewFilterName(const QString &initialFilterName) const
++ {
++ QString newFilterName = initialFilterName;
++ int counter = 1;
++ while (m_filterToItem.contains(newFilterName))
++ newFilterName = initialFilterName + u' ' + QString::number(++counter);
++ return newFilterName;
++ }
++
++ /*!
++ \class QHelpFilterSettingsWidget
++ \inmodule QtHelp
++ \since 5.15
++ \brief The QHelpFilterSettingsWidget class provides a widget that allows
++ for creating, editing and removing filters.
++
++ The instance of QHelpFilterSettingsWidget may be a part of
++ a preferences dialog. Before showing the dialog, \l setAvailableComponents()
++ and \l setAvailableVersions() should be called, otherwise the filter
++ settings widget will only offer a creation of empty filters,
++ which wouldn't be useful. In addition, \l readSettings should also
++ be called to fill up the filter settings widget with the list of filters
++ already stored in the filter engine. The creation of new filters,
++ modifications to existing filters and removal of unneeded filters are
++ handled by the widget automatically. If you want to store the current
++ state of the widget and apply it to the filter engine e.g. after
++ the user clicked the apply button - call \l applySettings().
++ */
++
++ /*!
++ Constructs a filter settings widget with \a parent as parent widget.
++ */
++ QHelpFilterSettingsWidget::QHelpFilterSettingsWidget(QWidget *parent)
++ : QWidget(parent)
++ , d_ptr(new QHelpFilterSettingsWidgetPrivate())
++ {
++ Q_D(QHelpFilterSettingsWidget);
++ d->q_ptr = this;
++ d->m_ui.setupUi(this);
++
++ // TODO: make resources configurable
++ QString resourcePath = ":/qt-project.org/assistant/images/"_L1;
++ #ifdef Q_OS_MACOS
++ resourcePath.append("mac"_L1);
++ #else
++ resourcePath.append("win"_L1);
++ #endif
++ d->m_ui.addButton->setIcon(QIcon(resourcePath + "/plus.png"_L1));
++ d->m_ui.removeButton->setIcon(QIcon(resourcePath + "/minus.png"_L1));
++
++ connect(d->m_ui.componentWidget, &QOptionsWidget::optionSelectionChanged,
++ this, [this](const QStringList &options) {
++ Q_D(QHelpFilterSettingsWidget);
++ d->componentsChanged(options);
++ });
++ connect(d->m_ui.versionWidget, &QOptionsWidget::optionSelectionChanged,
++ this, [this](const QStringList &options) {
++ Q_D(QHelpFilterSettingsWidget);
++ d->versionsChanged(options);
++ });
++ connect(d->m_ui.filterWidget, &QListWidget::currentItemChanged,
++ this, [this](QListWidgetItem *) {
++ Q_D(QHelpFilterSettingsWidget);
++ d->updateCurrentFilter();
++ });
++ connect(d->m_ui.filterWidget, &QListWidget::itemDoubleClicked,
++ this, [this](QListWidgetItem *) {
++ Q_D(QHelpFilterSettingsWidget);
++ d->renameFilterClicked();
++ });
++
++ // TODO: repeat these actions on context menu
++ connect(d->m_ui.addButton, &QAbstractButton::clicked, this, [this] {
++ Q_D(QHelpFilterSettingsWidget);
++ d->addFilterClicked();
++ });
++ connect(d->m_ui.renameButton, &QAbstractButton::clicked, this, [this] {
++ Q_D(QHelpFilterSettingsWidget);
++ d->renameFilterClicked();
++ });
++ connect(d->m_ui.removeButton, &QAbstractButton::clicked, this, [this] {
++ Q_D(QHelpFilterSettingsWidget);
++ d->removeFilterClicked();
++ });
++
++ d->m_ui.componentWidget->setNoOptionText(tr("No Component"));
++ d->m_ui.componentWidget->setInvalidOptionText(tr("Invalid Component"));
++ d->m_ui.versionWidget->setNoOptionText(tr("No Version"));
++ d->m_ui.versionWidget->setInvalidOptionText(tr("Invalid Version"));
++ }
++
++ /*!
++ Destroys the filter settings widget.
++ */
++ QHelpFilterSettingsWidget::~QHelpFilterSettingsWidget() = default;
++
++ /*!
++ Sets the list of all available components to \a components.
++ \sa QHelpFilterEngine::availableComponents()
++ */
++ void QHelpFilterSettingsWidget::setAvailableComponents(const QStringList &components)
++ {
++ Q_D(QHelpFilterSettingsWidget);
++ d->m_components = components;
++ d->updateCurrentFilter();
++ }
++
++ /*!
++ Sets the list of all available version numbers to \a versions.
++ \sa QHelpFilterEngine::availableVersions()
++ */
++ void QHelpFilterSettingsWidget::setAvailableVersions(const QList<QVersionNumber> &versions)
++ {
++ Q_D(QHelpFilterSettingsWidget);
++ d->m_versions = versions;
++ d->updateCurrentFilter();
++ }
++
++ /*!
++ Reads the filter settings stored inside \a filterEngine and sets up
++ this filter settings widget accordingly.
++ */
++ void QHelpFilterSettingsWidget::readSettings(const QHelpFilterEngine *filterEngine)
++ {
++ Q_D(QHelpFilterSettingsWidget);
++ const QHelpFilterSettings settings = readSettingsHelper(filterEngine);
++ d->setFilterSettings(settings);
++ }
++
++ /*!
++ Writes the filter settings, currently presented in this filter settings
++ widget, to the \a filterEngine. The old settings stored in the filter
++ engine will be overwritten. Returns \c true on success.
++ */
++ bool QHelpFilterSettingsWidget::applySettings(QHelpFilterEngine *filterEngine) const
++ {
++ Q_D(const QHelpFilterSettingsWidget);
++ return applySettingsHelper(filterEngine, d->filterSettings());
++ }
++
++ QT_END_NAMESPACE