From 30cf6d9e974d83ccc220a514b7822e39d6d40113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 24 Aug 2026 13:07:43 -0300 Subject: [PATCH] ecode: add centralized settings panel - add user and project settings windows with searchable tree navigation - centralize setting definitions with reusable bool, choice, text, numeric, and action controls - apply supported configuration changes reactively - add inline validation and editable choices for complex value types - disable project document options while inheriting global settings - migrate font, theme, language, renderer, screenshot, and terminal settings - improve keyboard navigation and modal shortcut isolation - make configuration dialogs modal - persist settings when the panel closes - add responsive dark and light theme styling - update translations for new settings labels and descriptions - add regression coverage for modal keybinding propagation --- bin/assets/i18n/de.xml | 9 +- bin/assets/i18n/en.xml | 9 +- bin/assets/i18n/fr.xml | 9 +- bin/assets/i18n/zh.xml | 9 +- src/eepp/ui/uiwindow.cpp | 3 +- src/tests/unit_tests/uiscenenode_tests.cpp | 30 ++ src/tools/ecode/settingsactions.cpp | 39 +- src/tools/ecode/settingsactions.hpp | 1 + src/tools/ecode/settingspanel.cpp | 585 ++++++++++++++++----- src/tools/ecode/settingspanel.hpp | 13 +- src/tools/ecode/terminalmanager.cpp | 4 +- 11 files changed, 528 insertions(+), 183 deletions(-) diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index cca58cc75..3e085cc1d 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -148,6 +148,7 @@ Diese Operation kann nicht rückgängig gemacht werden! Verwalten... Ignorierte Dateien verwalten... Konsolenverlaufslänge ändern + Terminal-Verlauf Konsolen-Shell wählen Gewählter Debugger Gewählter Formatter @@ -1024,10 +1025,10 @@ Verwendet strftime-Formatbezeichner. Die Dateierweiterung richtet sich nach dem Schriftgröße der Seitenbereiche festlegen. Standardschriftgröße des Code-Editors festlegen. Standardschriftgröße des Terminals festlegen. - Gesamte Benutzeroberfläche skalieren. - Zusätzlichen vertikalen Abstand zwischen Editorzeilen festlegen. - Blinkintervall des Textcursors festlegen. - Häufigkeit der Neuberechnung von Faltungsbereichen festlegen. + Gesamte Benutzeroberfläche von 1 bis 6 skalieren. Neustart erforderlich. + Zusätzlichen vertikalen Abstand zwischen Editorzeilen festlegen. Zum Deaktivieren 0 einstellen. + Blinkintervall des Textcursors festlegen. Zum Deaktivieren 0 einstellen. + Häufigkeit der Neuberechnung von Faltungsbereichen festlegen (mindestens 1 Sekunde). Speicherort für Bildschirmfotos auswählen. Zeitstempel-Dateinamensmuster für Bildschirmfotos konfigurieren. Bildformat für Bildschirmfotos auswählen. diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index f8d327faf..03574f9cd 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -132,6 +132,7 @@ This operation cannot be reverted! Configure... Configure Ignore Files... Configure Terminal Scrollback + Terminal Scrollback Configure Terminal Shell Configured debugger Configured formatter @@ -1008,10 +1009,10 @@ Uses strftime format specifiers. The file extension follows the selected screens Set the font size used by side panels. Set the default code editor font size. Set the default integrated terminal font size. - Scale the complete user interface. - Set additional vertical spacing between editor lines. - Set the text cursor blink interval. - Set how frequently code folding ranges are recalculated. + Scale the complete user interface from 1 to 6. Restart required. + Set additional vertical spacing between editor lines. Set 0 to disable. + Set the text cursor blink interval. Set 0 to disable. + Set how frequently code folding ranges are recalculated (minimum 1 second). Choose where screenshots are saved. Configure the timestamp-based screenshot filename pattern. Choose the image format for screenshots. diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index 14473157d..4e2522b9b 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -133,6 +133,7 @@ Cette opération est irréversible ! Configurer... Configurer les fichiers ignorés... Configurer le défilement de l'historique du terminal + Historique du terminal Configurer l'interpréteur de commandes du terminal Débogueur configuré Formatteur configuré @@ -1003,10 +1004,10 @@ Utilise les spécificateurs de format strftime. L’extension du fichier corresp Set the font size used by side panels. Set the default code editor font size. Set the default integrated terminal font size. - Scale the complete user interface. - Set additional vertical spacing between editor lines. - Set the text cursor blink interval. - Set how frequently code folding ranges are recalculated. + Mettre l’interface à l’échelle de 1 à 6. Redémarrage requis. + Définir l’espace vertical supplémentaire entre les lignes. Utiliser 0 pour désactiver. + Définir l’intervalle de clignotement du curseur. Utiliser 0 pour désactiver. + Définir la fréquence de recalcul des zones de pliage du code (minimum 1 seconde). Choose where screenshots are saved. Configure the timestamp-based screenshot filename pattern. Choose the image format for screenshots. diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 3bfb41d7b..fbc8981ef 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -110,6 +110,7 @@ 配置默认终端 设置忽略的文件... Configure Terminal Scrollback + 终端回滚行数 设置终端程序 你确定要删除构建设置吗? 确定要关闭此文件夹吗? @@ -782,10 +783,10 @@ file in the directory tree. Set the font size used by side panels. Set the default code editor font size. Set the default integrated terminal font size. - Scale the complete user interface. - Set additional vertical spacing between editor lines. - Set the text cursor blink interval. - Set how frequently code folding ranges are recalculated. + 将整个用户界面缩放为 1 到 6。需要重新启动。 + 设置编辑器行之间的额外垂直间距。设置为 0 可禁用。 + 设置文本光标闪烁间隔。设置为 0 可禁用。 + 设置重新计算代码折叠区域的频率(最少 1 秒)。 Choose where screenshots are saved. Configure the timestamp-based screenshot filename pattern. Choose the image format for screenshots. diff --git a/src/eepp/ui/uiwindow.cpp b/src/eepp/ui/uiwindow.cpp index 6d9fe1906..b33e040a9 100644 --- a/src/eepp/ui/uiwindow.cpp +++ b/src/eepp/ui/uiwindow.cpp @@ -1771,7 +1771,8 @@ Uint32 UIWindow::onKeyDown( const KeyEvent& event ) { executeKeyBindingCommand( cmd ); return 1; } - return UIWidget::onKeyDown( event ); + Uint32 handled = UIWidget::onKeyDown( event ); + return handled || isModal(); } KeyBindings& UIWindow::getKeyBindings() { diff --git a/src/tests/unit_tests/uiscenenode_tests.cpp b/src/tests/unit_tests/uiscenenode_tests.cpp index d3ceb0963..317c50908 100644 --- a/src/tests/unit_tests/uiscenenode_tests.cpp +++ b/src/tests/unit_tests/uiscenenode_tests.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -391,3 +393,31 @@ UTEST( UISceneNode, StyleStateUpdateAllowsWidgetCreation ) { Engine::destroySingleton(); } + +UTEST( UIWindow, ModalWindowStopsKeyBindingsFromReachingScene ) { + Engine::instance()->createWindow( WindowSettings( 1024, 768, "Modal Key Binding Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_scene_node(); + int executedCommands = 0; + sceneNode->setKeyBindingCommand( "global-command", + [&executedCommands] { executedCommands++; } ); + sceneNode->addKeyBinding( { KEY_F, KEYMOD_LMETA | KEYMOD_LSHIFT }, "global-command" ); + + UIWindow* window = UIWindow::New(); + window->setParent( sceneNode->getRoot() ); + sceneNode->getEventDispatcher()->setFocusNode( window->getContainer() ); + + sceneNode->getEventDispatcher()->sendKeyDown( KEY_F, SCANCODE_UNKNOWN, 0, + KEYMOD_LMETA | KEYMOD_LSHIFT ); + EXPECT_EQ( executedCommands, 1 ); + + window->setWindowFlags( window->getWinFlags() | UI_WIN_MODAL ); + sceneNode->getEventDispatcher()->sendKeyDown( KEY_F, SCANCODE_UNKNOWN, 0, + KEYMOD_LMETA | KEYMOD_LSHIFT ); + EXPECT_EQ( executedCommands, 1 ); + + Engine::destroySingleton(); +} diff --git a/src/tools/ecode/settingsactions.cpp b/src/tools/ecode/settingsactions.cpp index 70eb718b5..66b1a8a2f 100644 --- a/src/tools/ecode/settingsactions.cpp +++ b/src/tools/ecode/settingsactions.cpp @@ -410,30 +410,31 @@ void SettingsActions::setUIPanelFontSize() { msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); msgBox->showWhenReady(); msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { - mApp->getConfig().ui.panelFontSize = StyleSheetLength( msgBox->getTextInput()->getText() ); - - // Update the CSS - auto selsFound = mApp->getUISceneNode()->getStyleSheet().findStyleFromSelectorName( - "#project_view > treeview::row > treeview::cell" ); - if ( !selsFound.empty() ) { - for ( auto sel : selsFound ) - sel->updatePropertyValue( "font-size", - mApp->getConfig().ui.panelFontSize.toString() ); - mApp->getUISceneNode()->getStyleSheet().refreshCacheFromStyles( selsFound ); - } - - UITreeView* treeView = mApp->getUISceneNode()->find( "project_view" ); - if ( !treeView ) { - msgBox->closeWindow(); - return; - } - treeView->reloadStyle( true, true, true, true ); - treeView->updateContentSize(); + setUIPanelFontSize( StyleSheetLength( msgBox->getTextInput()->getText() ) ); msgBox->closeWindow(); } ); mApp->setFocusEditorOnClose( msgBox ); } +void SettingsActions::setUIPanelFontSize( const StyleSheetLength& size ) { + mApp->getConfig().ui.panelFontSize = size; + + // Update the CSS + auto selsFound = mApp->getUISceneNode()->getStyleSheet().findStyleFromSelectorName( + "#project_view > treeview::row > treeview::cell" ); + if ( !selsFound.empty() ) { + for ( auto sel : selsFound ) + sel->updatePropertyValue( "font-size", size.toString() ); + mApp->getUISceneNode()->getStyleSheet().refreshCacheFromStyles( selsFound ); + } + + UITreeView* treeView = mApp->getUISceneNode()->find( "project_view" ); + if ( !treeView ) + return; + treeView->reloadStyle( true, true, true, true ); + treeView->updateContentSize(); +} + void SettingsActions::setScreenshotSavePath() { std::string initialPath = mApp->getConfig().screenshot.savePath; if ( !FileSystem::isDirectory( initialPath ) ) { diff --git a/src/tools/ecode/settingsactions.hpp b/src/tools/ecode/settingsactions.hpp index 70068b271..67b8f3d39 100644 --- a/src/tools/ecode/settingsactions.hpp +++ b/src/tools/ecode/settingsactions.hpp @@ -41,6 +41,7 @@ class SettingsActions { void setTerminalFontSize( const StyleSheetLength& size ); void setUIPanelFontSize(); + void setUIPanelFontSize( const StyleSheetLength& size ); void setScreenshotSavePath(); diff --git a/src/tools/ecode/settingspanel.cpp b/src/tools/ecode/settingspanel.cpp index 3627f7825..22aaeba02 100644 --- a/src/tools/ecode/settingspanel.cpp +++ b/src/tools/ecode/settingspanel.cpp @@ -1,7 +1,9 @@ #include "settingspanel.hpp" +#include "datetimecontroller.hpp" #include "ecode.hpp" #include "uitreeviewfs.hpp" #include +#include using namespace EE::UI::Models; @@ -110,49 +112,82 @@ class SettingsCategoryModel final : public Model { static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml( - - - - + + + + + + - + @@ -190,24 +235,35 @@ static constexpr const char* SETTINGS_ROW_LAYOUT = R"xml( - - + + )xml"; -static constexpr const char* SETTINGS_CATEGORY_HEADING_LAYOUT = - R"xml()xml"; +static constexpr const char* SETTINGS_CATEGORY_HEADING_LAYOUT = R"xml( + +)xml"; static constexpr const char* SETTINGS_BOOL_LAYOUT = R"xml()xml"; static constexpr const char* SETTINGS_CHOICE_LAYOUT = R"xml()xml"; +static constexpr const char* SETTINGS_EDITABLE_CHOICE_LAYOUT = + R"xml()xml"; static constexpr const char* SETTINGS_INTEGER_LAYOUT = R"xml()xml"; +static constexpr const char* SETTINGS_TEXT_LAYOUT = R"xml()xml"; static constexpr const char* SETTINGS_ACTION_LAYOUT = R"xml()xml"; +static void disableTabFocusTree( Node* node ) { + if ( node->isWidget() ) + node->asType()->unsetTabFocusable(); + for ( auto* child = node->getFirstChild(); child; child = child->getNextNode() ) + disableTabFocusTree( child ); +} + SettingsPanel::SettingsPanel( App* app ) : mApp( app ) {} SettingsPanel::PanelState& SettingsPanel::state( Scope scope ) { @@ -242,7 +298,10 @@ void SettingsPanel::show( Scope scope ) { panel.categories->setFocus(); return; } + Clock c; create( scope ); + Log::info( "Settings Panel %s created in %s", scope == Scope::User ? "User" : "Project", + c.getElapsedTime().toString() ); } void SettingsPanel::create( Scope scope ) { @@ -274,11 +333,31 @@ void SettingsPanel::create( Scope scope ) { panel.scroll = layout->find( "settings_scroll" ); panel.scroll->setVerticalScrollMode( ScrollBarMode::Auto ); panel.scroll->setHorizontalScrollMode( ScrollBarMode::AlwaysOff ); + disableTabFocusTree( panel.categories->getVerticalScrollBar() ); + disableTabFocusTree( panel.categories->getHorizontalScrollBar() ); + disableTabFocusTree( panel.scroll->getVerticalScrollBar() ); + disableTabFocusTree( panel.scroll->getHorizontalScrollBar() ); + panel.window->setKeyBindingCommand( "focusSettingsFilter", [&panel] { + panel.search->setFocus(); + panel.search->getDocument().selectAll(); + } ); + panel.window->setKeyBindingCommand( "focusSettingsCategories", [&panel] { + auto selected = panel.categories->getSelection().first(); + if ( selected.isValid() ) + panel.categories->setSelection( selected ); + panel.categories->setFocus(); + } ); + panel.window->getKeyBindings().addKeybind( { KEY_F, KeyMod::getDefaultModifier() }, + "focusSettingsFilter" ); + panel.window->getKeyBindings().addKeybind( + { KEY_E, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "focusSettingsCategories" ); + panel.settings->beginAttributesTransaction(); if ( scope == Scope::User ) addUserSettings( panel ); else addProjectSettings( panel ); setupCategories( panel ); + panel.settings->endAttributesTransaction(); filter( panel ); panel.connections += panel.search->connect( Event::OnTextChanged, [this, &panel]( const Event* ) { filter( panel ); } ); @@ -426,17 +505,87 @@ void SettingsPanel::addChoice( PanelState& panel, SettingBinding binding, panel.bindings.emplace_back( std::move( binding ) ); } +void SettingsPanel::addEditableChoice( PanelState& panel, SettingBinding binding, + const std::vector& choices, + std::function get, + std::function set ) { + auto* row = createRow( panel, binding ); + auto* combo = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_EDITABLE_CHOICE_LAYOUT, + row->find( "setting_control" ) ) + ->asType(); + combo->getListBox()->addListBoxItems( choices ); + combo->setText( get() ); + panel.connections += + combo->connect( Event::OnValueChange, [combo, set = std::move( set )]( const Event* ) { + if ( !set( combo->getText() ) ) { + combo->addClass( "error" ); + combo->getDropDownList()->addClass( "error" ); + return; + } + combo->removeClass( "error" ); + combo->getDropDownList()->removeClass( "error" ); + } ); + panel.bindings.emplace_back( std::move( binding ) ); +} + void SettingsPanel::addInteger( PanelState& panel, SettingBinding binding, int min, int max, - int* value, std::function apply ) { + std::function get, std::function set ) { auto* row = createRow( panel, binding ); auto* spin = mApp->getUISceneNode() ->loadLayoutFromString( SETTINGS_INTEGER_LAYOUT, row->find( "setting_control" ) ) ->asType(); spin->setMinValue( min )->setMaxValue( max ); - auto valueBinding = UIDataBind::New( value, spin ); - valueBinding->onValueChangeCb = std::move( apply ); - panel.bindingGroup += std::move( valueBinding ); + spin->unsetTabFocusable(); + spin->getButtonPushUp()->asType()->unsetTabFocusable(); + spin->getButtonPushDown()->asType()->unsetTabFocusable(); + spin->setValue( get() ); + panel.connections += + spin->connect( Event::OnValueChange, [spin, set = std::move( set )]( const Event* ) { + set( static_cast( spin->getValue() ) ); + } ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +void SettingsPanel::addText( PanelState& panel, SettingBinding binding, + std::function get, + std::function set ) { + auto* row = createRow( panel, binding ); + auto* input = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_TEXT_LAYOUT, + row->find( "setting_control" ) ) + ->asType(); + input->setText( String::fromUtf8( get() ) ); + panel.connections += + input->connect( Event::OnTextChanged, [input, set = std::move( set )]( const Event* ) { + std::string text = input->getText().toUtf8(); + if ( !set( text ) ) { + input->addClass( "error" ); + return; + } + input->removeClass( "error" ); + } ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +void SettingsPanel::addFloat( PanelState& panel, SettingBinding binding, double min, double max, + double step, std::function get, + std::function set ) { + auto* row = createRow( panel, binding ); + auto* spin = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_INTEGER_LAYOUT, + row->find( "setting_control" ) ) + ->asType(); + spin->setMinValue( min )->setMaxValue( max )->setClickStep( step ); + spin->allowFloatingPoint( true )->setValue( get() ); + spin->unsetTabFocusable(); + spin->getButtonPushUp()->asType()->unsetTabFocusable(); + spin->getButtonPushDown()->asType()->unsetTabFocusable(); + panel.connections += + spin->connect( Event::OnValueChange, [spin, set = std::move( set )]( const Event* ) { + set( spin->getValue() ); + } ); panel.bindings.emplace_back( std::move( binding ) ); } @@ -453,6 +602,43 @@ void SettingsPanel::addAction( PanelState& panel, SettingBinding binding, const panel.bindings.emplace_back( std::move( binding ) ); } +static void setNodeTreeEnabled( Node* node, bool enabled ) { + node->setEnabled( enabled ); + for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() ) + setNodeTreeEnabled( child, enabled ); +} + +static bool parseNonNegativeTime( const std::string& text, Time& time ) { + auto parts = String::split( text, " " ); + bool hasValue = false; + for ( auto& part : parts ) { + String::trimInPlace( part ); + if ( part.empty() ) + continue; + size_t suffixLength = + String::endsWith( part, "ms" ) + ? 2 + : ( String::endsWith( part, "s" ) || String::endsWith( part, "m" ) ? 1 : 0 ); + std::string number = part.substr( 0, part.size() - suffixLength ); + double value; + if ( number.empty() || !String::fromString( value, number ) || value < 0 ) + return false; + hasValue = true; + } + if ( !hasValue ) + return false; + time = Time::fromString( text ); + return true; +} + +void SettingsPanel::setCategoryEnabled( PanelState& panel, const std::string& category, + bool enabled, const std::string& excludedSetting ) { + for ( auto& binding : panel.bindings ) { + if ( binding.category == category && binding.id != excludedSetting && binding.row ) + setNodeTreeEnabled( binding.row, enabled ); + } +} + void SettingsPanel::addUserSettings( PanelState& panel ) { addCategory( panel, "general.behavior", mApp->i18n( "general", "General" ), mApp->i18n( "behavior", "Behavior" ) ); @@ -665,7 +851,9 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { mApp->i18n( "line_breaking_column", "Line Breaking Column" ), mApp->i18n( "line_breaking_column_desc", "Column used for wrapping and the editor width guide. Set 0 to disable." ) }, - 0, 1000, &mApp->getConfig().doc.lineBreakingColumn, [this]( int value ) { + 0, 1000, [this] { return mApp->getConfig().doc.lineBreakingColumn; }, + [this]( int value ) { + mApp->getConfig().doc.lineBreakingColumn = value; mApp->getSplitter()->forEachEditor( [value]( UICodeEditor* editor ) { editor->setLineBreakingColumn( value ); } ); } ); @@ -848,14 +1036,18 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { editor->getDocument().setTabOutEnabled( value ); } ); } ); - addInteger( panel, - { "indentWidth", "editor.document", mApp->i18n( "indent_width", "Indent Width" ), - mApp->i18n( "indent_width_desc", "Columns in one indentation level." ) }, - 1, 16, &mApp->getConfig().doc.indentWidth ); - addInteger( panel, - { "tabWidth", "editor.document", mApp->i18n( "tab_width", "Tab Width" ), - mApp->i18n( "tab_width_desc", "Columns used to display a tab character." ) }, - 1, 16, &mApp->getConfig().doc.tabWidth ); + addInteger( + panel, + { "indentWidth", "editor.document", mApp->i18n( "indent_width", "Indent Width" ), + mApp->i18n( "indent_width_desc", "Columns in one indentation level." ) }, + 1, 16, [this] { return mApp->getConfig().doc.indentWidth; }, + [this]( int value ) { mApp->getConfig().doc.indentWidth = value; } ); + addInteger( + panel, + { "tabWidth", "editor.document", mApp->i18n( "tab_width", "Tab Width" ), + mApp->i18n( "tab_width_desc", "Columns used to display a tab character." ) }, + 1, 16, [this] { return mApp->getConfig().doc.tabWidth; }, + [this]( int value ) { mApp->getConfig().doc.tabWidth = value; } ); addChoice( panel, { "lineEndings", "editor.document", mApp->i18n( "line_endings", "Line Endings" ), @@ -1079,38 +1271,56 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { mApp->i18n( "fallback_font_desc", "Choose the font used for missing glyphs." ) }, mApp->i18n( "choose_font", "Choose Font..." ), [this] { mApp->runCommand( "fallback-font" ); } ); - addAction( - panel, + auto addFontSize = [this, &panel]( SettingBinding binding, std::function get, + std::function set ) { + addText( panel, std::move( binding ), std::move( get ), + [set = std::move( set )]( const std::string& text ) { + if ( !StyleSheetLength::isLength( text ) ) + return false; + set( StyleSheetLength::fromString( text ) ); + return true; + } ); + }; + addFontSize( { "uiFontSize", "appearance.fonts", mApp->i18n( "ui_font_size", "UI Font Size" ), mApp->i18n( "ui_font_size_desc", "Set the font size used by the application UI." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setUIFontSize(); } ); - addAction( - panel, + [this] { return mApp->getConfig().ui.fontSize.toString(); }, + [this]( const StyleSheetLength& size ) { + mApp->getSettingsActions()->setUIFontSize( size ); + } ); + addFontSize( { "panelFontSize", "appearance.fonts", mApp->i18n( "ui_panel_font_size", "Panel Font Size" ), mApp->i18n( "ui_panel_font_size_desc", "Set the font size used by side panels." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setUIPanelFontSize(); } ); - addAction( panel, - { "editorFontSize", "appearance.fonts", - mApp->i18n( "editor_font_size", "Editor Font Size" ), - mApp->i18n( "editor_font_size_desc", "Set the default code editor font size." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setEditorFontSize(); } ); - addAction( panel, - { "terminalFontSize", "appearance.fonts", - mApp->i18n( "terminal_font_size", "Terminal Font Size" ), - mApp->i18n( "terminal_font_size_desc", - "Set the default integrated terminal font size." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setTerminalFontSize(); } ); - addAction( panel, - { "uiScaleFactor", "appearance.fonts", - mApp->i18n( "ui_scale_factor", "UI Scale Factor" ), - mApp->i18n( "ui_scale_factor_desc", "Scale the complete user interface." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setUIScaleFactor(); } ); + [this] { return mApp->getConfig().ui.panelFontSize.toString(); }, + [this]( const StyleSheetLength& size ) { + mApp->getSettingsActions()->setUIPanelFontSize( size ); + } ); + addFontSize( + { "editorFontSize", "appearance.fonts", + mApp->i18n( "editor_font_size", "Editor Font Size" ), + mApp->i18n( "editor_font_size_desc", "Set the default code editor font size." ) }, + [this] { return mApp->getConfig().editor.fontSize.toString(); }, + [this]( const StyleSheetLength& size ) { + mApp->getSettingsActions()->setEditorFontSize( size ); + } ); + addFontSize( + { "terminalFontSize", "appearance.fonts", + mApp->i18n( "terminal_font_size", "Terminal Font Size" ), + mApp->i18n( "terminal_font_size_desc", + "Set the default integrated terminal font size." ) }, + [this] { return mApp->getConfig().term.fontSize.toString(); }, + [this]( const StyleSheetLength& size ) { + mApp->getSettingsActions()->setTerminalFontSize( size ); + } ); + addFloat( + panel, + { "uiScaleFactor", "appearance.fonts", mApp->i18n( "ui_scale_factor", "UI Scale Factor" ), + mApp->i18n( "ui_scale_factor_desc", + "Scale the complete user interface from 1 to 6. Restart required." ) }, + 1, 6, 0.1, + [this] { return std::max( 1, mApp->getConfig().windowState.pixelDensity ); }, + [this]( double value ) { mApp->getConfig().windowState.pixelDensity = value; } ); addBool( panel, { "editorFontInInputFields", "appearance.fonts", mApp->i18n( "editor_font_in_input_fields", "Editor Font in Input Fields" ), @@ -1202,25 +1412,59 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { addCategory( panel, "editor.advanced", mApp->i18n( "editor", "Editor" ), mApp->i18n( "advanced", "Advanced" ) ); - addAction( panel, - { "lineSpacing", "editor.advanced", mApp->i18n( "line_spacing", "Line Spacing" ), - mApp->i18n( "line_spacing_desc", - "Set additional vertical spacing between editor lines." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setLineSpacing(); } ); - addAction( panel, - { "cursorBlinkingTime", "editor.advanced", - mApp->i18n( "cursor_blinking_time", "Cursor Blinking Time" ), - mApp->i18n( "cursor_blinking_time_desc", "Set the text cursor blink interval." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setCursorBlinkingTime(); } ); - addAction( panel, - { "indentTabCharacter", "editor.advanced", - mApp->i18n( "indent_tab_character", "Indent Tab Character" ), - mApp->i18n( "indent_tab_character_desc", - "Choose the character inserted when indenting with Tab." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setIndentTabCharacter(); } ); + addText( + panel, + { "lineSpacing", "editor.advanced", mApp->i18n( "line_spacing", "Line Spacing" ), + mApp->i18n( "line_spacing_desc", + "Set additional vertical spacing between editor lines. Set 0 to disable." ) }, + [this] { return mApp->getConfig().editor.lineSpacing.toString(); }, + [this]( const std::string& text ) { + if ( !StyleSheetLength::isLength( text ) ) + return false; + mApp->getConfig().editor.lineSpacing = StyleSheetLength::fromString( text ); + mApp->getSplitter()->forEachEditor( [this]( UICodeEditor* editor ) { + editor->setLineSpacing( mApp->getConfig().editor.lineSpacing ); + } ); + return true; + } ); + addText( + panel, + { "cursorBlinkingTime", "editor.advanced", + mApp->i18n( "cursor_blinking_time", "Cursor Blinking Time" ), + mApp->i18n( "cursor_blinking_time_desc", + "Set the text cursor blink interval. Set 0 to disable." ) }, + [this] { return mApp->getConfig().editor.cursorBlinkingTime.toString(); }, + [this]( const std::string& text ) { + Time value; + if ( !parseNonNegativeTime( text, value ) ) + return false; + mApp->getConfig().editor.cursorBlinkingTime = value; + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setCursorBlinkTime( value ); } ); + return true; + } ); + addEditableChoice( + panel, + { "indentTabCharacter", "editor.advanced", + mApp->i18n( "indent_tab_character", "Indent Tab Character" ), + mApp->i18n( "indent_tab_character_desc", + "Choose the character inserted when indenting with Tab." ) }, + { String( u8"»" ), String( u8"→" ), String( u8"⇒" ), String( u8"↪" ), String( u8"⇢" ), + String( u8"↣" ) }, + [this] { + return mApp->getConfig().editor.tabIndentCharacter.empty() + ? String( u8"»" ) + : String::fromUtf8( mApp->getConfig().editor.tabIndentCharacter ); + }, + [this]( const String& value ) { + if ( value.size() != 1 ) + return false; + mApp->getConfig().editor.tabIndentCharacter = value.toUtf8(); + mApp->getSplitter()->forEachEditor( [character = value[0]]( UICodeEditor* editor ) { + editor->setTabIndentCharacter( character ); + } ); + return true; + } ); addChoice( panel, { "indentTabAlignment", "editor.advanced", @@ -1244,20 +1488,38 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { mApp->getSplitter()->forEachEditor( [value]( UICodeEditor* editor ) { editor->setTabIndentAlignment( value ); } ); } ); - addAction( panel, - { "foldRefreshFrequency", "editor.advanced", - mApp->i18n( "folds_refresh_freq", "Folds Refresh Frequency" ), - mApp->i18n( "folds_refresh_freq_desc", - "Set how frequently code folding ranges are recalculated." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setFoldRefreshFreq(); } ); - addAction( panel, - { "tabOutCharacters", "editor.advanced", - mApp->i18n( "set_tab_out_characters", "Set Tab Out Characters" ), - mApp->i18n( "set_tab_out_characters_message", - "Set the characters the cursor can move past when pressing Tab." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setTabOutChars(); } ); + addText( + panel, + { "foldRefreshFrequency", "editor.advanced", + mApp->i18n( "folds_refresh_freq", "Folds Refresh Frequency" ), + mApp->i18n( + "folds_refresh_freq_desc", + "Set how frequently code folding ranges are recalculated (minimum 1 second)." ) }, + [this] { return mApp->getConfig().editor.codeFoldingRefreshFreq.toString(); }, + [this]( const std::string& text ) { + Time value; + if ( !parseNonNegativeTime( text, value ) || value < Seconds( 1 ) ) + return false; + mApp->getConfig().editor.codeFoldingRefreshFreq = value; + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setFoldsRefreshTime( value ); } ); + return true; + } ); + addText( + panel, + { "tabOutCharacters", "editor.advanced", + mApp->i18n( "set_tab_out_characters", "Set Tab Out Characters" ), + mApp->i18n( "set_tab_out_characters_message", + "Set the characters the cursor can move past when pressing Tab." ) }, + [this] { return mApp->getConfig().doc.tabOutChars; }, + [this]( const std::string& text ) { + mApp->getConfig().doc.tabOutChars = text; + String characters = String::fromUtf8( text ); + mApp->getSplitter()->forEachEditor( [&characters]( UICodeEditor* editor ) { + editor->getDocument().setTabOutChars( characters ); + } ); + return true; + } ); addCategory( panel, "window.screenshots", mApp->i18n( "window", "Window" ), mApp->i18n( "screenshots", "Screenshots" ) ); @@ -1267,20 +1529,40 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { mApp->i18n( "screenshot_save_path_desc", "Choose where screenshots are saved." ) }, mApp->i18n( "configure", "Configure..." ), [this] { mApp->getSettingsActions()->setScreenshotSavePath(); } ); - addAction( panel, - { "screenshotFilenamePattern", "window.screenshots", - mApp->i18n( "set_screenshot_filename_pattern", "Screenshot Filename Pattern" ), - mApp->i18n( "screenshot_filename_pattern_desc", - "Configure the timestamp-based screenshot filename pattern." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setScreenshotFilenamePattern(); } ); - addAction( + addText( + panel, + { "screenshotFilenamePattern", "window.screenshots", + mApp->i18n( "set_screenshot_filename_pattern", "Screenshot Filename Pattern" ), + mApp->i18n( "screenshot_filename_pattern_desc", + "Set the timestamp-based screenshot filename pattern." ) }, + [this] { return mApp->getConfig().screenshot.filenamePattern; }, + [this]( const std::string& pattern ) { + std::string filename = DateTimeController::formatCurrentDate( pattern ); + if ( !DateTimeController::isValidDateFormat( pattern ) || filename.empty() || + filename.find_first_of( "<>:\"/\\|?*" ) != std::string::npos ) + return false; + mApp->getConfig().screenshot.filenamePattern = pattern; + return true; + } ); + static const std::vector screenshotFormats{ "PNG", "JPG", "WEBP", "QOI", + "BMP", "TGA", "DDS" }; + auto screenshotFormat = String( mApp->getConfig().screenshot.saveFormat ).toUpper().toUtf8(); + auto selectedScreenshotFormat = std::find( screenshotFormats.begin(), screenshotFormats.end(), + String::fromUtf8( screenshotFormat ) ); + addChoice( panel, { "screenshotSaveFormat", "window.screenshots", mApp->i18n( "set_screenshot_save_format", "Screenshot Save Format" ), mApp->i18n( "screenshot_save_format_desc", "Choose the image format for screenshots." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->getSettingsActions()->setScreenshotSaveFormat(); } ); + screenshotFormats, + [selected = static_cast( + selectedScreenshotFormat == screenshotFormats.end() + ? 0 + : selectedScreenshotFormat - screenshotFormats.begin() )] { return selected; }, + [this]( size_t selected ) { + String format = screenshotFormats[std::min( selected, screenshotFormats.size() - 1 )]; + mApp->getConfig().screenshot.saveFormat = format.toLower().toUtf8(); + } ); addCategory( panel, "window.renderer", mApp->i18n( "window", "Window" ), mApp->i18n( "renderer", "Renderer" ) ); @@ -1296,17 +1578,19 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { "Vsync configuration changed.\nRestart ecode to see the changes." ) .unescape() ); } ); - addInteger( panel, - { "frameRateLimit", "window.renderer", - mApp->i18n( "frame_rate_limit", "Frame Rate Limit" ), - mApp->i18n( "frame_rate_limit_desc", - "Limit rendered frames per second. Set 0 to disable." ) }, - 0, 1000, &mApp->getConfig().context.FrameRateLimit, [this]( int value ) { - mApp->saveConfig(); - mApp->getWindow()->setFrameRateLimit( value ); - mApp->getNotificationCenter()->addNotification( - mApp->i18n( "frame_rate_limit_applied", "Frame Rate Limit Applied" ) ); - } ); + addInteger( + panel, + { "frameRateLimit", "window.renderer", mApp->i18n( "frame_rate_limit", "Frame Rate Limit" ), + mApp->i18n( "frame_rate_limit_desc", + "Limit rendered frames per second. Set 0 to disable." ) }, + 0, 1000, [this] { return mApp->getConfig().context.FrameRateLimit; }, + [this]( int value ) { + mApp->getConfig().context.FrameRateLimit = value; + mApp->saveConfig(); + mApp->getWindow()->setFrameRateLimit( value ); + mApp->getNotificationCenter()->addNotification( + mApp->i18n( "frame_rate_limit_applied", "Frame Rate Limit Applied" ) ); + } ); std::vector rendererVersions = Renderer::getAvailableGraphicsLibraryVersions(); std::vector rendererVersionNames; @@ -1427,13 +1711,18 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { "Set the shell executable and command-line arguments." ) }, mApp->i18n( "configure", "Configure..." ), [this] { mApp->runCommand( "configure-terminal-shell" ); } ); - addAction( panel, - { "terminalScrollback", "terminal.behavior", - mApp->i18n( "configure_terminal_scrollback", "Configure Terminal Scrollback" ), - mApp->i18n( "configure_terminal_scrollback_desc", - "Set the number of terminal history lines retained." ) }, - mApp->i18n( "configure", "Configure..." ), - [this] { mApp->runCommand( "configure-terminal-scrollback" ); } ); + addInteger( + panel, + { "terminalScrollback", "terminal.behavior", + mApp->i18n( "terminal_scrollback", "Terminal Scrollback" ), + mApp->i18n( "configure_terminal_scrollback_desc", + "Set the number of terminal history lines retained." ) }, + 0, std::numeric_limits::max(), + [this] { + return static_cast( std::min( mApp->getConfig().term.scrollback, + std::numeric_limits::max() ) ); + }, + [this]( int value ) { mApp->getConfig().term.scrollback = static_cast( value ); } ); addAction( panel, { "terminalWorkingDirectory", "terminal.behavior", mApp->i18n( "configure_terminal_working_dir", @@ -1558,7 +1847,10 @@ void SettingsPanel::addProjectSettings( PanelState& panel ) { mApp->i18n( "use_global_settings", "Use Global Settings" ), mApp->i18n( "use_global_settings_desc", "Inherit document defaults from the user settings." ) }, - &mApp->getProjectConfig().useGlobalSettings ); + &mApp->getProjectConfig().useGlobalSettings, [this, &panel]( bool useGlobalSettings ) { + setCategoryEnabled( panel, "editor.document", !useGlobalSettings, + "useGlobalSettings" ); + } ); auto addProjectBool = [this, &panel]( std::string id, const char* nameKey, const char* name, const char* descriptionKey, const char* description, bool DocumentConfig::* member ) { @@ -1602,22 +1894,27 @@ void SettingsPanel::addProjectSettings( PanelState& panel ) { addProjectBool( "writeUnicodeBOM", "write_unicode_bom", "Write Unicode BOM", "write_unicode_bom_desc", "Write a Unicode byte-order mark when saving.", &DocumentConfig::writeUnicodeBOM ); - addInteger( panel, - { "indentWidth", "editor.document", mApp->i18n( "indent_width", "Indent Width" ), - mApp->i18n( "indent_width_desc", - "Number of columns inserted for one indentation level." ) }, - 1, 16, &mApp->getProjectConfig().doc.indentWidth ); + addInteger( + panel, + { "indentWidth", "editor.document", mApp->i18n( "indent_width", "Indent Width" ), + mApp->i18n( "indent_width_desc", + "Number of columns inserted for one indentation level." ) }, + 1, 16, [this] { return mApp->getProjectConfig().doc.indentWidth; }, + [this]( int value ) { mApp->getProjectConfig().doc.indentWidth = value; } ); addInteger( panel, { "tabWidth", "editor.document", mApp->i18n( "tab_width", "Tab Width" ), mApp->i18n( "tab_width_desc", "Number of columns used to display a tab character." ) }, - 1, 16, &mApp->getProjectConfig().doc.tabWidth ); - addInteger( panel, - { "lineBreakingColumn", "editor.document", - mApp->i18n( "line_breaking_column", "Line Breaking Column" ), - mApp->i18n( "line_breaking_column_desc", - "Column used for wrapping and the editor width guide." ) }, - 0, 1000, &mApp->getProjectConfig().doc.lineBreakingColumn ); + 1, 16, [this] { return mApp->getProjectConfig().doc.tabWidth; }, + [this]( int value ) { mApp->getProjectConfig().doc.tabWidth = value; } ); + addInteger( + panel, + { "lineBreakingColumn", "editor.document", + mApp->i18n( "line_breaking_column", "Line Breaking Column" ), + mApp->i18n( "line_breaking_column_desc", + "Column used for wrapping and the editor width guide." ) }, + 0, 1000, [this] { return mApp->getProjectConfig().doc.lineBreakingColumn; }, + [this]( int value ) { mApp->getProjectConfig().doc.lineBreakingColumn = value; } ); addChoice( panel, { "lineEndings", "editor.document", mApp->i18n( "line_endings", "Line Endings" ), @@ -1639,6 +1936,8 @@ void SettingsPanel::addProjectSettings( PanelState& panel ) { ? TextFormat::LineEnding::CRLF : ( selected == 2 ? TextFormat::LineEnding::CR : TextFormat::LineEnding::LF ); } ); + setCategoryEnabled( panel, "editor.document", !mApp->getProjectConfig().useGlobalSettings, + "useGlobalSettings" ); addCategory( panel, "languages.file_associations", mApp->i18n( "languages", "Languages" ), mApp->i18n( "file_associations", "File Associations" ) ); diff --git a/src/tools/ecode/settingspanel.hpp b/src/tools/ecode/settingspanel.hpp index abfa37a59..470a77712 100644 --- a/src/tools/ecode/settingspanel.hpp +++ b/src/tools/ecode/settingspanel.hpp @@ -67,12 +67,21 @@ class SettingsPanel { void addChoice( PanelState& state, SettingBinding binding, const std::vector& choices, std::function get, std::function set, std::vector choiceDescriptions = {} ); - void addInteger( PanelState& state, SettingBinding binding, int min, int max, int* value, - std::function apply = {} ); + void addEditableChoice( PanelState& state, SettingBinding binding, + const std::vector& choices, std::function get, + std::function set ); + void addInteger( PanelState& state, SettingBinding binding, int min, int max, + std::function get, std::function set ); + void addText( PanelState& state, SettingBinding binding, std::function get, + std::function set ); + void addFloat( PanelState& state, SettingBinding binding, double min, double max, double step, + std::function get, std::function set ); void addAction( PanelState& state, SettingBinding binding, const String& buttonText, std::function action ); UIWidget* createRow( PanelState& state, SettingBinding& binding ); UICheckBox* createBoolControl( PanelState& state, SettingBinding& binding ); + void setCategoryEnabled( PanelState& state, const std::string& category, bool enabled, + const std::string& excludedSetting = {} ); void filter( PanelState& state ); }; diff --git a/src/tools/ecode/terminalmanager.cpp b/src/tools/ecode/terminalmanager.cpp index 9baa26c21..5a422e830 100644 --- a/src/tools/ecode/terminalmanager.cpp +++ b/src/tools/ecode/terminalmanager.cpp @@ -170,7 +170,7 @@ void TerminalManager::setUseFrameBuffer( bool useFrameBuffer ) { void TerminalManager::configureTerminalShell() { static const auto layout( R"xml( - + @@ -265,7 +265,7 @@ void TerminalManager::configureTerminalScrollback() { void TerminalManager::configureTerminalWorkingDir() { static const auto layout( R"xml( -