diff --git a/src/ApplicationSettings.cpp b/src/ApplicationSettings.cpp index 17def0ca6..0fcdfe222 100644 --- a/src/ApplicationSettings.cpp +++ b/src/ApplicationSettings.cpp @@ -18,6 +18,7 @@ #include "ApplicationSettings.h" +#include #include #include @@ -36,8 +37,45 @@ ApplicationSetting name{#group "/" #name, default};\ ApplicationSettings::ApplicationSettings(QObject *parent) - : QSettings{parent} + : QSettings(parent) +{ } + +ApplicationSettings::ApplicationSettings(QTemporaryFile *tempFile, QObject *parent) + : QSettings(tempFile->fileName(), QSettings::NativeFormat, parent) +{ + tempFile->setParent(this); +} + +void ApplicationSettings::fillFrom(ApplicationSettings *other) +{ + if (!other) return; + + const auto meta = metaObject(); + + for (auto i = meta->propertyOffset(); i < meta->propertyCount(); ++i) + { + const auto property = meta->property(i); + + if (property.isWritable() && property.isReadable()) + property.write(this, property.read(other)); + } +} + +bool ApplicationSettings::isEquals(ApplicationSettings *other) const { + if (!other) return false; + + const auto meta = metaObject(); + + for (auto i = meta->propertyOffset(); i < meta->propertyCount(); ++i) + { + const auto property = meta->property(i); + + if (property.read(this) != property.read(other)) + return false; + } + + return true; } CREATE_SETTING(Gui, ShowMenuBar, showMenuBar, bool, true) diff --git a/src/ApplicationSettings.h b/src/ApplicationSettings.h index cd4089801..7dfe75303 100644 --- a/src/ApplicationSettings.h +++ b/src/ApplicationSettings.h @@ -26,6 +26,7 @@ #include #include +class QTemporaryFile; template class ApplicationSetting @@ -53,39 +54,54 @@ class ApplicationSetting }; -#define DEFINE_SETTING(name, lname, type)\ -public:\ - type lname() const;\ -public slots:\ - void set##name(type lname);\ -Q_SIGNAL\ - void lname##Changed(type lname);\ - +#define DEFINE_SETTING(name, lname, type) \ + Q_PROPERTY(type lname READ lname WRITE set##name NOTIFY lname##Changed FINAL) \ + public: \ + type lname() const; \ + public slots: \ + void set##name(type lname); \ + Q_SIGNAL \ + void lname##Changed(type lname); class ApplicationSettings : public QSettings { Q_OBJECT - public: - explicit ApplicationSettings(QObject *parent = nullptr); - - enum DefaultDirectoryBehaviorEnum { + enum DefaultDirectoryBehaviorEnum + { FollowCurrentDocument, RememberLastUsed, HardCoded }; Q_ENUM(DefaultDirectoryBehaviorEnum) +public: + explicit ApplicationSettings(QObject *parent = nullptr); + + /** + * @brief Create temporary instance with default settings. + * @param tempFile Temporary file instance to be used. + * @warning Ensure tempFile->open() was called before passing it. + * @note The instance takes ownership of tempFile. + */ + ApplicationSettings(QTemporaryFile *tempFile, QObject *parent = nullptr); + + inline void copyTo(ApplicationSettings *other) const { + other->fillFrom(const_cast(this)); + } + void fillFrom(ApplicationSettings *other); + bool isEquals(ApplicationSettings *other) const; + template - T get(const char *key, const T &defaultValue) const + inline T get(const char *key, const T &defaultValue) const { return value(QLatin1String(key), defaultValue).template value(); } template - T get(const ApplicationSetting &setting) const + inline T get(const ApplicationSetting &setting) const { return get(setting.key(), setting.getDefault()); } template - void set(const ApplicationSetting &setting, const T &value) + inline void set(const ApplicationSetting &setting, const T &value) { setValue(QLatin1String(setting.key()), value); } DEFINE_SETTING(ShowMenuBar, showMenuBar, bool) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c465a6aaa..23d8d7985 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -125,6 +125,17 @@ qt_add_executable(NotepadNext decorators/SurroundSelection.h decorators/URLFinder.cpp decorators/URLFinder.h + dialogs/Preferences/PreferencesCategoryListModel.cpp + dialogs/Preferences/PreferencesCategoryListModel.h + dialogs/Preferences/BehaviorCategoryItem.cpp + dialogs/Preferences/BehaviorCategoryItem.h + dialogs/Preferences/PreferencesViewUtils.h + dialogs/Preferences/PreferencesCategoryItem.h + dialogs/Preferences/PreferencesCategoryItemT.h + dialogs/Preferences/AppearanceCategoryItem.cpp + dialogs/Preferences/AppearanceCategoryItem.h + dialogs/PreferencesDialog.cpp + dialogs/PreferencesDialog.h dialogs/ColumnEditorDialog.cpp dialogs/ColumnEditorDialog.h dialogs/ColumnEditorDialog.ui @@ -143,9 +154,9 @@ qt_add_executable(NotepadNext dialogs/MainWindow.cpp dialogs/MainWindow.h dialogs/MainWindow.ui - dialogs/PreferencesDialog.cpp - dialogs/PreferencesDialog.h - dialogs/PreferencesDialog.ui + + + docks/DebugLogDock.cpp docks/DebugLogDock.h docks/DebugLogDock.ui diff --git a/src/dialogs/MainWindow.cpp b/src/dialogs/MainWindow.cpp index 333a33771..a8bf8f3eb 100644 --- a/src/dialogs/MainWindow.cpp +++ b/src/dialogs/MainWindow.cpp @@ -719,12 +719,15 @@ MainWindow::MainWindow(NotepadNextApplication *app) : languageActionGroup->setExclusive(true); connect(ui->actionPreferences, &QAction::triggered, this, [=] { - PreferencesDialog *pd = findChild(QString(), Qt::FindDirectChildrenOnly); + auto pd = findChild(QString(), Qt::FindDirectChildrenOnly); if (pd == Q_NULLPTR) { pd = new PreferencesDialog(app->getSettings(), this); } + pd->resize(700, 400); + pd->setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, pd->size(), geometry())); + pd->show(); pd->raise(); pd->activateWindow(); diff --git a/src/dialogs/Preferences/AppearanceCategoryItem.cpp b/src/dialogs/Preferences/AppearanceCategoryItem.cpp new file mode 100644 index 000000000..e584a9f0f --- /dev/null +++ b/src/dialogs/Preferences/AppearanceCategoryItem.cpp @@ -0,0 +1,146 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#include +#include +#include +#include +#include +#include + +#include "AppearanceCategoryItem.h" +#include "PreferencesViewUtils.h" +#include "ApplicationSettings.h" + +using namespace Preferences; + +namespace +{ + inline QGroupBox *WindowAppearanceView(ApplicationSettings *settings) + { + const auto group = new QGroupBox(QObject::tr("Main window")); + + const auto layout = new QVBoxLayout(group); + layout->addWidget(CreateCheckBox( + QObject::tr("Show menu bar"), + settings, + PREFERENCES_BIND_PROPERTY(showMenuBar, ShowMenuBar) + )); + layout->addWidget(CreateCheckBox( + QObject::tr("Show toolbar"), + settings, + PREFERENCES_BIND_PROPERTY(showToolBar, ShowToolBar) + )); + layout->addWidget(CreateCheckBox( + QObject::tr("Show status bar"), + settings, + PREFERENCES_BIND_PROPERTY(showStatusBar, ShowStatusBar) + )); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(SPACING_6); + + return group; + } + + inline QGroupBox *FontAppearanceView(ApplicationSettings *settings) + { + const auto group = new QGroupBox(QObject::tr("Font")); + + const auto familyCombo = new QFontComboBox; + familyCombo->setMinimumWidth(120); + + const auto sizeCombo = new QComboBox; + sizeCombo->addItems({ + "4", "6", "8", "9", "10", + "11", "12", "14", "16", "18", + "20", "22", "24", "26", "28", + "36", "48", "72" + }); + sizeCombo->setValidator(new QIntValidator(4, 200, sizeCombo)); + sizeCombo->setEditable(true); + + const auto wheelSuppressor = new WheelEventSuppressFilter(group); + wheelSuppressor->setupFor(familyCombo); + wheelSuppressor->setupFor(sizeCombo); + + const auto layout = new QHBoxLayout(group); + layout->addWidget(new QLabel(QObject::tr("Family:"))); + layout->addWidget(familyCombo, 1); + layout->addSpacing(3); + layout->addWidget(new QLabel(QObject::tr("Size:"))); + layout->addWidget(sizeCombo); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(3); + + familyCombo->setCurrentFont(QFont(settings->fontName())); + sizeCombo->setCurrentText(QString("%1").arg(settings->fontSize())); + + QObject::connect(familyCombo, &QFontComboBox::currentFontChanged, settings, [settings](const QFont &font) { + settings->setFontName(font.family()); + }); + QObject::connect(settings, &ApplicationSettings::fontNameChanged, familyCombo, [familyCombo](const QString &fontName){ + familyCombo->setCurrentFont(QFont(fontName)); + }); + + QObject::connect(sizeCombo, &QComboBox::currentTextChanged, settings, [settings](const QString &value) { + settings->setFontSize(value.toInt()); + }); + QObject::connect(settings, &ApplicationSettings::fontSizeChanged, sizeCombo, [sizeCombo](int size) { + sizeCombo->setCurrentText(QString("%1").arg(size)); + }); + + return group; + } + + inline QGroupBox *EditorAppearanceView(ApplicationSettings *settings) + { + const auto group = new QGroupBox(QObject::tr("Editor")); + + const auto layout = new QVBoxLayout(group); + layout->addWidget(FontAppearanceView(settings)); + layout->addWidget(CreateCheckBox( + QObject::tr("Highlight URLs"), + settings, + PREFERENCES_BIND_PROPERTY(urlHighlighting, URLHighlighting) + )); + layout->addWidget(CreateCheckBox( + QObject::tr("Show line numbers"), + settings, + PREFERENCES_BIND_PROPERTY(showLineNumbers, ShowLineNumbers) + )); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(SPACING_6); + + return group; + } +} + +QWidget *AppearanceCategoryItem::contentView(ApplicationSettings *settings) const +{ + const auto widget = new QWidget; + const auto layout = new QVBoxLayout(widget); + + layout->addWidget(WindowAppearanceView(settings)); + layout->addWidget(EditorAppearanceView(settings)); + layout->addStretch(1); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(SPACING_6); + + return widget; +} diff --git a/src/dialogs/Preferences/AppearanceCategoryItem.h b/src/dialogs/Preferences/AppearanceCategoryItem.h new file mode 100644 index 000000000..e1d503dbd --- /dev/null +++ b/src/dialogs/Preferences/AppearanceCategoryItem.h @@ -0,0 +1,36 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#ifndef APPEARANCECATEGORYITEM_H +#define APPEARANCECATEGORYITEM_H + +#include "PreferencesCategoryItem.h" + +class AppearanceCategoryItem : public PreferencesCategoryItem +{ +public: + AppearanceCategoryItem() = default; + virtual ~AppearanceCategoryItem() = default; + + virtual QString title() const override { return QObject::tr("Appearance"); } + virtual QString iconPath() const override { return "://icons/paintbrush.svg"; } + virtual QWidget *contentView(ApplicationSettings *settings) const override; +}; + +#endif // APPEARANCECATEGORYITEM_H diff --git a/src/dialogs/Preferences/BehaviorCategoryItem.cpp b/src/dialogs/Preferences/BehaviorCategoryItem.cpp new file mode 100644 index 000000000..c735b14a5 --- /dev/null +++ b/src/dialogs/Preferences/BehaviorCategoryItem.cpp @@ -0,0 +1,311 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "NotepadNextApplication.h" +#include "PreferencesViewUtils.h" +#include "BehaviorCategoryItem.h" +#include "TranslationManager.h" +#include "ScintillaNext.h" + +using namespace Preferences; + +namespace +{ + const QLatin1StringView PathStyleValid("QLineEdit { border: 1px solid #2ecc71; }"); + const QLatin1StringView PathStyleInvalid("QLineEdit { border: 1px solid #e74c3c; }"); + +#if QT_CONFIG(completer) + inline QCompleter *CreateDefaultFolderCompleter(QObject *parent) + { + const auto fsModel = new QFileSystemModel(parent); + fsModel->setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); + + const auto completer = new QCompleter(fsModel, parent); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + + return completer; + } +#endif + + inline QGroupBox *DefaultFolderView(ApplicationSettings *settings) + { + using BehaviourEnum = ApplicationSettings::DefaultDirectoryBehaviorEnum; + + const auto group = new QGroupBox(QObject::tr("Default directory")); + + const auto layout = new QVBoxLayout(group); + + const auto currentRadio = new QRadioButton(QObject::tr("Current document directory")); + const auto lastUsedRadio = new QRadioButton(QObject::tr("Last used directory")); + const auto selectedRadio = new QRadioButton(QObject::tr("Selected directory:")); + + const auto buttonGroup = new QButtonGroup(group); + buttonGroup->addButton(currentRadio, ApplicationSettings::FollowCurrentDocument); + buttonGroup->addButton(lastUsedRadio, ApplicationSettings::RememberLastUsed); + buttonGroup->addButton(selectedRadio, ApplicationSettings::HardCoded); + + const auto selectedPathEdit = new QLineEdit; + selectedPathEdit->setClearButtonEnabled(true); + selectedPathEdit->setPlaceholderText(QObject::tr("Selected directory path here...")); +#if QT_CONFIG(completer) + selectedPathEdit->setCompleter(CreateDefaultFolderCompleter(selectedPathEdit)); +#endif + + const auto pathBrowseButton = new QPushButton; + pathBrowseButton->setIcon(qApp->style()->standardIcon(QStyle::SP_DirIcon)); + pathBrowseButton->setToolTip(QObject::tr("Open directory select dialog")); + + const auto setPathInputEnabled = [selectedPathEdit, pathBrowseButton](bool state) { + selectedPathEdit->setEnabled(state); + pathBrowseButton->setEnabled(state); + }; + + const auto selectedLayout = new QHBoxLayout; + selectedLayout->addWidget(selectedRadio); + selectedLayout->addWidget(selectedPathEdit, 1); + selectedLayout->addWidget(pathBrowseButton); + selectedLayout->setContentsMargins(MARGINS_0); + selectedLayout->setSpacing(SPACING_3); + + layout->addWidget(currentRadio); + layout->addWidget(lastUsedRadio); + layout->addLayout(selectedLayout); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(SPACING_6); + + {// Load actual path + const auto selectedPath = QDir::toNativeSeparators(settings->defaultDirectory()); + if (!selectedPath.isEmpty()) + selectedPathEdit->setText(selectedPath); + else + selectedPathEdit->setText(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + } + + QObject::connect(selectedPathEdit, &QLineEdit::textChanged, settings, [selectedPathEdit, settings](const QString &text) { + QFileInfo checkDir(text); + QString toolTip; + QString style; + + if (!text.isEmpty()) + { + bool checks = checkDir.exists() + && checkDir.isDir() + && checkDir.isWritable(); + if (checks) + { + style = PathStyleValid; + settings->setDefaultDirectory(text); + } + else + { + QStringList whys; + + if (!checkDir.exists()) + whys += QObject::tr("Not exists"); + if (!checkDir.isDir()) + whys += QObject::tr("Not a directory"); + if (!checkDir.isWritable()) + whys += QObject::tr("No write access"); + + toolTip = whys.join(" | "); + + style = PathStyleInvalid; + } + } + else + toolTip = QObject::tr("Please, use absolute path"); + + selectedPathEdit->setStyleSheet(style); + selectedPathEdit->setToolTip(toolTip); + }); + QObject::connect(pathBrowseButton, &QPushButton::clicked, group, [group, selectedPathEdit]() { + const auto selected = QFileDialog::getExistingDirectory( + group, + QObject::tr("Select default directory"), + selectedPathEdit->text(), + QFileDialog::ShowDirsOnly + | QFileDialog::DontResolveSymlinks + ); + if (!selected.isEmpty()) + selectedPathEdit->setText(selected); + }); + + QObject::connect(buttonGroup, &QButtonGroup::idClicked, settings, [settings, setPathInputEnabled](int id) { + setPathInputEnabled(id == BehaviourEnum::HardCoded); + settings->setDefaultDirectoryBehavior((BehaviourEnum)id); + }); + QObject::connect(settings, &ApplicationSettings::defaultDirectoryBehaviorChanged, buttonGroup, [buttonGroup, setPathInputEnabled](BehaviourEnum value) { + const auto button = buttonGroup->button(value); + const QSignalBlocker block(buttonGroup); // Prevent ss-loop + if (button) button->click(); + setPathInputEnabled(value == BehaviourEnum::HardCoded); + }); + + // Load current selection AFTER QObject::connect's + const auto checkableRadio = buttonGroup->button(settings->defaultDirectoryBehavior()); + if (checkableRadio) checkableRadio->click(); + + return group; + } + + inline QGroupBox *PreviousSessionView(ApplicationSettings *settings) + { + const auto group = new QGroupBox(QObject::tr("Restore previous session")); + group->setCheckable(true); + + const auto layout = new QVBoxLayout(group); + layout->addWidget(CreateCheckBox( + QObject::tr("Unsaved changes"), + settings, + PREFERENCES_BIND_PROPERTY(restoreUnsavedFiles, RestoreUnsavedFiles) + )); + layout->addWidget(CreateCheckBox( + QObject::tr("Temporary files"), + settings, + PREFERENCES_BIND_PROPERTY(restoreTempFiles, RestoreTempFiles) + )); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(SPACING_6); + + group->setChecked(settings->restorePreviousSession()); + + QObject::connect(settings, &ApplicationSettings::restorePreviousSessionChanged, group, &QGroupBox::setChecked); + QObject::connect(group, &QGroupBox::toggled, settings, &ApplicationSettings::setRestorePreviousSession); + + return group; + } + + inline QGridLayout *AppLanguageView(ApplicationSettings *settings) + { + const auto app = qobject_cast(qApp); + + const auto languageCombo = new QComboBox; + languageCombo->addItem(QObject::tr("Like in system"), ""); + for (const auto &languageData : app->getTranslationManager()->availableTranslations()) + { + QLocale locale(languageData); + const auto languageTitle = TranslationManager::FormatLocaleTerritoryAndLanguage(locale); + languageCombo->addItem(languageTitle, languageData); + } + + const auto wheelSuppressor = new WheelEventSuppressFilter(languageCombo); + wheelSuppressor->setupFor(languageCombo); + + const auto restartNotification = new NotificationLabel(QObject::tr("Application restart required to apply changes.")); + + const auto layout = new QGridLayout; + layout->addWidget(new QLabel(QObject::tr("Language:")), 0, 0); + layout->addWidget(languageCombo, 0, 1); + layout->addWidget(restartNotification, 1, 0, 1, 2); + layout->setColumnStretch(1, 1); + layout->setContentsMargins(MARGINS_0); + layout->setSpacing(SPACING_3); + + const auto setComboByData = [languageCombo](const QString &data) { + const auto index = languageCombo->findData(data); + languageCombo->setCurrentIndex(index >= 0 ? index : 0); + }; + + setComboByData(settings->translation()); + + QObject::connect(languageCombo, QOverload::of(&QComboBox::currentIndexChanged), settings, [settings, languageCombo](int index) { + settings->setTranslation(languageCombo->itemData(index).toString()); + }); + QObject::connect(settings, &ApplicationSettings::translationChanged, languageCombo, setComboByData); + + return layout; + } + + inline QHBoxLayout *EOLTypeView(ApplicationSettings *settings) + { + const auto eolCombo = new QComboBox; + eolCombo->addItem(QObject::tr("System default"), QString("")); + eolCombo->addItem(QObject::tr("Windows (CR LF)"), ScintillaNext::eolModeToString(SC_EOL_CRLF)); + eolCombo->addItem(QObject::tr("Unix (LF)"), ScintillaNext::eolModeToString(SC_EOL_LF)); + eolCombo->addItem(QObject::tr("Macintosh (CR)"), ScintillaNext::eolModeToString(SC_EOL_CR)); + + const auto wheelSuppressor = new WheelEventSuppressFilter(eolCombo); + wheelSuppressor->setupFor(eolCombo); + + const auto layout = new QHBoxLayout; + layout->addWidget(new QLabel(QObject::tr("Default line endings:"))); + layout->addWidget(eolCombo, 1); + layout->setContentsMargins(MARGINS_0); + layout->setSpacing(SPACING_3); + + const auto setComboByData = [eolCombo](const QString &data) { + const auto index = eolCombo->findData(data); + eolCombo->setCurrentIndex(index >= 0 ? index : 0); + }; + + setComboByData(settings->defaultEOLMode()); + + QObject::connect(eolCombo, QOverload::of(&QComboBox::currentIndexChanged), settings, [settings, eolCombo](int index) { + settings->setDefaultEOLMode(eolCombo->itemData(index).toString()); + }); + QObject::connect(settings, &ApplicationSettings::defaultEOLModeChanged, eolCombo, setComboByData); + + return layout; + } +} + +QWidget *BehaviorCategoryItem::contentView(ApplicationSettings *settings) const +{ + const auto widget = new QWidget; + const auto layout = new QVBoxLayout(widget); + + layout->addLayout(AppLanguageView(settings)); + layout->addWidget(PreviousSessionView(settings)); + layout->addWidget(DefaultFolderView(settings)); + layout->addLayout(EOLTypeView(settings)); + layout->addWidget(CreateCheckBox( + QObject::tr("Recenter find/replace dialog when opened"), + settings, + PREFERENCES_BIND_PROPERTY(centerSearchDialog, CenterSearchDialog) + )); + layout->addWidget(CreateCheckBox( + QObject::tr("Combine search results"), + settings, + PREFERENCES_BIND_PROPERTY(combineSearchResults, CombineSearchResults) + )); + layout->addWidget(CreateCheckBox( + QObject::tr("Exit on last tab closed"), + settings, + PREFERENCES_BIND_PROPERTY(exitOnLastTabClosed, ExitOnLastTabClosed) + )); + layout->addStretch(1); + layout->setContentsMargins(MARGINS_6); + layout->setSpacing(SPACING_6); + + return widget; +} diff --git a/src/dialogs/Preferences/BehaviorCategoryItem.h b/src/dialogs/Preferences/BehaviorCategoryItem.h new file mode 100644 index 000000000..29effd446 --- /dev/null +++ b/src/dialogs/Preferences/BehaviorCategoryItem.h @@ -0,0 +1,36 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#ifndef BEHAVIORCATEGORYITEM_H +#define BEHAVIORCATEGORYITEM_H + +#include "PreferencesCategoryItem.h" + +class BehaviorCategoryItem : public PreferencesCategoryItem +{ +public: + BehaviorCategoryItem() = default; + virtual ~BehaviorCategoryItem() = default; + + virtual QString title() const override { return QObject::tr("Behavior"); } + virtual QString iconPath() const override { return "://icons/audio-waveform.svg"; } + virtual QWidget *contentView(ApplicationSettings *settings) const override; +}; + +#endif // BEHAVIORCATEGORYITEM_H diff --git a/src/dialogs/Preferences/PreferencesCategoryItem.h b/src/dialogs/Preferences/PreferencesCategoryItem.h new file mode 100644 index 000000000..c80b50407 --- /dev/null +++ b/src/dialogs/Preferences/PreferencesCategoryItem.h @@ -0,0 +1,39 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#ifndef PREFERENCESCATEGORYITEM_H +#define PREFERENCESCATEGORYITEM_H + +#include + +class ApplicationSettings; + +class PreferencesCategoryItem +{ +public: + PreferencesCategoryItem() = default; + virtual ~PreferencesCategoryItem() = default; + + virtual QString title() const = 0; + virtual QString iconPath() const = 0; + /// @warning Caller becomes owner. + virtual QWidget *contentView(ApplicationSettings *settings) const = 0; +}; + +#endif // PREFERENCESCATEGORYITEM_H diff --git a/src/dialogs/Preferences/PreferencesCategoryItemT.h b/src/dialogs/Preferences/PreferencesCategoryItemT.h new file mode 100644 index 000000000..8ba7f8ac3 --- /dev/null +++ b/src/dialogs/Preferences/PreferencesCategoryItemT.h @@ -0,0 +1,45 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#ifndef PREFERENCESCATEGORYITEMT_H +#define PREFERENCESCATEGORYITEMT_H + +#include "PreferencesCategoryItem.h" + +template +class PreferencesCategoryItemT : public PreferencesCategoryItem +{ +public: + PreferencesCategoryItemT(const QString &title, const QString &icon) + : PreferencesCategoryItem(), mTitle(title), mIcon(icon) + { } + virtual ~PreferencesCategoryItemT() = default; + + inline virtual QString title() const override { return mTitle; } + inline virtual QString iconPath() const override { return mIcon; } + inline virtual T *contentView(ApplicationSettings *settings) const override { + return new T(settings); + } + +private: + QString mTitle; + QString mIcon; +}; + +#endif // PREFERENCESCATEGORYITEMT_H diff --git a/src/dialogs/Preferences/PreferencesCategoryListModel.cpp b/src/dialogs/Preferences/PreferencesCategoryListModel.cpp new file mode 100644 index 000000000..402b3801c --- /dev/null +++ b/src/dialogs/Preferences/PreferencesCategoryListModel.cpp @@ -0,0 +1,83 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#include "PreferencesCategoryListModel.h" + +namespace +{ + using ItemPtr = std::unique_ptr; + + constexpr unsigned int RowHeight = 32; +} + +PreferencesCategoryListModel::PreferencesCategoryListModel(QObject *parent) + : QAbstractListModel(parent) +{ + items.reserve(5); +} + +QVariant PreferencesCategoryListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= items.size()) + return {}; + + const auto &item = items.at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + return item->title(); + case Qt::DecorationRole: + return QIcon(item->iconPath()); + case Qt::SizeHintRole: + return QSize(0, RowHeight); + case Qt::TextAlignmentRole: + return Qt::AlignVCenter; + } + + return {}; +} + +void PreferencesCategoryListModel::addCategory(PreferencesCategoryItem *category, int row) +{ + if (row < 0 || row > items.size()) + row = items.size(); + + beginInsertRows(index(row), row, row + 1); + items.insert(items.cbegin() + row, std::move(ItemPtr(category))); + endInsertRows(); +} + +void PreferencesCategoryListModel::removeCategory(int row) +{ + if (row < 0 || row >= items.size()) + return; + + beginRemoveRows(index(row), row, row + 1); + items.erase(items.cbegin() + row); + endRemoveRows(); +} + +PreferencesCategoryItem *PreferencesCategoryListModel::category(int row) const +{ + if (row < 0 || row >= items.size()) + return nullptr; + + return items.at(row).get(); +} diff --git a/src/dialogs/Preferences/PreferencesCategoryListModel.h b/src/dialogs/Preferences/PreferencesCategoryListModel.h new file mode 100644 index 000000000..ce45e6f10 --- /dev/null +++ b/src/dialogs/Preferences/PreferencesCategoryListModel.h @@ -0,0 +1,53 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#ifndef PREFERENCESCATEGORYLISTMODEL_H +#define PREFERENCESCATEGORYLISTMODEL_H + +#include +#include + +#include "PreferencesCategoryItem.h" + +class PreferencesCategoryListModel : public QAbstractListModel +{ +public: + explicit PreferencesCategoryListModel(QObject *parent = nullptr); + virtual ~PreferencesCategoryListModel() = default; + + inline virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override { + return items.size(); + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + /** + * @param item Specific item. + * @param pos Item position. -1 will append to the end. + * @note takes ownership of the item. + */ + void addCategory(PreferencesCategoryItem *category, int row = -1); + void removeCategory(int row); + PreferencesCategoryItem *category(int row) const; + +private: + std::vector> items; +}; + +#endif // PREFERENCESCATEGORYLISTMODEL_H diff --git a/src/dialogs/Preferences/PreferencesViewUtils.h b/src/dialogs/Preferences/PreferencesViewUtils.h new file mode 100644 index 000000000..beaf6c73a --- /dev/null +++ b/src/dialogs/Preferences/PreferencesViewUtils.h @@ -0,0 +1,130 @@ +/* + * This file is part of Notepad Next. + * Copyright 2026 Justin Dailey + * + * Notepad Next is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Notepad Next 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Notepad Next. If not, see . + */ + + +#ifndef PREFERENCESVIEWUTILS_H +#define PREFERENCESVIEWUTILS_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ApplicationSettings.h" + +#define PREFERENCES_BIND_PROPERTY(name, Name) \ + &ApplicationSettings::name, \ + &ApplicationSettings::set##Name, \ + &ApplicationSettings::name##Changed + +namespace Preferences +{ + inline const QMargins MARGINS_0; + inline const QMargins MARGINS_6(6, 6, 6, 6); + + inline const int SPACING_0(0); + inline const int SPACING_3(3); + inline const int SPACING_6(6); + + class WheelEventSuppressFilter : public QObject + { + public: + explicit WheelEventSuppressFilter(QObject *parent = nullptr) + : QObject(parent) { } + + inline void setupFor(QWidget *widget) + { + widget->installEventFilter(this); + widget->setFocusPolicy(Qt::ClickFocus); + } + + inline virtual bool eventFilter(QObject *obj, QEvent *event) override + { + if (event->type() == QEvent::Wheel) + { + const auto widget = qobject_cast(obj); + if (widget && !widget->hasFocus()) + { + event->ignore(); + return true; + } + } + + return QObject::eventFilter(obj, event); + } + }; + + class NotificationLabel : public QWidget + { + public: + NotificationLabel(const QString &text, QWidget *parent = nullptr) + : QWidget(parent) + { + const auto iconSize = qApp->style()->pixelMetric(QStyle::PM_SmallIconSize); + + const auto icon = new QLabel; + icon->setPixmap(qApp->style()->standardIcon(QStyle::SP_MessageBoxInformation).pixmap(iconSize, iconSize)); + + label = new QLabel(text); + label->setWordWrap(true); + + auto infoFont = label->font(); + infoFont.setPointSize(infoFont.pointSize() - 1); + infoFont.setItalic(true); + label->setFont(infoFont); + + const auto layout = new QHBoxLayout(this); + layout->addWidget(icon, 0, Qt::AlignTop); + layout->addWidget(label, 1); + layout->setContentsMargins(9, 0, 3, 6); + layout->setSpacing(SPACING_3); + } + + inline const QLabel *getLabel() const { + return label; + } + + private: + QLabel *label = nullptr; + }; + + template + inline QCheckBox *CreateCheckBox(const QString &title, + ApplicationSettings *settings, + G getter, S setter, N notifier, + const QString &toolTip = QString()) + { + const auto checkBox = new QCheckBox(title); + + if (!toolTip.isEmpty()) + checkBox->setToolTip(toolTip); + + checkBox->setChecked(std::bind(getter, settings)()); + + QObject::connect(settings, notifier, checkBox, &QCheckBox::setChecked); + QObject::connect(checkBox, &QCheckBox::toggled, settings, setter); + + return checkBox; + } +} + +#endif // PREFERENCESVIEWUTILS_H diff --git a/src/dialogs/PreferencesDialog.cpp b/src/dialogs/PreferencesDialog.cpp index 54296f482..9d5bd1bdc 100644 --- a/src/dialogs/PreferencesDialog.cpp +++ b/src/dialogs/PreferencesDialog.cpp @@ -1,6 +1,6 @@ /* * This file is part of Notepad Next. - * Copyright 2019 Justin Dailey + * Copyright 2026 Justin Dailey * * Notepad Next is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,184 +17,235 @@ */ -#include "PreferencesDialog.h" -#include "NotepadNextApplication.h" -#include "TranslationManager.h" -#include "ui_PreferencesDialog.h" -#include "ScintillaNext.h" - -#include -#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Preferences/PreferencesCategoryListModel.h" +#include "Preferences/AppearanceCategoryItem.h" +#include "Preferences/BehaviorCategoryItem.h" +#include "PreferencesDialog.h" +#include "ApplicationSettings.h" -PreferencesDialog::PreferencesDialog(ApplicationSettings *settings, QWidget *parent) : - QDialog(parent, Qt::Tool), - ui(new Ui::PreferencesDialog), - settings(settings) +namespace { - ui->setupUi(this); - - QIcon icon = style()->standardIcon(QStyle::SP_MessageBoxInformation); - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->labelAppRestartIcon->setPixmap(pixmap); - ui->labelAppRestartIcon->hide(); - ui->labelAppRestart->hide(); - - MapSettingToCheckBox(ui->checkBoxMenuBar, &ApplicationSettings::showMenuBar, &ApplicationSettings::setShowMenuBar, &ApplicationSettings::showMenuBarChanged); - MapSettingToCheckBox(ui->checkBoxToolBar, &ApplicationSettings::showToolBar, &ApplicationSettings::setShowToolBar, &ApplicationSettings::showToolBarChanged); - MapSettingToCheckBox(ui->checkBoxStatusBar, &ApplicationSettings::showStatusBar, &ApplicationSettings::setShowStatusBar, &ApplicationSettings::showStatusBarChanged); - MapSettingToCheckBox(ui->checkBoxRecenterSearchDialog, &ApplicationSettings::centerSearchDialog, &ApplicationSettings::setCenterSearchDialog, &ApplicationSettings::centerSearchDialogChanged); - - MapSettingToGroupBox(ui->gbxRestorePreviousSession, &ApplicationSettings::restorePreviousSession, &ApplicationSettings::setRestorePreviousSession, &ApplicationSettings::restorePreviousSessionChanged); - connect(ui->gbxRestorePreviousSession, &QGroupBox::toggled, this, [=](bool checked) { - if (!checked) { - ui->checkBoxUnsavedFiles->setChecked(false); - ui->checkBoxRestoreTempFiles->setChecked(false); - } - else { - QMessageBox::warning(this, tr("Warning"), tr("This feature is experimental and it should not be considered safe for critically important work. It may lead to possible data loss. Use at your own risk.")); - } - }); - - MapSettingToCheckBox(ui->checkBoxUnsavedFiles, &ApplicationSettings::restoreUnsavedFiles, &ApplicationSettings::setRestoreUnsavedFiles, &ApplicationSettings::restoreUnsavedFilesChanged); - MapSettingToCheckBox(ui->checkBoxRestoreTempFiles, &ApplicationSettings::restoreTempFiles, &ApplicationSettings::setRestoreTempFiles, &ApplicationSettings::restoreTempFilesChanged); + using ListModel = PreferencesCategoryListModel; - MapSettingToCheckBox(ui->checkBoxCombineSearchResults, &ApplicationSettings::combineSearchResults, &ApplicationSettings::setCombineSearchResults, &ApplicationSettings::combineSearchResultsChanged); - - populateTranslationComboBox(); - connect(ui->comboBoxTranslation, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { - settings->setTranslation(ui->comboBoxTranslation->itemData(index).toString()); - showApplicationRestartRequired(); - }); - - MapSettingToCheckBox(ui->checkBoxExitOnLastTabClosed, &ApplicationSettings::exitOnLastTabClosed, &ApplicationSettings::setExitOnLastTabClosed, &ApplicationSettings::exitOnLastTabClosedChanged); - - ui->fcbDefaultFont->setCurrentFont(QFont(settings->fontName())); - connect(ui->fcbDefaultFont, &QFontComboBox::currentFontChanged, this, [=](const QFont &f) { - settings->setFontName(f.family()); - }); - connect(settings, &ApplicationSettings::fontNameChanged, this, [=](QString fontName){ - ui->fcbDefaultFont->setCurrentFont(QFont(fontName)); - }); - - ui->spbDefaultFontSize->setValue(settings->fontSize()); - connect(ui->spbDefaultFontSize, QOverload::of(&QSpinBox::valueChanged), settings, &ApplicationSettings::setFontSize); - connect(settings, &ApplicationSettings::fontSizeChanged, ui->spbDefaultFontSize, &QSpinBox::setValue); + inline QFont ContentTitleFont() noexcept + { + auto font = QApplication::font("QLabel"); + font.setPointSize(16); + font.setBold(true); + return font; + } +} - ui->comboBoxLineEndings->addItem(tr("System Default"), QString("")); - ui->comboBoxLineEndings->addItem(tr("Windows (CR LF)"), ScintillaNext::eolModeToString(SC_EOL_CRLF)); - ui->comboBoxLineEndings->addItem(tr("Linux (LF)"), ScintillaNext::eolModeToString(SC_EOL_LF)); - ui->comboBoxLineEndings->addItem(tr("Macintosh (CR)"), ScintillaNext::eolModeToString(SC_EOL_CR)); +struct PreferencesDialog::PreferencesDialogPrivate +{ +public: + inline bool hasUnsavedChanges() const { + return (settings.backup) ? !settings.actual->isEquals(settings.backup) : false; + } + inline void makeSettingsBackup() { + if (settings.backup) settings.backup->fillFrom(settings.actual); + } + inline void makeSettingsRestore() { + if (settings.backup) settings.actual->fillFrom(settings.backup); + } + inline void makeSettingsReset() const + { + std::unique_ptr tempSettings(createTemporarySettings(nullptr)); + if (tempSettings) settings.actual->fillFrom(tempSettings.get()); + } - // Select the current one - int index = ui->comboBoxLineEndings->findData(settings->defaultEOLMode()); - ui->comboBoxLineEndings->setCurrentIndex(index == -1 ? 0 : index); + inline ApplicationSettings *createTemporarySettings(QObject *parent) const + { + auto tempFile = std::make_unique(); - connect(ui->comboBoxLineEndings, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { - settings->setDefaultEOLMode(ui->comboBoxLineEndings->itemData(index).toString()); - }); - connect(settings, &ApplicationSettings::defaultEOLModeChanged, this, [=](QString defaultEOLMode) { - int index = ui->comboBoxLineEndings->findData(defaultEOLMode); - ui->comboBoxLineEndings->setCurrentIndex(index == -1 ? 0 : index); - }); + if (!tempFile->open()) + { + qWarning() << "Unable to create temporary file:" + << tempFile->errorString(); + return nullptr; + } - MapSettingToCheckBox(ui->checkBoxHighlightURLs, &ApplicationSettings::urlHighlighting, &ApplicationSettings::setURLHighlighting, &ApplicationSettings::urlHighlightingChanged); - MapSettingToCheckBox(ui->checkBoxShowLineNumbers, &ApplicationSettings::showLineNumbers, &ApplicationSettings::setShowLineNumbers, &ApplicationSettings::showLineNumbersChanged); + return new ApplicationSettings(tempFile.release(), parent); + } +public: /* Logic */ + struct { + /// @brief Actual settings populated in-app. + ApplicationSettings *actual = nullptr; + /// @brief Settings backup on latest show event. + ApplicationSettings *backup = nullptr; + } settings; - QButtonGroup *buttonGroup = new QButtonGroup(this); - buttonGroup->addButton(ui->radioFollowCurrentDirectory, ApplicationSettings::FollowCurrentDocument); - buttonGroup->addButton(ui->radioLastUsedDirectory, ApplicationSettings::RememberLastUsed); - buttonGroup->addButton(ui->radioHardCoded, ApplicationSettings::HardCoded); +public: /* View */ + QGridLayout *mainLayout = nullptr; - connect(buttonGroup, &QButtonGroup::idClicked, this, [=](int id) { - ApplicationSettings::DefaultDirectoryBehaviorEnum e = static_cast(id); - settings->setDefaultDirectoryBehavior(e); - }); + struct { + QListView *listView = nullptr; + ListModel *model = nullptr; + } category; - connect(ui->radioHardCoded, &QRadioButton::toggled, this, [=](bool checked){ - ui->btnSelectHardCodedPath->setEnabled(checked); - ui->txtHardCodedPath->setEnabled(checked); - }); + struct { + QWidget *widget = nullptr; + QVBoxLayout *layout = nullptr; - connect(ui->btnSelectHardCodedPath, &QToolButton::clicked, this, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Default Directory")); - if (dir.isEmpty()) return; // user cancelled + QLabel *title = nullptr; + QScrollArea *viewport = nullptr; + } content; - settings->setDefaultDirectory(QDir::fromNativeSeparators(dir)); - ui->txtHardCodedPath->setText(QDir::toNativeSeparators(dir)); - }); + QDialogButtonBox *controlsBox = nullptr; +}; - connect(ui->txtHardCodedPath, &QLineEdit::editingFinished, this, [=]() { - QString dir = ui->txtHardCodedPath->text(); - settings->setDefaultDirectory(QDir::fromNativeSeparators(dir)); - ui->txtHardCodedPath->setText(QDir::toNativeSeparators(dir)); +PreferencesDialog::PreferencesDialog(ApplicationSettings *settings, QWidget *parent) + : QDialog(parent, Qt::Tool), + d(new PreferencesDialogPrivate) +{ + setWindowTitle(tr("Preferences")); + + d->settings.actual = settings; + d->settings.backup = d->createTemporarySettings(this); + + // Category + d->category.listView = new QListView; + d->category.listView->setFixedWidth(180); + + d->category.model = new ListModel(d->category.listView); + d->category.model->addCategory(new BehaviorCategoryItem); + d->category.model->addCategory(new AppearanceCategoryItem); + + d->category.listView->setModel(d->category.model); + + // Content + d->content.title = new QLabel; + d->content.title->setFont(ContentTitleFont()); + + d->content.viewport = new QScrollArea; + + d->content.widget = new QWidget; + d->content.widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + d->content.layout = new QVBoxLayout(d->content.widget); + d->content.layout->setContentsMargins(0, 0, 0, 0); + d->content.layout->addWidget(d->content.title); + d->content.layout->addWidget(d->content.viewport); + + // Controls + d->controlsBox = new QDialogButtonBox(Qt::Horizontal); + d->controlsBox->setStandardButtons( + QDialogButtonBox::RestoreDefaults + | QDialogButtonBox::Ok + | QDialogButtonBox::Cancel + ); + + // Main assembly + d->mainLayout = new QGridLayout(this); + d->mainLayout->addWidget(d->category.listView, 0, 0, 2, 1); + d->mainLayout->addWidget(d->content.widget, 0, 1); + d->mainLayout->addWidget(d->controlsBox, 1, 1); + + connect(d->category.listView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &PreferencesDialog::onCategoryChanged); + + connect(d->controlsBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) { + switch (d->controlsBox->buttonRole(button)) + { + case QDialogButtonBox::ResetRole: onResetClicked(); break; + case QDialogButtonBox::AcceptRole: onOkClicked(); break; + case QDialogButtonBox::RejectRole: onCancelClicked(); break; + default: break; + } }); - if (auto b = buttonGroup->button(settings->defaultDirectoryBehavior())) { - b->setChecked(true); - } - - if (settings->defaultDirectoryBehavior() == ApplicationSettings::HardCoded) { - ui->txtHardCodedPath->setText((QDir::toNativeSeparators(settings->defaultDirectory()))); - } - else { - ui->txtHardCodedPath->setText(QString()); - } + // Force switch to first category + QMetaObject::invokeMethod(this, [this](){ + d->category.listView->setCurrentIndex(d->category.model->index(0)); + }, Qt::QueuedConnection); } PreferencesDialog::~PreferencesDialog() { - delete ui; + delete d; } -void PreferencesDialog::showApplicationRestartRequired() const +void PreferencesDialog::showEvent(QShowEvent *event) { - ui->labelAppRestartIcon->show(); - ui->labelAppRestart->show(); + d->makeSettingsBackup(); + QDialog::showEvent(event); } -template -void PreferencesDialog::MapSettingToCheckBox(QCheckBox *checkBox, Func1 getter, Func2 setter, Func3 notifier) const +void PreferencesDialog::closeEvent(QCloseEvent *event) { - // Get the value and set the checkbox state - checkBox->setChecked(std::bind(getter, settings)()); + if (isVisible() && d->hasUnsavedChanges()) + { + const auto reply = QMessageBox::question( + this, + tr("Unsaved сhanges"), + tr("You have unsaved changes.\n" + "Do you want to save them before closing?"), + QMessageBox::Save + | QMessageBox::Discard + | QMessageBox::Cancel + ); + + switch (reply) + { + case QMessageBox::Cancel: + event->ignore(); + return; + case QMessageBox::Discard: + d->makeSettingsRestore(); + [[fallthrough]]; + case QMessageBox::Save: + d->settings.actual->sync(); + break; + default: + break; + } + } - // Set up two way connection - connect(settings, notifier, checkBox, &QCheckBox::setChecked); - connect(checkBox, &QCheckBox::toggled, settings, setter); + QDialog::closeEvent(event); } -template -void PreferencesDialog::MapSettingToGroupBox(QGroupBox *groupBox, Func1 getter, Func2 setter, Func3 notifier) const +void PreferencesDialog::onCategoryChanged(const QModelIndex &index) { - // Get the value and set the checkbox state - groupBox->setChecked(std::bind(getter, settings)()); + if (!index.isValid()) return; - // Set up two way connection - connect(settings, notifier, groupBox, &QGroupBox::setChecked); - connect(groupBox, &QGroupBox::toggled, settings, setter); + const auto item = d->category.model->category(index.row()); + if (!item) return; + + d->content.title->setText(item->title()); + d->content.viewport->setWidget(item->contentView(d->settings.actual)); + d->content.viewport->widget()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + d->content.viewport->setWidgetResizable(true); } -void PreferencesDialog::populateTranslationComboBox() +void PreferencesDialog::onOkClicked() { - NotepadNextApplication *app = qobject_cast(qApp); - - // Add the system default at the top - ui->comboBoxTranslation->addItem(tr(""), QStringLiteral("")); + d->settings.actual->sync(); + accept(); +} - // TODO: sort this list and keep the system default at the top - for (const auto &localeName : app->getTranslationManager()->availableTranslations()) - { - QLocale locale(localeName); - const QString localeDisplay = TranslationManager::FormatLocaleTerritoryAndLanguage(locale); - ui->comboBoxTranslation->addItem(localeDisplay, localeName); - } +void PreferencesDialog::onCancelClicked() +{ + close(); +} - // Select the current one - int index = ui->comboBoxTranslation->findData(settings->translation()); - if (index != -1) { - ui->comboBoxTranslation->setCurrentIndex(index); - } +void PreferencesDialog::onResetClicked() +{ + d->makeSettingsReset(); } diff --git a/src/dialogs/PreferencesDialog.h b/src/dialogs/PreferencesDialog.h index 985a9b8b4..507f5908a 100644 --- a/src/dialogs/PreferencesDialog.h +++ b/src/dialogs/PreferencesDialog.h @@ -1,6 +1,6 @@ /* * This file is part of Notepad Next. - * Copyright 2019 Justin Dailey + * Copyright 2026 Justin Dailey * * Notepad Next is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,39 +20,31 @@ #ifndef PREFERENCESDIALOG_H #define PREFERENCESDIALOG_H -#include "ApplicationSettings.h" - #include -#include -#include - -namespace Ui { -class PreferencesDialog; -} -class Settings; +class ApplicationSettings; class PreferencesDialog : public QDialog { Q_OBJECT - public: - PreferencesDialog(ApplicationSettings *settings, QWidget *parent = 0); - ~PreferencesDialog(); + PreferencesDialog(ApplicationSettings *settings, QWidget *parent = nullptr); + virtual ~PreferencesDialog(); - void showApplicationRestartRequired() const; +protected: + virtual void showEvent(QShowEvent *event) override; + virtual void closeEvent(QCloseEvent *event) override; -private: - Ui::PreferencesDialog *ui; - ApplicationSettings *settings; - - template - void MapSettingToCheckBox(QCheckBox *checkBox, Func1 getter, Func2 setter, Func3 notifier) const; +private slots: + void onCategoryChanged(const QModelIndex &index); - template - void MapSettingToGroupBox(QGroupBox *groupBox, Func1 getter, Func2 setter, Func3 notifier) const; + void onOkClicked(); + void onCancelClicked(); + void onResetClicked(); - void populateTranslationComboBox(); +private: + struct PreferencesDialogPrivate; + PreferencesDialogPrivate *d; }; #endif // PREFERENCESDIALOG_H diff --git a/src/dialogs/PreferencesDialog.ui b/src/dialogs/PreferencesDialog.ui deleted file mode 100644 index 2d98c2230..000000000 --- a/src/dialogs/PreferencesDialog.ui +++ /dev/null @@ -1,342 +0,0 @@ - - - PreferencesDialog - - - - 0 - 0 - 803 - 597 - - - - Preferences - - - - - - true - - - - - 0 - -99 - 769 - 644 - - - - - - - - - Show menu bar - - - - - - - Show toolbar - - - - - - - Show status bar - - - - - - - Restore previous session - - - true - - - false - - - - - - Unsaved changes - - - - - - - Temporary files - - - - - - - - - - - - Recenter find/replace dialog when opened - - - - - - - Combine search results - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - - Translation: - - - - - - - - - Exit on last tab closed - - - - - - - - - Default Font - - - - - - Font - - - - - - - - - - Font Size - - - - - - - pt - - - 2 - - - 48 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Default Line Endings - - - - - - - - - - - - Highlight URLs - - - - - - - Show Line Numbers - - - - - - - Default Directory - - - - - - Follow Current Document - - - - - - - Last Used Directory - - - - - - - - - - - - - - - - - - - ... - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - TextLabel - - - - - - - - true - - - - An application restart is required to apply certain settings. - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - accepted() - PreferencesDialog - accept() - - - 792 - 586 - - - 157 - 274 - - - - - buttonBox - rejected() - PreferencesDialog - reject() - - - 792 - 586 - - - 286 - 274 - - - - - diff --git a/src/icons/audio-waveform.svg b/src/icons/audio-waveform.svg new file mode 100644 index 000000000..17d3bb76b --- /dev/null +++ b/src/icons/audio-waveform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/paintbrush.svg b/src/icons/paintbrush.svg new file mode 100644 index 000000000..078adf1da --- /dev/null +++ b/src/icons/paintbrush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources.qrc b/src/resources.qrc index 9f390c4bd..7e07d8241 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -42,6 +42,8 @@ icons/application_osx_terminal.png icons/folder_go.png icons/wrapindicator.png + icons/audio-waveform.svg + icons/paintbrush.svg icons/cross.svg icons/plus.svg icons/list_with_icons.svg