As the title says. This patch adds support for finding tooltip text, also extends widget types to QSpinBoxes/QDoubleSpinBoxes and also adds support for finding combobox items from QComboBox dropdown. Also adds support for QGroupBox titles.
2077 lines
70 KiB
C++
2077 lines
70 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
/****************************************************************************
|
|
* Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> *
|
|
* Copyright (c) 2023 FreeCAD Project Association *
|
|
* *
|
|
* This file is part of FreeCAD. *
|
|
* *
|
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
|
* under the terms of the GNU Lesser General Public License as *
|
|
* published by the Free Software Foundation, either version 2.1 of the *
|
|
* License, or (at your option) any later version. *
|
|
* *
|
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
* Lesser General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Lesser General Public *
|
|
* License along with FreeCAD. If not, see *
|
|
* <https://www.gnu.org/licenses/>. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <QAbstractButton>
|
|
#include <QApplication>
|
|
#include <QCheckBox>
|
|
#include <QDoubleSpinBox>
|
|
#include <QSpinBox>
|
|
#include <QComboBox>
|
|
#include <QCursor>
|
|
#include <QDebug>
|
|
#include <QFrame>
|
|
#include <QGroupBox>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QListWidget>
|
|
#include <QMap>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QRadioButton>
|
|
#include <QScreen>
|
|
#include <QScrollArea>
|
|
#include <QScrollBar>
|
|
#include <QTabWidget>
|
|
#include <QTimer>
|
|
#include <QToolButton>
|
|
#include <QToolTip>
|
|
#include <QProcess>
|
|
#include <QPushButton>
|
|
#include <QWindow>
|
|
#include <QKeyEvent>
|
|
#include <QFocusEvent>
|
|
#include <QMouseEvent>
|
|
#include <QPointer>
|
|
#include <QSet>
|
|
#include <QStyledItemDelegate>
|
|
#include <QPainter>
|
|
|
|
#include <App/Application.h>
|
|
#include <Base/Console.h>
|
|
#include <Base/Exception.h>
|
|
#include <Base/Tools.h>
|
|
|
|
#include "Dialogs/DlgPreferencesImp.h"
|
|
#include "ui_DlgPreferences.h"
|
|
#include "BitmapFactory.h"
|
|
#include "MainWindow.h"
|
|
#include "Tools.h"
|
|
#include "WidgetFactory.h"
|
|
|
|
using namespace Gui::Dialog;
|
|
|
|
// Simple delegate to render first line bold, second line normal
|
|
// used by search box
|
|
class MixedFontDelegate: public QStyledItemDelegate
|
|
{
|
|
static constexpr int horizontalPadding = 12;
|
|
static constexpr int verticalPadding = 4;
|
|
|
|
public:
|
|
explicit MixedFontDelegate(QObject* parent = nullptr)
|
|
: QStyledItemDelegate(parent)
|
|
{}
|
|
|
|
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
|
|
{
|
|
if (!index.isValid()) {
|
|
QStyledItemDelegate::paint(painter, option, index);
|
|
return;
|
|
}
|
|
|
|
QString pathText, widgetText;
|
|
extractTextData(index, pathText, widgetText);
|
|
|
|
if (pathText.isEmpty()) {
|
|
QStyledItemDelegate::paint(painter, option, index);
|
|
return;
|
|
}
|
|
|
|
QFont boldFont, normalFont;
|
|
createFonts(option.font, boldFont, normalFont);
|
|
|
|
LayoutInfo layout
|
|
= calculateLayout(pathText, widgetText, boldFont, normalFont, option.rect.width());
|
|
|
|
painter->save();
|
|
|
|
// draw selection background if selected
|
|
if (option.state & QStyle::State_Selected) {
|
|
painter->fillRect(option.rect, option.palette.highlight());
|
|
}
|
|
|
|
// Set text color based on selection
|
|
QColor textColor = (option.state & QStyle::State_Selected)
|
|
? option.palette.highlightedText().color()
|
|
: option.palette.text().color();
|
|
painter->setPen(textColor);
|
|
|
|
// draw path in bold (Tab/Page) with wrapping
|
|
painter->setFont(boldFont);
|
|
QRect boldRect(
|
|
option.rect.left() + horizontalPadding,
|
|
option.rect.top() + verticalPadding,
|
|
layout.availableWidth,
|
|
layout.pathHeight
|
|
);
|
|
painter->drawText(boldRect, Qt::TextWordWrap | Qt::AlignTop, pathText);
|
|
|
|
// draw widget text in normal font (if present)
|
|
if (!widgetText.isEmpty()) {
|
|
painter->setFont(normalFont);
|
|
QRect normalRect(
|
|
option.rect.left() + horizontalPadding,
|
|
option.rect.top() + verticalPadding + layout.pathHeight,
|
|
layout.availableWidth,
|
|
layout.widgetHeight
|
|
);
|
|
painter->drawText(normalRect, Qt::TextWordWrap | Qt::AlignTop, widgetText);
|
|
}
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override
|
|
{
|
|
if (!index.isValid()) {
|
|
return QStyledItemDelegate::sizeHint(option, index);
|
|
}
|
|
|
|
QString pathText, widgetText;
|
|
extractTextData(index, pathText, widgetText);
|
|
|
|
if (pathText.isEmpty()) {
|
|
return QStyledItemDelegate::sizeHint(option, index);
|
|
}
|
|
|
|
QFont boldFont, normalFont;
|
|
createFonts(option.font, boldFont, normalFont);
|
|
|
|
LayoutInfo layout
|
|
= calculateLayout(pathText, widgetText, boldFont, normalFont, option.rect.width());
|
|
|
|
return {layout.totalWidth, layout.totalHeight};
|
|
}
|
|
|
|
private:
|
|
struct LayoutInfo
|
|
{
|
|
int availableWidth;
|
|
int pathHeight;
|
|
int widgetHeight;
|
|
int totalWidth;
|
|
int totalHeight;
|
|
};
|
|
|
|
void extractTextData(const QModelIndex& index, QString& pathText, QString& widgetText) const
|
|
{
|
|
// Use separate roles - all items should have proper role data
|
|
pathText = index.data(PreferencesSearchController::PathRole).toString();
|
|
widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString();
|
|
}
|
|
|
|
void createFonts(const QFont& baseFont, QFont& boldFont, QFont& normalFont) const
|
|
{
|
|
boldFont = baseFont;
|
|
boldFont.setBold(true);
|
|
boldFont.setPointSize(boldFont.pointSize() - 1); // make header smaller like a subtitle
|
|
|
|
normalFont = baseFont; // keep widget text at normal size
|
|
}
|
|
|
|
LayoutInfo calculateLayout(
|
|
const QString& pathText,
|
|
const QString& widgetText,
|
|
const QFont& boldFont,
|
|
const QFont& normalFont,
|
|
int containerWidth
|
|
) const
|
|
{
|
|
|
|
QFontMetrics boldFm(boldFont);
|
|
QFontMetrics normalFm(normalFont);
|
|
|
|
int availableWidth = containerWidth
|
|
- horizontalPadding * 2; // account for left and right padding
|
|
if (availableWidth <= 0) {
|
|
constexpr int defaultPopupWidth = 300;
|
|
availableWidth = defaultPopupWidth
|
|
- horizontalPadding * 2; // Fallback to popup width minus padding
|
|
}
|
|
|
|
// Calculate dimensions for path text (bold)
|
|
QRect pathBoundingRect
|
|
= boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText);
|
|
int pathHeight = pathBoundingRect.height();
|
|
int pathWidth = pathBoundingRect.width();
|
|
|
|
// Calculate dimensions for widget text (normal font, if present)
|
|
int widgetHeight = 0;
|
|
int widgetWidth = 0;
|
|
if (!widgetText.isEmpty()) {
|
|
QRect widgetBoundingRect
|
|
= normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText);
|
|
widgetHeight = widgetBoundingRect.height();
|
|
widgetWidth = widgetBoundingRect.width();
|
|
}
|
|
|
|
int totalWidth = qMax(pathWidth, widgetWidth)
|
|
+ horizontalPadding * 2; // +24 horizontal padding
|
|
int totalHeight = verticalPadding * 2 + pathHeight
|
|
+ widgetHeight; // 8 vertical padding + content heights
|
|
|
|
LayoutInfo layout;
|
|
layout.availableWidth = availableWidth;
|
|
layout.pathHeight = pathHeight;
|
|
layout.widgetHeight = widgetHeight;
|
|
layout.totalWidth = totalWidth;
|
|
layout.totalHeight = totalHeight;
|
|
return layout;
|
|
}
|
|
};
|
|
|
|
bool isParentOf(const QModelIndex& parent, const QModelIndex& child)
|
|
{
|
|
for (auto it = child; it.isValid(); it = it.parent()) {
|
|
if (it == parent) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QModelIndex findRootIndex(const QModelIndex& index)
|
|
{
|
|
auto root = index;
|
|
|
|
while (root.parent().isValid()) {
|
|
root = root.parent();
|
|
}
|
|
|
|
return root;
|
|
}
|
|
|
|
QWidget* PreferencesPageItem::getWidget() const
|
|
{
|
|
return _widget;
|
|
}
|
|
|
|
void PreferencesPageItem::setWidget(QWidget* widget)
|
|
{
|
|
if (_widget) {
|
|
_widget->setProperty(PropertyName, QVariant::fromValue<PreferencesPageItem*>(nullptr));
|
|
}
|
|
|
|
_widget = widget;
|
|
_widget->setProperty(PropertyName, QVariant::fromValue(this));
|
|
}
|
|
|
|
bool PreferencesPageItem::isExpanded() const
|
|
{
|
|
return _expanded;
|
|
}
|
|
|
|
void PreferencesPageItem::setExpanded(bool expanded)
|
|
{
|
|
_expanded = expanded;
|
|
}
|
|
|
|
// NOLINTBEGIN
|
|
Q_DECLARE_METATYPE(PreferencesPageItem*);
|
|
|
|
const int DlgPreferencesImp::GroupNameRole = Qt::UserRole + 1;
|
|
const int DlgPreferencesImp::PageNameRole = Qt::UserRole + 2;
|
|
|
|
/* TRANSLATOR Gui::Dialog::DlgPreferencesImp */
|
|
|
|
std::list<DlgPreferencesImp::TGroupPages> DlgPreferencesImp::_pages;
|
|
std::map<std::string, DlgPreferencesImp::Group> DlgPreferencesImp::_groupMap;
|
|
|
|
DlgPreferencesImp* DlgPreferencesImp::_activeDialog = nullptr;
|
|
// NOLINTEND
|
|
|
|
/**
|
|
* Constructs a DlgPreferencesImp which is a child of 'parent', with
|
|
* widget flags set to 'fl'
|
|
*
|
|
* The dialog will by default be modeless, unless you set 'modal' to
|
|
* true to construct a modal dialog.
|
|
*/
|
|
DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl)
|
|
: QDialog(parent, fl)
|
|
, ui(new Ui_DlgPreferences)
|
|
, invalidParameter(false)
|
|
, canEmbedScrollArea(true)
|
|
, restartRequired(false)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
// remove unused help button
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
// Initialize search controller
|
|
m_searchController = std::make_unique<PreferencesSearchController>(this, this);
|
|
|
|
setupConnections();
|
|
|
|
ui->groupsTreeView->setModel(&_model);
|
|
|
|
// Configure search controller after UI setup
|
|
m_searchController->setPreferencesModel(&_model);
|
|
m_searchController->setGroupNameRole(GroupNameRole);
|
|
m_searchController->setPageNameRole(PageNameRole);
|
|
|
|
setupPages();
|
|
|
|
// Maintain a static pointer to the current active dialog (if there is one) so that
|
|
// if the static page manipulation functions are called while the dialog is showing
|
|
// it can update its content.
|
|
DlgPreferencesImp::_activeDialog = this;
|
|
}
|
|
|
|
/**
|
|
* Destroys the object and frees any allocated resources.
|
|
*/
|
|
DlgPreferencesImp::~DlgPreferencesImp()
|
|
{
|
|
// Remove global event filter
|
|
qApp->removeEventFilter(this);
|
|
|
|
if (DlgPreferencesImp::_activeDialog == this) {
|
|
DlgPreferencesImp::_activeDialog = nullptr;
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::setupConnections()
|
|
{
|
|
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &DlgPreferencesImp::onButtonBoxClicked);
|
|
connect(ui->buttonBox, &QDialogButtonBox::helpRequested, getMainWindow(), &MainWindow::whatsThis);
|
|
connect(ui->groupsTreeView, &QTreeView::clicked, this, &DlgPreferencesImp::onPageSelected);
|
|
connect(ui->groupsTreeView, &QTreeView::expanded, this, &DlgPreferencesImp::onGroupExpanded);
|
|
connect(ui->groupsTreeView, &QTreeView::collapsed, this, &DlgPreferencesImp::onGroupCollapsed);
|
|
connect(ui->buttonReset, &QPushButton::clicked, this, &DlgPreferencesImp::showResetOptions);
|
|
connect(
|
|
ui->groupWidgetStack,
|
|
&QStackedWidget::currentChanged,
|
|
this,
|
|
&DlgPreferencesImp::onStackWidgetChange
|
|
);
|
|
// Connect search functionality to controller
|
|
connect(
|
|
ui->searchBox,
|
|
&QLineEdit::textChanged,
|
|
m_searchController.get(),
|
|
&PreferencesSearchController::onSearchTextChanged
|
|
);
|
|
|
|
// Connect navigation signal from controller to dialog
|
|
connect(
|
|
m_searchController.get(),
|
|
&PreferencesSearchController::navigationRequested,
|
|
this,
|
|
&DlgPreferencesImp::onNavigationRequested
|
|
);
|
|
|
|
// Install event filter on search box for arrow key navigation
|
|
ui->searchBox->installEventFilter(this);
|
|
|
|
// Install global event filter to handle clicks outside popup
|
|
qApp->installEventFilter(this);
|
|
}
|
|
|
|
void DlgPreferencesImp::setupPages()
|
|
{
|
|
// make sure that pages are ready to create
|
|
GetWidgetFactorySupplier();
|
|
|
|
for (const auto& [name, pages] : _pages) {
|
|
auto* group = createGroup(name);
|
|
|
|
for (const auto& page : pages) {
|
|
createPageInGroup(group, page);
|
|
}
|
|
}
|
|
|
|
updatePageDependentWidgets();
|
|
}
|
|
|
|
QPixmap DlgPreferencesImp::loadIconForGroup(const std::string& name) const
|
|
{
|
|
// normalize file name
|
|
auto normalizeName = [](std::string str) {
|
|
std::transform(str.begin(), str.end(), str.begin(), [](unsigned char ch) {
|
|
return ch == ' ' ? '_' : std::tolower(ch);
|
|
});
|
|
return str;
|
|
};
|
|
|
|
std::string fileName = normalizeName(name);
|
|
fileName = std::string("preferences-") + fileName;
|
|
|
|
const int px = 24;
|
|
QSize iconSize(px, px);
|
|
QPixmap icon = Gui::BitmapFactory().pixmapFromSvg(fileName.c_str(), iconSize);
|
|
|
|
if (icon.isNull()) {
|
|
icon = Gui::BitmapFactory().pixmap(fileName.c_str());
|
|
if (icon.isNull()) {
|
|
qWarning() << "No group icon found for " << fileName.c_str();
|
|
}
|
|
else if (icon.size() != iconSize) {
|
|
icon = icon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
}
|
|
|
|
return icon;
|
|
}
|
|
|
|
/**
|
|
* Create the necessary widgets for a new group named \a groupName. Returns a
|
|
* pointer to the group's SettingsPageItem: that widget's lifetime is managed by the
|
|
* QStandardItemModel, do not manually deallocate.
|
|
*/
|
|
PreferencesPageItem* DlgPreferencesImp::createGroup(const std::string& groupName)
|
|
{
|
|
QString groupNameQString = QString::fromStdString(groupName);
|
|
|
|
std::string iconName;
|
|
|
|
QString tooltip;
|
|
getGroupData(groupName, iconName, tooltip);
|
|
|
|
auto groupPages = new QStackedWidget;
|
|
groupPages->setProperty(GroupNameProperty, QVariant(groupNameQString));
|
|
|
|
connect(groupPages, &QStackedWidget::currentChanged, this, &DlgPreferencesImp::onStackWidgetChange);
|
|
|
|
|
|
if (ui->groupWidgetStack->count() > 0) {
|
|
groupPages->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
|
}
|
|
ui->groupWidgetStack->addWidget(groupPages);
|
|
|
|
auto item = new PreferencesPageItem;
|
|
|
|
item->setData(QVariant(groupNameQString), GroupNameRole);
|
|
item->setText(QObject::tr(groupNameQString.toLatin1()));
|
|
item->setToolTip(tooltip);
|
|
item->setIcon(loadIconForGroup(iconName));
|
|
item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
item->setWidget(groupPages);
|
|
item->setSelectable(false);
|
|
|
|
_model.invisibleRootItem()->appendRow(item);
|
|
|
|
return item;
|
|
}
|
|
|
|
|
|
PreferencePage* DlgPreferencesImp::createPreferencePage(
|
|
const std::string& pageName,
|
|
const std::string& groupName
|
|
)
|
|
{
|
|
PreferencePage* page = WidgetFactory().createPreferencePage(pageName.c_str());
|
|
|
|
if (!page) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto resetMargins = [](QWidget* widget) {
|
|
widget->setContentsMargins(0, 0, 0, 0);
|
|
if (auto layout = widget->layout()) {
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
}
|
|
};
|
|
|
|
// settings layout already takes care for margins, we need to reset everything to 0
|
|
resetMargins(page);
|
|
|
|
// special handling for PreferenceUiForm to reset margins for forms too
|
|
if (auto uiFormPage = qobject_cast<PreferenceUiForm*>(page)) {
|
|
resetMargins(uiFormPage->form());
|
|
}
|
|
|
|
page->setProperty(GroupNameProperty, QString::fromStdString(groupName));
|
|
page->setProperty(PageNameProperty, QString::fromStdString(pageName));
|
|
|
|
return page;
|
|
}
|
|
|
|
/**
|
|
* Create a new preference page called \a pageName in the group \a groupItem.
|
|
*/
|
|
void DlgPreferencesImp::createPageInGroup(PreferencesPageItem* groupItem, const std::string& pageName)
|
|
{
|
|
try {
|
|
PreferencePage* page
|
|
= createPreferencePage(pageName, groupItem->data(GroupNameRole).toString().toStdString());
|
|
|
|
if (!page) {
|
|
Base::Console().warning("%s is not a preference page\n", pageName.c_str());
|
|
return;
|
|
}
|
|
|
|
auto pageItem = new PreferencesPageItem;
|
|
|
|
pageItem->setText(page->windowTitle());
|
|
pageItem->setEditable(false);
|
|
pageItem->setData(groupItem->data(GroupNameRole), GroupNameRole);
|
|
pageItem->setData(QString::fromStdString(pageName), PageNameRole);
|
|
pageItem->setWidget(page);
|
|
|
|
groupItem->appendRow(pageItem);
|
|
|
|
page->loadSettings();
|
|
|
|
auto pages = qobject_cast<QStackedWidget*>(groupItem->getWidget());
|
|
|
|
if (pages->count() > 0) {
|
|
page->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
|
}
|
|
|
|
pages->addWidget(page);
|
|
addSizeHint(page);
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
Base::Console().error("Base exception thrown for '%s'\n", pageName.c_str());
|
|
e.reportException();
|
|
}
|
|
catch (const std::exception& e) {
|
|
Base::Console().error("C++ exception thrown for '%s' (%s)\n", pageName.c_str(), e.what());
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::addSizeHint(QWidget* page)
|
|
{
|
|
_sizeHintOfPages = _sizeHintOfPages.expandedTo(page->minimumSizeHint());
|
|
}
|
|
|
|
int DlgPreferencesImp::minimumPageWidth() const
|
|
{
|
|
return _sizeHintOfPages.width();
|
|
}
|
|
|
|
int DlgPreferencesImp::minimumDialogWidth(int pageWidth) const
|
|
{
|
|
// this is additional safety spacing to ensure that everything fits with scrollbar etc.
|
|
const auto additionalMargin = style()->pixelMetric(QStyle::PM_ScrollBarExtent) + 8;
|
|
|
|
QSize tree = ui->groupsTreeView->sizeHint();
|
|
return pageWidth + tree.width() + additionalMargin;
|
|
}
|
|
|
|
void DlgPreferencesImp::updatePageDependentWidgets()
|
|
{
|
|
auto currentPageItem = getCurrentPage();
|
|
|
|
// update header of the page
|
|
ui->headerLabel->setText(currentPageItem->text());
|
|
|
|
// reset scroll area to start position
|
|
ui->scrollArea->horizontalScrollBar()->setValue(0);
|
|
ui->scrollArea->verticalScrollBar()->setValue(0);
|
|
}
|
|
|
|
/**
|
|
* Adds a preference page with its class name \a className and
|
|
* the group \a group it belongs to. To create this page it must
|
|
* be registered in the WidgetFactory.
|
|
* @see WidgetFactory
|
|
* @see PrefPageProducer
|
|
*/
|
|
void DlgPreferencesImp::addPage(const std::string& className, const std::string& group)
|
|
{
|
|
auto groupToAddTo = _pages.end();
|
|
for (auto it = _pages.begin(); it != _pages.end(); ++it) {
|
|
if (it->first == group) {
|
|
groupToAddTo = it;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (groupToAddTo != _pages.end()) {
|
|
// The group exists: add this page to the end of the list
|
|
groupToAddTo->second.push_back(className);
|
|
}
|
|
else {
|
|
// This is a new group: create it, with its one page
|
|
std::list<std::string> pages;
|
|
pages.push_back(className);
|
|
_pages.emplace_back(group, pages);
|
|
}
|
|
|
|
if (DlgPreferencesImp::_activeDialog) {
|
|
// If the dialog is currently showing, tell it to insert the new page
|
|
_activeDialog->reloadPages();
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::removePage(const std::string& className, const std::string& group)
|
|
{
|
|
for (auto it = _pages.begin(); it != _pages.end(); ++it) {
|
|
if (it->first == group) {
|
|
if (className.empty()) {
|
|
_pages.erase(it);
|
|
return;
|
|
}
|
|
|
|
std::list<std::string>& p = it->second;
|
|
for (auto jt = p.begin(); jt != p.end(); ++jt) {
|
|
if (*jt == className) {
|
|
p.erase(jt);
|
|
if (p.empty()) {
|
|
_pages.erase(it);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a custom icon name or tool tip for a given group.
|
|
*/
|
|
void DlgPreferencesImp::setGroupData(const std::string& name, const std::string& icon, const QString& tip)
|
|
{
|
|
Group group;
|
|
group.iconName = icon;
|
|
group.tooltip = tip;
|
|
_groupMap[name] = group;
|
|
}
|
|
|
|
/**
|
|
* Gets the icon name or tool tip for a given group. If no custom name or tool tip is given
|
|
* they are determined from the group name.
|
|
*/
|
|
void DlgPreferencesImp::getGroupData(const std::string& group, std::string& icon, QString& tip)
|
|
{
|
|
auto it = _groupMap.find(group);
|
|
|
|
if (it != _groupMap.end()) {
|
|
icon = it->second.iconName;
|
|
tip = it->second.tooltip;
|
|
}
|
|
|
|
if (icon.empty()) {
|
|
icon = group;
|
|
}
|
|
|
|
if (tip.isEmpty()) {
|
|
tip = QObject::tr(group.c_str());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Activates the page at position \a index of the group with name \a group.
|
|
*/
|
|
void DlgPreferencesImp::activateGroupPage(const QString& group, int index)
|
|
{
|
|
for (int i = 0; i < ui->groupWidgetStack->count(); i++) {
|
|
auto* pageStackWidget = qobject_cast<QStackedWidget*>(ui->groupWidgetStack->widget(i));
|
|
|
|
if (!pageStackWidget) {
|
|
continue;
|
|
}
|
|
|
|
if (pageStackWidget->property(GroupNameProperty).toString() == group) {
|
|
ui->groupWidgetStack->setCurrentWidget(pageStackWidget);
|
|
pageStackWidget->setCurrentIndex(index);
|
|
|
|
updatePageDependentWidgets();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Activates the page with name \a pageName of the group with name \a group.
|
|
*/
|
|
void DlgPreferencesImp::activateGroupPageByPageName(const QString& group, const QString& pageName)
|
|
{
|
|
|
|
for (int i = 0; i < ui->groupWidgetStack->count(); i++) {
|
|
auto* pageStackWidget = qobject_cast<QStackedWidget*>(ui->groupWidgetStack->widget(i));
|
|
|
|
if (!pageStackWidget) {
|
|
continue;
|
|
}
|
|
|
|
if (pageStackWidget->property(GroupNameProperty).toString() == group) {
|
|
ui->groupWidgetStack->setCurrentWidget(pageStackWidget);
|
|
for (int pageIdx = 0; pageIdx < pageStackWidget->count(); pageIdx++) {
|
|
auto page = qobject_cast<PreferencePage*>(pageStackWidget->widget(pageIdx));
|
|
if (page) {
|
|
if (page->property(PageNameProperty).toString() == pageName) {
|
|
pageStackWidget->setCurrentIndex(pageIdx);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
updatePageDependentWidgets();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the group name \a group and position \a index of the active page.
|
|
*/
|
|
void DlgPreferencesImp::activeGroupPage(QString& group, int& index) const
|
|
{
|
|
auto groupWidget = qobject_cast<QStackedWidget*>(ui->groupWidgetStack->currentWidget());
|
|
|
|
if (groupWidget) {
|
|
group = groupWidget->property(GroupNameProperty).toString();
|
|
index = groupWidget->currentIndex();
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::accept()
|
|
{
|
|
this->invalidParameter = false;
|
|
|
|
applyChanges();
|
|
|
|
if (!this->invalidParameter) {
|
|
QDialog::accept();
|
|
restartIfRequired();
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::reject()
|
|
{
|
|
QDialog::reject();
|
|
restartIfRequired();
|
|
}
|
|
|
|
void DlgPreferencesImp::onButtonBoxClicked(QAbstractButton* btn)
|
|
{
|
|
if (ui->buttonBox->standardButton(btn) == QDialogButtonBox::Apply) {
|
|
applyChanges();
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::showResetOptions()
|
|
{
|
|
QMenu menu(this);
|
|
|
|
auto currentPageItem = getCurrentPage();
|
|
auto currentGroupItem = static_cast<PreferencesPageItem*>(currentPageItem->parent());
|
|
|
|
auto pageText = currentPageItem->text();
|
|
auto groupText = currentGroupItem->text();
|
|
|
|
// Reset per page
|
|
QAction* pageAction = menu.addAction(tr("Reset Page '%1'").arg(pageText), this, [&] {
|
|
restorePageDefaults(currentPageItem);
|
|
});
|
|
pageAction->setToolTip(tr("Resets the user settings for the page '%1'").arg(pageText));
|
|
|
|
// Reset per group
|
|
QAction* groupAction = menu.addAction(
|
|
tr("Reset Group '%1'").arg(groupText),
|
|
|
|
this,
|
|
[&] { restorePageDefaults(static_cast<PreferencesPageItem*>(currentPageItem->parent())); }
|
|
);
|
|
groupAction->setToolTip(tr("Resets the user settings for the group '%1'").arg(groupText));
|
|
|
|
// Reset all
|
|
QAction* allAction = menu.addAction(tr("Reset All"), this, &DlgPreferencesImp::restoreDefaults);
|
|
|
|
allAction->setToolTip(tr("Resets the user settings entirely"));
|
|
|
|
connect(&menu, &QMenu::hovered, [&menu](QAction* hover) {
|
|
QPoint pos = menu.pos();
|
|
const int pad = 10;
|
|
pos.rx() += menu.width() + pad;
|
|
QToolTip::showText(pos, hover->toolTip());
|
|
});
|
|
|
|
menu.exec(QCursor::pos());
|
|
}
|
|
|
|
void DlgPreferencesImp::restoreDefaults()
|
|
{
|
|
QMessageBox box(this);
|
|
box.setIcon(QMessageBox::Question);
|
|
box.setWindowTitle(tr("Clear User Settings"));
|
|
box.setText(tr("Clear all your user settings?"));
|
|
box.setInformativeText(tr("All settings will be cleared."));
|
|
box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
|
box.setDefaultButton(QMessageBox::No);
|
|
|
|
if (box.exec() == QMessageBox::Yes) {
|
|
// keep this parameter
|
|
bool saveParameter = App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/General")
|
|
->GetBool("SaveUserParameter", true);
|
|
|
|
ParameterManager* mgr = App::GetApplication().GetParameterSet("User parameter");
|
|
mgr->Clear();
|
|
|
|
App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/General")
|
|
->SetBool("SaveUserParameter", saveParameter);
|
|
|
|
reject();
|
|
}
|
|
}
|
|
/**
|
|
* If the dialog is currently showing and the static variable _pages changed, this function
|
|
* will rescan that list of pages and add any that are new to the current dialog. It will not
|
|
* remove any pages that are no longer in the list, and will not change the user's current
|
|
* active page.
|
|
*/
|
|
void DlgPreferencesImp::reloadPages()
|
|
{
|
|
// Make sure that pages are ready to create
|
|
GetWidgetFactorySupplier();
|
|
|
|
for (const auto& [group, pages] : _pages) {
|
|
QString groupName = QString::fromStdString(group);
|
|
|
|
// First, does this group already exist?
|
|
PreferencesPageItem* groupItem = nullptr;
|
|
|
|
auto root = _model.invisibleRootItem();
|
|
for (int i = 0; i < root->rowCount(); i++) {
|
|
auto currentGroupItem = static_cast<PreferencesPageItem*>(root->child(i));
|
|
auto currentGroupName = currentGroupItem->data(GroupNameRole).toString();
|
|
|
|
if (currentGroupName == groupName) {
|
|
groupItem = currentGroupItem;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This is a new group that wasn't there when we started this instance of the dialog:
|
|
if (!groupItem) {
|
|
groupItem = createGroup(group);
|
|
}
|
|
|
|
// Move on to the pages in the group to see if we need to add any
|
|
for (const auto& page : pages) {
|
|
// Does this page already exist?
|
|
QString pageName = QString::fromStdString(page);
|
|
|
|
bool pageExists = false;
|
|
|
|
for (int i = 0; i < groupItem->rowCount(); i++) {
|
|
auto currentPageItem = static_cast<PreferencesPageItem*>(groupItem->child(i));
|
|
|
|
if (currentPageItem->data(PageNameRole).toString() == pageName) {
|
|
pageExists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This is a new page that wasn't there when we started this instance of the dialog:
|
|
if (!pageExists) {
|
|
createPageInGroup(groupItem, page);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::applyChanges()
|
|
{
|
|
// Checks if any of the classes that represent several pages of settings
|
|
// (DlgSettings*.*) implement checkSettings() method. If any of them do,
|
|
// call it to validate if user input is correct. If something fails (i.e.,
|
|
// not correct), shows a messageBox and set this->invalidParameter = true to
|
|
// cancel further operation in other methods (like in accept()).
|
|
|
|
for (int i = 0; i < ui->groupWidgetStack->count(); i++) {
|
|
auto pagesStackWidget = qobject_cast<QStackedWidget*>(ui->groupWidgetStack->widget(i));
|
|
|
|
for (int j = 0; j < pagesStackWidget->count(); j++) {
|
|
QWidget* page = pagesStackWidget->widget(j);
|
|
|
|
int index = page->metaObject()->indexOfMethod("checkSettings()");
|
|
|
|
if (index >= 0) {
|
|
try {
|
|
page->qt_metacall(QMetaObject::InvokeMetaMethod, index, nullptr);
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
ui->groupWidgetStack->setCurrentIndex(i);
|
|
pagesStackWidget->setCurrentIndex(j);
|
|
|
|
QMessageBox::warning(this, tr("Wrong parameter"), QString::fromLatin1(e.what()));
|
|
|
|
this->invalidParameter = true;
|
|
|
|
// exit early due to found errors
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If everything is ok (i.e., no validation problem), call method
|
|
// saveSettings() in every subpage (DlgSetting*) object.
|
|
for (int i = 0; i < ui->groupWidgetStack->count(); i++) {
|
|
auto pageStackWidget = qobject_cast<QStackedWidget*>(ui->groupWidgetStack->widget(i));
|
|
|
|
for (int j = 0; j < pageStackWidget->count(); j++) {
|
|
auto page = qobject_cast<PreferencePage*>(pageStackWidget->widget(j));
|
|
|
|
if (page) {
|
|
page->saveSettings();
|
|
restartRequired = restartRequired || page->isRestartRequired();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool saveParameter = App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/General")
|
|
->GetBool("SaveUserParameter", true);
|
|
|
|
if (saveParameter) {
|
|
ParameterManager* parmgr = App::GetApplication().GetParameterSet("User parameter");
|
|
parmgr->SaveDocument(App::Application::Config()["UserParameter"].c_str());
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::restartIfRequired()
|
|
{
|
|
if (restartRequired) {
|
|
QMessageBox restartBox(parentWidget()); // current window likely already closed, cant
|
|
// parent to it
|
|
|
|
restartBox.setIcon(QMessageBox::Warning);
|
|
restartBox.setWindowTitle(tr("Restart Required"));
|
|
restartBox.setText(tr("Restart FreeCAD for changes to take effect."));
|
|
restartBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
|
|
restartBox.setDefaultButton(QMessageBox::Cancel);
|
|
auto okBtn = restartBox.button(QMessageBox::Ok);
|
|
auto cancelBtn = restartBox.button(QMessageBox::Cancel);
|
|
okBtn->setText(tr("Restart Now"));
|
|
cancelBtn->setText(tr("Restart Later"));
|
|
|
|
int exec = restartBox.exec();
|
|
|
|
if (exec == QMessageBox::Ok) {
|
|
// restart FreeCAD after a delay to give time to this dialog to close
|
|
const int ms = 1000;
|
|
QTimer::singleShot(ms, []() {
|
|
QStringList args = QApplication::arguments();
|
|
args.pop_front();
|
|
if (getMainWindow()->close()) {
|
|
QProcess::startDetached(QApplication::applicationFilePath(), args);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::showEvent(QShowEvent* ev)
|
|
{
|
|
QDialog::showEvent(ev);
|
|
|
|
auto screen = windowHandle()->screen();
|
|
auto availableSize = screen->availableSize();
|
|
|
|
// leave some portion of height so preferences window does not take
|
|
// entire screen height. User will still be able to resize the window,
|
|
// but it should never start too tall.
|
|
auto maxStartHeight = availableSize.height() - minVerticalEmptySpace;
|
|
|
|
if (height() > maxStartHeight) {
|
|
auto heightDifference = availableSize.height() - maxStartHeight;
|
|
|
|
// resize and reposition window so it is fully visible
|
|
resize(width(), maxStartHeight);
|
|
move(x(), heightDifference / 2);
|
|
}
|
|
|
|
expandToMinimumDialogWidth();
|
|
}
|
|
|
|
void DlgPreferencesImp::expandToMinimumDialogWidth()
|
|
{
|
|
auto screen = windowHandle()->screen();
|
|
auto availableSize = screen->availableSize();
|
|
|
|
int mw = minimumDialogWidth(minimumPageWidth());
|
|
// expand dialog to minimum size required but do not use more than specified width portion
|
|
resize(std::min(int(maxScreenWidthCoveragePercent * availableSize.width()), mw), height());
|
|
}
|
|
|
|
void DlgPreferencesImp::onPageSelected(const QModelIndex& index)
|
|
{
|
|
auto* currentItem = static_cast<PreferencesPageItem*>(_model.itemFromIndex(index));
|
|
|
|
if (currentItem->hasChildren()) {
|
|
auto pageIndex = currentItem->child(0)->index();
|
|
|
|
ui->groupsTreeView->selectionModel()->select(pageIndex, QItemSelectionModel::ClearAndSelect);
|
|
|
|
onPageSelected(pageIndex);
|
|
|
|
return;
|
|
}
|
|
|
|
auto groupIndex = findRootIndex(index);
|
|
|
|
auto* groupItem = static_cast<PreferencesPageItem*>(_model.itemFromIndex(groupIndex));
|
|
auto* pagesStackWidget = static_cast<QStackedWidget*>(groupItem->getWidget());
|
|
|
|
ui->groupWidgetStack->setCurrentWidget(groupItem->getWidget());
|
|
|
|
if (index != groupIndex) {
|
|
pagesStackWidget->setCurrentIndex(index.row());
|
|
}
|
|
|
|
updatePageDependentWidgets();
|
|
}
|
|
|
|
void DlgPreferencesImp::onGroupExpanded(const QModelIndex& index)
|
|
{
|
|
auto root = findRootIndex(index);
|
|
|
|
auto* groupItem = static_cast<PreferencesPageItem*>(_model.itemFromIndex(root));
|
|
|
|
groupItem->setExpanded(true);
|
|
}
|
|
|
|
void DlgPreferencesImp::onGroupCollapsed(const QModelIndex& index)
|
|
{
|
|
auto root = findRootIndex(index);
|
|
|
|
auto* groupItem = static_cast<PreferencesPageItem*>(_model.itemFromIndex(root));
|
|
|
|
groupItem->setExpanded(false);
|
|
}
|
|
|
|
void DlgPreferencesImp::onStackWidgetChange(int index)
|
|
{
|
|
auto stack = qobject_cast<QStackedWidget*>(sender());
|
|
|
|
for (int i = 0; i < stack->count(); i++) {
|
|
auto current = stack->widget(i);
|
|
current->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
|
}
|
|
|
|
if (auto selected = stack->widget(index)) {
|
|
selected->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
}
|
|
|
|
auto currentItem = getCurrentPage();
|
|
|
|
if (!currentItem) {
|
|
return;
|
|
}
|
|
|
|
auto currentIndex = currentItem->index();
|
|
|
|
auto root = _model.invisibleRootItem();
|
|
for (int i = 0; i < root->rowCount(); i++) {
|
|
auto currentGroup = static_cast<PreferencesPageItem*>(root->child(i));
|
|
auto currentGroupIndex = currentGroup->index();
|
|
|
|
// don't do anything to group of selected item
|
|
if (isParentOf(currentGroupIndex, currentIndex)) {
|
|
continue;
|
|
}
|
|
|
|
if (!currentGroup->isExpanded()) {
|
|
ui->groupsTreeView->collapse(currentGroupIndex);
|
|
}
|
|
}
|
|
|
|
auto parentItem = currentItem;
|
|
while ((parentItem = static_cast<PreferencesPageItem*>(parentItem->parent()))) {
|
|
bool wasExpanded = parentItem->isExpanded();
|
|
ui->groupsTreeView->expand(parentItem->index());
|
|
parentItem->setExpanded(wasExpanded);
|
|
}
|
|
|
|
ui->groupsTreeView->selectionModel()->select(currentIndex, QItemSelectionModel::ClearAndSelect);
|
|
}
|
|
|
|
void DlgPreferencesImp::onNavigationRequested(const QString& groupName, const QString& pageName)
|
|
{
|
|
navigateToSearchResult(groupName, pageName);
|
|
}
|
|
|
|
void DlgPreferencesImp::navigateToSearchResult(const QString& groupName, const QString& pageName)
|
|
{
|
|
// Find the group and page items
|
|
auto root = _model.invisibleRootItem();
|
|
for (int i = 0; i < root->rowCount(); i++) {
|
|
auto groupItem = static_cast<PreferencesPageItem*>(root->child(i));
|
|
if (groupItem->data(GroupNameRole).toString() == groupName) {
|
|
|
|
// Find the specific page
|
|
for (int j = 0; j < groupItem->rowCount(); j++) {
|
|
auto pageItem = static_cast<PreferencesPageItem*>(groupItem->child(j));
|
|
if (pageItem->data(PageNameRole).toString() == pageName) {
|
|
|
|
// Expand the group if needed
|
|
ui->groupsTreeView->expand(groupItem->index());
|
|
|
|
// Select the page
|
|
ui->groupsTreeView->selectionModel()->select(
|
|
pageItem->index(),
|
|
QItemSelectionModel::ClearAndSelect
|
|
);
|
|
|
|
// Navigate to the page
|
|
onPageSelected(pageItem->index());
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If no specific page found, just navigate to the group
|
|
ui->groupsTreeView->selectionModel()->select(
|
|
groupItem->index(),
|
|
QItemSelectionModel::ClearAndSelect
|
|
);
|
|
onPageSelected(groupItem->index());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::changeEvent(QEvent* e)
|
|
{
|
|
if (e->type() == QEvent::LanguageChange) {
|
|
ui->retranslateUi(this);
|
|
|
|
auto root = _model.invisibleRootItem();
|
|
for (int i = 0; i < root->rowCount(); i++) {
|
|
auto groupItem = static_cast<PreferencesPageItem*>(root->child(i));
|
|
auto groupName = groupItem->data(GroupNameRole).toString();
|
|
|
|
groupItem->setText(QObject::tr(groupName.toLatin1()));
|
|
|
|
for (int j = 0; j < groupItem->rowCount(); j++) {
|
|
auto pageModelItem = static_cast<PreferencesPageItem*>(groupItem->child(j));
|
|
auto pageModelWidget = static_cast<PreferencePage*>(pageModelItem->getWidget());
|
|
|
|
pageModelItem->setText(pageModelWidget->windowTitle());
|
|
}
|
|
}
|
|
|
|
expandToMinimumDialogWidth();
|
|
updatePageDependentWidgets();
|
|
}
|
|
else {
|
|
QWidget::changeEvent(e);
|
|
}
|
|
}
|
|
|
|
void DlgPreferencesImp::reload()
|
|
{
|
|
for (int i = 0; i < ui->groupWidgetStack->count(); i++) {
|
|
auto pageStackWidget = static_cast<QStackedWidget*>(ui->groupWidgetStack->widget(i));
|
|
|
|
for (int j = 0; j < pageStackWidget->count(); j++) {
|
|
auto page = qobject_cast<PreferencePage*>(pageStackWidget->widget(j));
|
|
|
|
if (page) {
|
|
page->loadSettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
applyChanges();
|
|
}
|
|
|
|
void DlgPreferencesImp::restorePageDefaults(PreferencesPageItem* item)
|
|
{
|
|
if (item->hasChildren()) {
|
|
// If page has children iterate over them and restore each
|
|
for (int i = 0; i < item->rowCount(); i++) {
|
|
auto child = static_cast<PreferencesPageItem*>(item->child(i));
|
|
|
|
restorePageDefaults(child);
|
|
}
|
|
}
|
|
else {
|
|
auto* page = qobject_cast<PreferencePage*>(item->getWidget());
|
|
|
|
page->resetSettingsToDefaults();
|
|
/**
|
|
* Let's save the restart request before the page object is deleted and replaced with
|
|
* the newPage object (which has restartRequired initialized to false)
|
|
*/
|
|
restartRequired = restartRequired || page->isRestartRequired();
|
|
|
|
std::string pageName = page->property(PageNameProperty).toString().toStdString();
|
|
std::string groupName = page->property(GroupNameProperty).toString().toStdString();
|
|
|
|
auto newPage = createPreferencePage(pageName, groupName);
|
|
|
|
newPage->loadSettings();
|
|
|
|
auto groupPageStack = qobject_cast<QStackedWidget*>(page->parentWidget());
|
|
auto replacedWidgetIndex = groupPageStack->indexOf(page);
|
|
auto currentWidgetIndex = groupPageStack->currentIndex();
|
|
|
|
groupPageStack->removeWidget(page);
|
|
groupPageStack->insertWidget(replacedWidgetIndex, newPage);
|
|
|
|
item->setWidget(newPage);
|
|
|
|
if (replacedWidgetIndex == currentWidgetIndex) {
|
|
groupPageStack->setCurrentIndex(currentWidgetIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
PreferencesPageItem* DlgPreferencesImp::getCurrentPage() const
|
|
{
|
|
auto groupPagesStack = qobject_cast<QStackedWidget*>(ui->groupWidgetStack->currentWidget());
|
|
|
|
if (!groupPagesStack) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto pageWidget = qobject_cast<PreferencePage*>(groupPagesStack->currentWidget());
|
|
|
|
if (!pageWidget) {
|
|
return nullptr;
|
|
}
|
|
|
|
return pageWidget->property(PreferencesPageItem::PropertyName).value<PreferencesPageItem*>();
|
|
}
|
|
|
|
// PreferencesSearchController implementation
|
|
PreferencesSearchController::PreferencesSearchController(DlgPreferencesImp* parentDialog, QObject* parent)
|
|
: QObject(parent)
|
|
, m_parentDialog(parentDialog)
|
|
, m_preferencesModel(nullptr)
|
|
, m_groupNameRole(0)
|
|
, m_pageNameRole(0)
|
|
, m_searchBox(nullptr)
|
|
, m_searchResultsList(nullptr)
|
|
{
|
|
// Get reference to search box from parent dialog's UI
|
|
m_searchBox = m_parentDialog->ui->searchBox;
|
|
|
|
// Create the search results popup list
|
|
m_searchResultsList = new QListWidget(m_parentDialog);
|
|
m_searchResultsList->setWindowFlags(
|
|
Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint
|
|
);
|
|
m_searchResultsList->setVisible(false);
|
|
m_searchResultsList->setMinimumWidth(300);
|
|
m_searchResultsList->setMaximumHeight(400); // Increased max height
|
|
m_searchResultsList->setFrameStyle(QFrame::Box | QFrame::Raised);
|
|
m_searchResultsList->setLineWidth(1);
|
|
m_searchResultsList->setFocusPolicy(Qt::NoFocus); // Don't steal focus from search box
|
|
m_searchResultsList->setAttribute(Qt::WA_ShowWithoutActivating); // Show without
|
|
// activating/stealing focus
|
|
m_searchResultsList->setWordWrap(true); // Enable word wrapping
|
|
m_searchResultsList->setTextElideMode(Qt::ElideNone); // Don't elide text, let it wrap instead
|
|
m_searchResultsList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Disable horizontal
|
|
// scrollbar
|
|
m_searchResultsList->setSpacing(0); // Remove spacing between items
|
|
m_searchResultsList->setContentsMargins(0, 0, 0, 0); // Remove margins
|
|
|
|
// Set custom delegate for mixed font rendering (bold first line, normal second line)
|
|
m_searchResultsList->setItemDelegate(new MixedFontDelegate(m_searchResultsList));
|
|
|
|
// Connect search results list signals
|
|
connect(
|
|
m_searchResultsList,
|
|
&QListWidget::itemSelectionChanged,
|
|
this,
|
|
&PreferencesSearchController::onSearchResultSelected
|
|
);
|
|
connect(
|
|
m_searchResultsList,
|
|
&QListWidget::itemDoubleClicked,
|
|
this,
|
|
&PreferencesSearchController::onSearchResultDoubleClicked
|
|
);
|
|
connect(
|
|
m_searchResultsList,
|
|
&QListWidget::itemClicked,
|
|
this,
|
|
&PreferencesSearchController::onSearchResultClicked
|
|
);
|
|
|
|
// Install event filter for keyboard navigation in search results
|
|
m_searchResultsList->installEventFilter(m_parentDialog);
|
|
}
|
|
|
|
void PreferencesSearchController::setPreferencesModel(QStandardItemModel* model)
|
|
{
|
|
m_preferencesModel = model;
|
|
}
|
|
|
|
void PreferencesSearchController::setGroupNameRole(int role)
|
|
{
|
|
m_groupNameRole = role;
|
|
}
|
|
|
|
void PreferencesSearchController::setPageNameRole(int role)
|
|
{
|
|
m_pageNameRole = role;
|
|
}
|
|
|
|
QListWidget* PreferencesSearchController::getSearchResultsList() const
|
|
{
|
|
return m_searchResultsList;
|
|
}
|
|
|
|
bool PreferencesSearchController::isPopupVisible() const
|
|
{
|
|
return m_searchResultsList && m_searchResultsList->isVisible();
|
|
}
|
|
|
|
bool PreferencesSearchController::isPopupUnderMouse() const
|
|
{
|
|
return m_searchResultsList && m_searchResultsList->underMouse();
|
|
}
|
|
|
|
bool PreferencesSearchController::isPopupAncestorOf(QWidget* widget) const
|
|
{
|
|
return m_searchResultsList && m_searchResultsList->isAncestorOf(widget);
|
|
}
|
|
|
|
void PreferencesSearchController::onSearchTextChanged(const QString& text)
|
|
{
|
|
if (text.isEmpty()) {
|
|
clearHighlights();
|
|
m_searchResults.clear();
|
|
m_lastSearchText.clear();
|
|
hideSearchResultsList();
|
|
return;
|
|
}
|
|
|
|
// Only perform new search if text changed
|
|
if (text != m_lastSearchText) {
|
|
performSearch(text);
|
|
m_lastSearchText = text;
|
|
}
|
|
}
|
|
|
|
void PreferencesSearchController::performSearch(const QString& searchText)
|
|
{
|
|
clearHighlights();
|
|
m_searchResults.clear();
|
|
|
|
if (searchText.length() < 2) {
|
|
hideSearchResultsList();
|
|
return;
|
|
}
|
|
|
|
// Search through all groups and pages to collect ALL results
|
|
auto root = m_preferencesModel->invisibleRootItem();
|
|
for (int i = 0; i < root->rowCount(); i++) {
|
|
auto groupItem = static_cast<PreferencesPageItem*>(root->child(i));
|
|
auto groupName = groupItem->data(m_groupNameRole).toString();
|
|
auto groupStack = qobject_cast<QStackedWidget*>(groupItem->getWidget());
|
|
|
|
if (!groupStack) {
|
|
continue;
|
|
}
|
|
|
|
// Search in each page of the group
|
|
for (int j = 0; j < groupItem->rowCount(); j++) {
|
|
auto pageItem = static_cast<PreferencesPageItem*>(groupItem->child(j));
|
|
auto pageName = pageItem->data(m_pageNameRole).toString();
|
|
auto pageWidget = qobject_cast<PreferencePage*>(pageItem->getWidget());
|
|
|
|
if (!pageWidget) {
|
|
continue;
|
|
}
|
|
|
|
// Collect all matching widgets in this page
|
|
collectSearchResults(
|
|
pageWidget,
|
|
searchText,
|
|
groupName,
|
|
pageName,
|
|
pageItem->text(),
|
|
groupItem->text()
|
|
);
|
|
}
|
|
}
|
|
|
|
// Sort results by score (highest first)
|
|
std::sort(
|
|
m_searchResults.begin(),
|
|
m_searchResults.end(),
|
|
[](const SearchResult& a, const SearchResult& b) { return a.score > b.score; }
|
|
);
|
|
|
|
// Update UI with search results
|
|
if (!m_searchResults.isEmpty()) {
|
|
populateSearchResultsList();
|
|
showSearchResultsList();
|
|
}
|
|
else {
|
|
hideSearchResultsList();
|
|
}
|
|
}
|
|
|
|
void PreferencesSearchController::clearHighlights()
|
|
{
|
|
// Restore original styles for all highlighted widgets
|
|
for (int i = 0; i < m_highlightedWidgets.size(); ++i) {
|
|
QWidget* widget = m_highlightedWidgets.at(i);
|
|
if (widget && m_originalStyles.contains(widget)) {
|
|
widget->setStyleSheet(m_originalStyles[widget]);
|
|
}
|
|
}
|
|
m_highlightedWidgets.clear();
|
|
m_originalStyles.clear();
|
|
}
|
|
|
|
void PreferencesSearchController::collectSearchResults(
|
|
QWidget* widget,
|
|
const QString& searchText,
|
|
const QString& groupName,
|
|
const QString& pageName,
|
|
const QString& pageDisplayName,
|
|
const QString& tabName
|
|
)
|
|
{
|
|
if (!widget) {
|
|
return;
|
|
}
|
|
|
|
const QString lowerSearchText = searchText.toLower();
|
|
|
|
// First, check if the page display name itself matches (highest priority)
|
|
int pageScore = 0;
|
|
if (fuzzyMatch(searchText, pageDisplayName, pageScore)) {
|
|
SearchResult result {
|
|
.groupName = groupName,
|
|
.pageName = pageName,
|
|
.widget = widget, // Use the page widget itself
|
|
.matchText = pageDisplayName, // Use display name, not internal name
|
|
.groupBoxName = QString(), // No groupbox for page-level match
|
|
.tabName = tabName,
|
|
.pageDisplayName = pageDisplayName,
|
|
.isPageLevelMatch = true, // Mark as page-level match
|
|
.score = pageScore + 2000 // Boost page-level matches
|
|
};
|
|
m_searchResults.append(result);
|
|
// Continue searching for individual items even if page matches
|
|
}
|
|
|
|
// Search different widget types using the template method
|
|
searchWidgetType<QLabel>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QCheckBox>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QRadioButton>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QPushButton>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QGroupBox>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QComboBox>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QSpinBox>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
searchWidgetType<QDoubleSpinBox>(widget, searchText, groupName, pageName, pageDisplayName, tabName);
|
|
}
|
|
|
|
void PreferencesSearchController::onSearchResultSelected()
|
|
{
|
|
// This method is called when a search result is selected (arrow keys or single click)
|
|
// Navigate immediately but keep popup open
|
|
if (m_searchResultsList && m_searchResultsList->currentItem()) {
|
|
navigateToCurrentSearchResult(PopupAction::KeepOpen);
|
|
}
|
|
|
|
ensureSearchBoxFocus();
|
|
}
|
|
|
|
void PreferencesSearchController::onSearchResultClicked()
|
|
{
|
|
// Handle single click - navigate immediately but keep popup open
|
|
if (m_searchResultsList && m_searchResultsList->currentItem()) {
|
|
navigateToCurrentSearchResult(PopupAction::KeepOpen);
|
|
}
|
|
|
|
ensureSearchBoxFocus();
|
|
}
|
|
|
|
void PreferencesSearchController::onSearchResultDoubleClicked()
|
|
{
|
|
// Handle double click - navigate and close popup
|
|
if (m_searchResultsList && m_searchResultsList->currentItem()) {
|
|
navigateToCurrentSearchResult(PopupAction::CloseAfter);
|
|
}
|
|
}
|
|
|
|
void PreferencesSearchController::navigateToCurrentSearchResult(PopupAction action)
|
|
{
|
|
QListWidgetItem* currentItem = m_searchResultsList->currentItem();
|
|
|
|
// Skip if it's a separator (non-selectable item) or no item selected
|
|
if (!currentItem || !(currentItem->flags() & Qt::ItemIsSelectable)) {
|
|
return;
|
|
}
|
|
|
|
// Get the result index directly from the item data
|
|
bool ok;
|
|
int resultIndex = currentItem->data(Qt::UserRole).toInt(&ok);
|
|
|
|
if (ok && resultIndex >= 0 && resultIndex < m_searchResults.size()) {
|
|
const SearchResult& result = m_searchResults.at(resultIndex);
|
|
|
|
// Emit signal to request navigation
|
|
Q_EMIT navigationRequested(result.groupName, result.pageName);
|
|
|
|
// Clear any existing highlights
|
|
clearHighlights();
|
|
|
|
// Only highlight specific widgets for non-page-level matches
|
|
if (!result.isPageLevelMatch && !result.widget.isNull()) {
|
|
applyHighlightToWidget(result.widget);
|
|
}
|
|
// For page-level matches, we just navigate without highlighting anything
|
|
|
|
// Close popup only if requested (double-click or Enter)
|
|
if (action == PopupAction::CloseAfter) {
|
|
hideSearchResultsList();
|
|
}
|
|
}
|
|
}
|
|
|
|
void PreferencesSearchController::populateSearchResultsList()
|
|
{
|
|
m_searchResultsList->clear();
|
|
|
|
for (int i = 0; i < m_searchResults.size(); ++i) {
|
|
const SearchResult& result = m_searchResults.at(i);
|
|
|
|
// Create item without setting DisplayRole
|
|
QListWidgetItem* item = new QListWidgetItem();
|
|
|
|
// Store path and widget text in separate roles
|
|
if (result.isPageLevelMatch) {
|
|
// For page matches: parent group as header, page name as content
|
|
item->setData(PathRole, result.tabName);
|
|
item->setData(WidgetTextRole, result.pageDisplayName);
|
|
}
|
|
else {
|
|
// For widget matches: full path as header, widget text as content
|
|
QString pathText = result.tabName + QStringLiteral("/") + result.pageDisplayName;
|
|
item->setData(PathRole, pathText);
|
|
item->setData(WidgetTextRole, result.matchText);
|
|
}
|
|
item->setData(Qt::UserRole, i); // Keep existing index storage
|
|
|
|
m_searchResultsList->addItem(item);
|
|
}
|
|
|
|
// Select first actual item (not separator)
|
|
if (!m_searchResults.isEmpty()) {
|
|
m_searchResultsList->setCurrentRow(0);
|
|
}
|
|
}
|
|
|
|
void PreferencesSearchController::hideSearchResultsList()
|
|
{
|
|
m_searchResultsList->setVisible(false);
|
|
}
|
|
|
|
void PreferencesSearchController::showSearchResultsList()
|
|
{
|
|
// Configure popup size and position
|
|
configurePopupSize();
|
|
|
|
// Show the popup
|
|
m_searchResultsList->setVisible(true);
|
|
m_searchResultsList->raise();
|
|
|
|
// Use QTimer to ensure focus returns to search box after Qt finishes processing the popup show event
|
|
QTimer::singleShot(0, this, [this]() {
|
|
if (m_searchBox) {
|
|
m_searchBox->setFocus();
|
|
m_searchBox->activateWindow();
|
|
}
|
|
});
|
|
}
|
|
|
|
QString PreferencesSearchController::findGroupBoxForWidget(QWidget* widget)
|
|
{
|
|
if (!widget) {
|
|
return QString();
|
|
}
|
|
|
|
// Walk up the parent hierarchy to find a QGroupBox
|
|
QWidget* parent = widget->parentWidget();
|
|
while (parent) {
|
|
QGroupBox* groupBox = qobject_cast<QGroupBox*>(parent);
|
|
if (groupBox) {
|
|
return groupBox->title();
|
|
}
|
|
parent = parent->parentWidget();
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
|
|
template<typename WidgetType>
|
|
void PreferencesSearchController::searchWidgetType(
|
|
QWidget* parentWidget,
|
|
const QString& searchText,
|
|
const QString& groupName,
|
|
const QString& pageName,
|
|
const QString& pageDisplayName,
|
|
const QString& tabName
|
|
)
|
|
{
|
|
const QList<WidgetType*> widgets = parentWidget->findChildren<WidgetType*>();
|
|
|
|
for (WidgetType* widget : widgets) {
|
|
QString widgetText;
|
|
|
|
// Get text based on widget type
|
|
if constexpr (std::is_same_v<WidgetType, QLabel>) {
|
|
widgetText = widget->text();
|
|
}
|
|
else if constexpr (std::is_same_v<WidgetType, QCheckBox>) {
|
|
widgetText = widget->text();
|
|
}
|
|
else if constexpr (std::is_same_v<WidgetType, QGroupBox>) {
|
|
widgetText = widget->title();
|
|
}
|
|
else if constexpr (std::is_same_v<WidgetType, QRadioButton>) {
|
|
widgetText = widget->text();
|
|
}
|
|
else if constexpr (std::is_same_v<WidgetType, QPushButton>) {
|
|
widgetText = widget->text();
|
|
}
|
|
|
|
if (!widgetText.isEmpty()) {
|
|
int score = 0;
|
|
if (fuzzyMatch(searchText, widgetText, score)) {
|
|
SearchResult result {
|
|
.groupName = groupName,
|
|
.pageName = pageName,
|
|
.widget = widget,
|
|
.matchText = widgetText,
|
|
.groupBoxName = findGroupBoxForWidget(widget),
|
|
.tabName = tabName,
|
|
.pageDisplayName = pageDisplayName,
|
|
.isPageLevelMatch = false,
|
|
.score = score
|
|
};
|
|
m_searchResults.append(result);
|
|
}
|
|
}
|
|
|
|
// search tooltip text for all widget types
|
|
QString tooltip = widget->toolTip();
|
|
if (!tooltip.isEmpty()) {
|
|
int tooltipScore = 0;
|
|
if (fuzzyMatch(searchText, tooltip, tooltipScore)) {
|
|
SearchResult result {
|
|
.groupName = groupName,
|
|
.pageName = pageName,
|
|
.widget = widget,
|
|
.matchText = QStringLiteral("Tooltip: ") + tooltip,
|
|
.groupBoxName = findGroupBoxForWidget(widget),
|
|
.tabName = tabName,
|
|
.pageDisplayName = pageDisplayName,
|
|
.isPageLevelMatch = false,
|
|
.score = tooltipScore - 100 // lower score for tooltip match
|
|
};
|
|
m_searchResults.append(result);
|
|
}
|
|
}
|
|
|
|
// search throughout combobox items
|
|
if constexpr (std::is_same_v<WidgetType, QComboBox>) {
|
|
for (int i = 0; i < widget->count(); ++i) {
|
|
QString itemText = widget->itemText(i);
|
|
if (!itemText.isEmpty()) {
|
|
int itemScore = 0;
|
|
if (fuzzyMatch(searchText, itemText, itemScore)) {
|
|
SearchResult result {
|
|
.groupName = groupName,
|
|
.pageName = pageName,
|
|
.widget = widget,
|
|
.matchText = QStringLiteral("Option: ") + itemText,
|
|
.groupBoxName = findGroupBoxForWidget(widget),
|
|
.tabName = tabName,
|
|
.pageDisplayName = pageDisplayName,
|
|
.isPageLevelMatch = false,
|
|
.score = itemScore + 50 // boost score for combo item
|
|
};
|
|
m_searchResults.append(result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int PreferencesSearchController::calculatePopupHeight(int popupWidth)
|
|
{
|
|
int totalHeight = 0;
|
|
int itemCount = m_searchResultsList->count();
|
|
int visibleItemCount = 0;
|
|
const int maxVisibleItems = 4;
|
|
|
|
for (int i = 0; i < itemCount && visibleItemCount < maxVisibleItems; ++i) {
|
|
QListWidgetItem* item = m_searchResultsList->item(i);
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
|
|
// For separator items, use their widget height
|
|
if (m_searchResultsList->itemWidget(item)) {
|
|
totalHeight += m_searchResultsList->itemWidget(item)->sizeHint().height();
|
|
}
|
|
else {
|
|
// For text items, use the delegate's size hint instead of calculating manually
|
|
QStyleOptionViewItem option;
|
|
option.rect = QRect(0, 0, popupWidth, 100); // Temporary rect for calculation
|
|
option.font = m_searchResultsList->font();
|
|
|
|
QSize delegateSize = m_searchResultsList->itemDelegate()->sizeHint(
|
|
option,
|
|
m_searchResultsList->model()->index(i, 0)
|
|
);
|
|
totalHeight += delegateSize.height();
|
|
|
|
visibleItemCount++; // Only count actual items, not separators
|
|
}
|
|
}
|
|
|
|
return qMax(50, totalHeight); // Minimum 50px height
|
|
}
|
|
|
|
void PreferencesSearchController::configurePopupSize()
|
|
{
|
|
if (m_searchResults.isEmpty()) {
|
|
hideSearchResultsList();
|
|
return;
|
|
}
|
|
|
|
// Set a fixed width to prevent flashing when content changes
|
|
int popupWidth = 300; // Fixed width for consistent appearance
|
|
m_searchResultsList->setFixedWidth(popupWidth);
|
|
|
|
// Calculate and set the height
|
|
int finalHeight = calculatePopupHeight(popupWidth);
|
|
m_searchResultsList->setFixedHeight(finalHeight);
|
|
|
|
// Position the popup's upper-left corner at the upper-right corner of the search box
|
|
QPoint globalPos = m_searchBox->mapToGlobal(QPoint(m_searchBox->width(), 0));
|
|
|
|
// Check if popup would go off-screen to the right
|
|
QScreen* screen = QApplication::screenAt(globalPos);
|
|
if (!screen) {
|
|
screen = QApplication::primaryScreen();
|
|
}
|
|
QRect screenGeometry = screen->availableGeometry();
|
|
|
|
// If popup would extend beyond right edge of screen, position it below the search box instead
|
|
if (globalPos.x() + popupWidth > screenGeometry.right()) {
|
|
globalPos = m_searchBox->mapToGlobal(QPoint(0, m_searchBox->height()));
|
|
}
|
|
|
|
m_searchResultsList->move(globalPos);
|
|
}
|
|
|
|
// Fuzzy search implementation
|
|
|
|
bool PreferencesSearchController::isExactMatch(const QString& searchText, const QString& targetText)
|
|
{
|
|
return targetText.toLower().contains(searchText.toLower());
|
|
}
|
|
|
|
bool PreferencesSearchController::fuzzyMatch(
|
|
const QString& searchText,
|
|
const QString& targetText,
|
|
int& score
|
|
)
|
|
{
|
|
if (searchText.isEmpty()) {
|
|
score = 0;
|
|
return true;
|
|
}
|
|
|
|
const QString lowerSearch = searchText.toLower();
|
|
const QString lowerTarget = targetText.toLower();
|
|
|
|
// First check for exact substring match (highest score)
|
|
if (lowerTarget.contains(lowerSearch)) {
|
|
// Score based on how early the match appears and how much of the string it covers
|
|
int matchIndex = lowerTarget.indexOf(lowerSearch);
|
|
int coverage = (lowerSearch.length() * 100) / lowerTarget.length(); // Percentage coverage
|
|
score = 1000 - matchIndex + coverage; // Higher score for earlier matches and better coverage
|
|
return true;
|
|
}
|
|
|
|
// For fuzzy matching, require minimum search length to avoid too many false positives
|
|
if (lowerSearch.length() < 3) {
|
|
score = 0;
|
|
return false;
|
|
}
|
|
|
|
// Fuzzy matching: check if all characters appear in order
|
|
int searchIndex = 0;
|
|
int targetIndex = 0;
|
|
int consecutiveMatches = 0;
|
|
int maxConsecutive = 0;
|
|
int firstMatchIndex = -1;
|
|
int lastMatchIndex = -1;
|
|
|
|
while (searchIndex < lowerSearch.length() && targetIndex < lowerTarget.length()) {
|
|
if (lowerSearch[searchIndex] == lowerTarget[targetIndex]) {
|
|
if (firstMatchIndex == -1) {
|
|
firstMatchIndex = targetIndex;
|
|
}
|
|
lastMatchIndex = targetIndex;
|
|
searchIndex++;
|
|
consecutiveMatches++;
|
|
maxConsecutive = qMax(maxConsecutive, consecutiveMatches);
|
|
}
|
|
else {
|
|
consecutiveMatches = 0;
|
|
}
|
|
targetIndex++;
|
|
}
|
|
|
|
// Check if all search characters were found
|
|
if (searchIndex == lowerSearch.length()) {
|
|
// Calculate match density - how spread out are the matches?
|
|
int matchSpan = lastMatchIndex - firstMatchIndex + 1;
|
|
int density = (lowerSearch.length() * 100) / matchSpan; // Characters per span
|
|
|
|
// Require minimum density - matches shouldn't be too spread out
|
|
if (density < 20) { // Less than 20% density is too sparse
|
|
score = 0;
|
|
return false;
|
|
}
|
|
|
|
// Require minimum coverage of search term
|
|
int coverage = (lowerSearch.length() * 100) / lowerTarget.length();
|
|
if (coverage < 15 && lowerTarget.length() > 20) { // For long strings, require better coverage
|
|
score = 0;
|
|
return false;
|
|
}
|
|
|
|
// Calculate score based on:
|
|
// - Match density (how compact the matches are)
|
|
// - Consecutive matches bonus
|
|
// - Coverage (how much of target string is the search term)
|
|
// - Position bonus (earlier matches are better)
|
|
int densityScore = qMin(density, 100); // Cap at 100
|
|
int consecutiveBonus = (maxConsecutive * 30) / lowerSearch.length();
|
|
int coverageScore = qMin(coverage * 2, 100); // Coverage is important
|
|
int positionBonus = qMax(0, 50 - firstMatchIndex); // Earlier is better
|
|
|
|
score = densityScore + consecutiveBonus + coverageScore + positionBonus;
|
|
|
|
// Minimum score threshold for fuzzy matches
|
|
if (score < 80) {
|
|
score = 0;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
score = 0;
|
|
return false;
|
|
}
|
|
|
|
void PreferencesSearchController::ensureSearchBoxFocus()
|
|
{
|
|
if (m_searchBox && !m_searchBox->hasFocus()) {
|
|
m_searchBox->setFocus();
|
|
}
|
|
}
|
|
|
|
QString PreferencesSearchController::getHighlightStyleForWidget(QWidget* widget)
|
|
{
|
|
const QString baseStyle = QStringLiteral(
|
|
"background-color: #E3F2FD; color: #1565C0; border: 2px solid #2196F3; border-radius: 3px;"
|
|
);
|
|
|
|
if (qobject_cast<QLabel*>(widget)) {
|
|
return QStringLiteral("QLabel { ") + baseStyle + QStringLiteral(" padding: 2px; }");
|
|
}
|
|
else if (qobject_cast<QCheckBox*>(widget)) {
|
|
return QStringLiteral("QCheckBox { ") + baseStyle + QStringLiteral(" padding: 2px; }");
|
|
}
|
|
else if (qobject_cast<QRadioButton*>(widget)) {
|
|
return QStringLiteral("QRadioButton { ") + baseStyle + QStringLiteral(" padding: 2px; }");
|
|
}
|
|
else if (qobject_cast<QGroupBox*>(widget)) {
|
|
return QStringLiteral("QGroupBox::title { ") + baseStyle + QStringLiteral(" padding: 2px; }");
|
|
}
|
|
else if (qobject_cast<QPushButton*>(widget)) {
|
|
return QStringLiteral("QPushButton { ") + baseStyle + QStringLiteral(" }");
|
|
}
|
|
else {
|
|
return QStringLiteral("QWidget { ") + baseStyle + QStringLiteral(" padding: 2px; }");
|
|
}
|
|
}
|
|
|
|
void PreferencesSearchController::applyHighlightToWidget(QWidget* widget)
|
|
{
|
|
if (!widget) {
|
|
return;
|
|
}
|
|
|
|
m_originalStyles[widget] = widget->styleSheet();
|
|
widget->setStyleSheet(getHighlightStyleForWidget(widget));
|
|
m_highlightedWidgets.append(widget);
|
|
}
|
|
|
|
bool PreferencesSearchController::handleSearchBoxKeyPress(QKeyEvent* keyEvent)
|
|
{
|
|
if (!m_searchResultsList->isVisible() || m_searchResults.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
switch (keyEvent->key()) {
|
|
case Qt::Key_Down: {
|
|
// Move selection down in popup, skipping separators
|
|
int currentRow = m_searchResultsList->currentRow();
|
|
int totalItems = m_searchResultsList->count();
|
|
for (int i = 1; i < totalItems; ++i) {
|
|
int nextRow = (currentRow + i) % totalItems;
|
|
QListWidgetItem* item = m_searchResultsList->item(nextRow);
|
|
if (item && (item->flags() & Qt::ItemIsSelectable)) {
|
|
m_searchResultsList->setCurrentRow(nextRow);
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case Qt::Key_Up: {
|
|
// Move selection up in popup, skipping separators
|
|
int currentRow = m_searchResultsList->currentRow();
|
|
int totalItems = m_searchResultsList->count();
|
|
for (int i = 1; i < totalItems; ++i) {
|
|
int prevRow = (currentRow - i + totalItems) % totalItems;
|
|
QListWidgetItem* item = m_searchResultsList->item(prevRow);
|
|
if (item && (item->flags() & Qt::ItemIsSelectable)) {
|
|
m_searchResultsList->setCurrentRow(prevRow);
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
navigateToCurrentSearchResult(PopupAction::CloseAfter);
|
|
return true;
|
|
case Qt::Key_Escape:
|
|
hideSearchResultsList();
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool PreferencesSearchController::handlePopupKeyPress(QKeyEvent* keyEvent)
|
|
{
|
|
switch (keyEvent->key()) {
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
navigateToCurrentSearchResult(PopupAction::CloseAfter);
|
|
return true;
|
|
case Qt::Key_Escape:
|
|
hideSearchResultsList();
|
|
ensureSearchBoxFocus();
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool PreferencesSearchController::isClickOutsidePopup(QMouseEvent* mouseEvent)
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
QPoint globalPos = mouseEvent->globalPos();
|
|
#else
|
|
QPoint globalPos = mouseEvent->globalPosition().toPoint();
|
|
#endif
|
|
QRect searchBoxRect = QRect(m_searchBox->mapToGlobal(QPoint(0, 0)), m_searchBox->size());
|
|
QRect popupRect
|
|
= QRect(m_searchResultsList->mapToGlobal(QPoint(0, 0)), m_searchResultsList->size());
|
|
|
|
return !searchBoxRect.contains(globalPos) && !popupRect.contains(globalPos);
|
|
}
|
|
|
|
bool DlgPreferencesImp::eventFilter(QObject* obj, QEvent* event)
|
|
{
|
|
// Handle search box key presses
|
|
if (obj == ui->searchBox && event->type() == QEvent::KeyPress) {
|
|
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
|
return m_searchController->handleSearchBoxKeyPress(keyEvent);
|
|
}
|
|
|
|
// Handle popup key presses
|
|
if (obj == m_searchController->getSearchResultsList() && event->type() == QEvent::KeyPress) {
|
|
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
|
return m_searchController->handlePopupKeyPress(keyEvent);
|
|
}
|
|
|
|
// Prevent popup from stealing focus
|
|
if (obj == m_searchController->getSearchResultsList() && event->type() == QEvent::FocusIn) {
|
|
m_searchController->ensureSearchBoxFocus();
|
|
return true;
|
|
}
|
|
|
|
// Handle search box focus loss
|
|
if (obj == ui->searchBox && event->type() == QEvent::FocusOut) {
|
|
QFocusEvent* focusEvent = static_cast<QFocusEvent*>(event);
|
|
if (focusEvent->reason() != Qt::PopupFocusReason
|
|
&& focusEvent->reason() != Qt::MouseFocusReason) {
|
|
// Only hide if focus is going somewhere else, not due to popup interaction
|
|
QTimer::singleShot(100, this, [this]() {
|
|
if (!ui->searchBox->hasFocus() && !m_searchController->isPopupUnderMouse()) {
|
|
m_searchController->hideSearchResultsList();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle clicks outside popup
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
|
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
QWidget* widget = qobject_cast<QWidget*>(obj);
|
|
|
|
// Check if click is outside search area
|
|
if (m_searchController->isPopupVisible() && obj != m_searchController->getSearchResultsList()
|
|
&& obj != ui->searchBox && widget && // Only check if obj is actually a QWidget
|
|
!m_searchController->isPopupAncestorOf(widget) && !ui->searchBox->isAncestorOf(widget)) {
|
|
|
|
if (m_searchController->isClickOutsidePopup(mouseEvent)) {
|
|
m_searchController->hideSearchResultsList();
|
|
m_searchController->clearHighlights();
|
|
}
|
|
}
|
|
}
|
|
|
|
return QDialog::eventFilter(obj, event);
|
|
}
|
|
|
|
#include "moc_DlgPreferencesImp.cpp"
|