ecode: add structured settings pages for plugins

- introduce reusable settings models, documents, and plugin settings pages
  - expose all registered plugin configuration options in the Settings panel
  - support groups, choices, editable values, lists, secrets, and JSON objects
  - stage plugin changes and persist them when the Settings window closes
  - preserve raw JSON access for advanced configuration
  - safely migrate legacy AI provider API keys
  - add translated plugin setting labels and descriptions
  - remove the obsolete plugin GUI configuration API
  - fix autocomplete signature documentation persistence
  - fix AI model catalog refresh interval loading
This commit is contained in:
Martín Lucas Golini
2026-08-26 01:37:28 -03:00
parent 42d9dd6473
commit a182878837
35 changed files with 1409 additions and 63 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ const char DOC_FIND_REPLACE_CSS[] = R"css(
}
.ce_find_replace_box .input-find.error,
.ce_find_replace_box .input-replace.error {
border-color: #ff4040;
border-color: var(--theme-error);
}
)css";
+1 -1
View File
@@ -82,7 +82,7 @@ StatusBar > #doc_info {
}
#search_find.error,
#search_replace.error {
border-color: #ff4040;
border-color: var(--theme-error);
}
TableView#locate_bar_table > tableview::row > tableview::cell:nth-child(2),
TableView#locate_bar_table > tableview::row > tableview::cell:nth-child(3) {
@@ -4,6 +4,7 @@
#include "../../appconfig.hpp"
#include "../../notificationcenter.hpp"
#include "../../settingspage.hpp"
#include "../../widgetcommandexecuter.hpp"
#include <eepp/system/filesystem.hpp>
@@ -13,6 +14,66 @@ using json = nlohmann::json;
namespace ecode {
void AIAssistantPlugin::registerSettings( SettingsPage& page ) {
auto general = i18n( "general", "General" );
page.addGroup( general );
page.addBool( "display-reasoning", "/config/display_reasoning",
i18n( "aiassistant_display_reasoning", "Display Reasoning" ),
i18n( "aiassistant_display_reasoning_desc",
"Display reasoning content returned by supported models." ),
false, general );
page.addBool( "model-catalog-enabled", "/config/model_catalog_enabled",
i18n( "aiassistant_model_catalog_enabled", "Enable Model Catalog" ),
i18n( "aiassistant_model_catalog_enabled_desc",
"Download model metadata from the configured catalog." ),
true, general );
page.addText(
"model-catalog-url", "/config/model_catalog_url",
i18n( "aiassistant_model_catalog_url", "Model Catalog URL" ),
i18n( "aiassistant_model_catalog_url_desc",
"URL used to download model catalog metadata." ),
"https://models.dev/api.json", []( const std::string& value ) { return !value.empty(); },
false, general );
page.addInteger(
"model-catalog-refresh-hours", "/config/model_catalog_refresh_hours",
i18n( "aiassistant_model_catalog_refresh_hours", "Model Catalog Refresh Interval" ),
i18n( "aiassistant_model_catalog_refresh_hours_desc",
"Hours between model catalog refreshes." ),
1, 8760, 24, general );
auto apiKeys = i18n( "aiassistant_api_keys", "API Keys" );
page.addGroup( apiKeys );
struct Provider {
const char* id;
const char* name;
const char* legacyKey;
};
static constexpr Provider providers[] = {
{ "openai", "OpenAI", "openai_api_key" },
{ "anthropic", "Anthropic", "anthropic_api_key" },
{ "google", "Google AI", "google_ai_api_key" },
{ "deepseek", "DeepSeek", "deepseek_api_key" },
{ "mistral", "Mistral", "mistral_api_key" },
{ "xai", "xAI", "xai_api_key" },
{ "github", "GitHub Models", "github_api_key" },
{ "perplexity", "Perplexity", "perplexity_api_key" },
{ "openrouter", "OpenRouter", "openrouter_api_key" },
{ "moonshotai", "Moonshot AI", "moonshot_api_key" },
{ "nvidia", "NVIDIA", "nvidia_api_key" },
{ "togetherai", "Together AI", "together_api_key" },
{ "xiaomi", "Xiaomi MiMo", "mimo_api_key" },
};
for ( const auto& provider : providers ) {
page.addSecret(
std::string{ "api-key-" } + provider.id,
std::string{ "/config/api_keys/" } + provider.id,
i18n( std::string{ "aiassistant_api_key_" } + provider.id,
String::fromUtf8( std::string_view{ provider.name } ) + " API Key" ),
i18n( "aiassistant_api_key_desc", "Credential used to access this model provider." ),
{}, apiKeys, std::string{ "/config/" } + provider.legacyKey );
}
}
static std::initializer_list<std::string> AIAssistantUnlockedCommandList = {
"new-ai-assistant"
@@ -285,7 +346,7 @@ void AIAssistantPlugin::load( PluginManager* pluginManager ) {
userConfig["model_catalog_url"].is_string() )
catalogSettings.url = userConfig["model_catalog_url"].get<std::string>();
if ( userConfig.contains( "model_catalog_refresh_hours" ) &&
userConfig["model_catalog_refresh_hours"].is_number_unsigned() )
userConfig["model_catalog_refresh_hours"].is_number_integer() )
catalogSettings.refreshIntervalHours =
userConfig["model_catalog_refresh_hours"].get<std::uint32_t>();
}
@@ -40,7 +40,12 @@ class AIAssistantPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
const LLMProviders& getProviders() { return mProviders; }
const ACPAgents& getAgents() { return mAgents; }
std::string getPluginStatePath() const;
@@ -1,4 +1,5 @@
#include "autocompleteplugin.hpp"
#include "../../settingspage.hpp"
#include "../../universallocator.hpp"
#include <eepp/graphics/primitives.hpp>
#include <eepp/graphics/text.hpp>
@@ -25,6 +26,55 @@ using namespace std::literals;
namespace ecode {
void AutoCompletePlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addBool(
"suggestions-syntax-highlight", "/config/suggestions_syntax_highlight",
i18n( "autocomplete_suggestions_syntax_highlight", "Syntax Highlight Suggestions" ),
i18n( "autocomplete_suggestions_syntax_highlight_desc",
"Apply syntax highlighting to completion suggestions." ),
true );
page.addInteger( "max-label-characters", "/config/max_label_characters",
i18n( "autocomplete_max_label_characters", "Maximum Label Characters" ),
i18n( "autocomplete_max_label_characters_desc",
"Maximum number of characters shown in a suggestion label." ),
1, 10000, 100 );
auto cssLength = []( const std::string& text ) { return StyleSheetLength::isLength( text ); };
page.addText( "max-suggestion-documentation-width",
"/config/max_suggestion_documentation_width",
i18n( "autocomplete_max_suggestion_documentation_width",
"Maximum Suggestion Documentation Width" ),
i18n( "autocomplete_max_suggestion_documentation_width_desc",
"Maximum documentation popup width as a CSS length." ),
"100%", cssLength );
page.addText( "max-signature-helper-width", "/config/max_signature_helper_width",
i18n( "autocomplete_max_signature_helper_width", "Maximum Signature Help Width" ),
i18n( "autocomplete_max_signature_helper_width_desc",
"Maximum signature help popup width as a CSS length." ),
"90%", cssLength );
page.addBool( "signature-help-multi-line", "/config/signature_help_multi_line",
i18n( "autocomplete_signature_help_multi_line", "Multiline Signature Help" ),
i18n( "autocomplete_signature_help_multi_line_desc",
"Allow signature help to use multiple lines." ),
true );
page.addBool( "suggestion-documentation", "/config/suggestion_documentation",
i18n( "autocomplete_suggestion_documentation", "Suggestion Documentation" ),
i18n( "autocomplete_suggestion_documentation_desc",
"Show documentation alongside completion suggestions." ),
true );
page.addBool(
"signature-help-documentation", "/config/signature_help_documentation",
i18n( "autocomplete_signature_help_documentation", "Signature Help Documentation" ),
i18n( "autocomplete_signature_help_documentation_desc",
"Show documentation alongside signature help." ),
true );
page.addBool( "load-vscode-snippets", "/config/load_vscode_snippets",
i18n( "autocomplete_load_vscode_snippets", "Load VS Code Snippets" ),
i18n( "autocomplete_load_vscode_snippets_desc",
"Load compatible snippets installed for VS Code." ),
true );
}
class SnippetLocatorModel : public Model {
public:
struct Row {
@@ -380,7 +430,7 @@ void AutoCompletePlugin::load( PluginManager* pluginManager ) {
if ( config.contains( "signature_help_documentation" ) )
mSignatureHelpDocumentation = config.value( "signature_help_documentation", true );
else {
config["suggestion_documentation"] = mSignatureHelpDocumentation;
config["signature_help_documentation"] = mSignatureHelpDocumentation;
updateConfigFile = true;
}
@@ -95,21 +95,38 @@ class AutoCompletePlugin : public Plugin {
bool isReady() const override { return true; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
void onRegister( UICodeEditor* ) override;
void onUnregister( UICodeEditor* ) override;
bool onKeyDown( UICodeEditor*, const KeyEvent& ) override;
bool onTextInput( UICodeEditor*, const TextInputEvent& ) override;
void update( UICodeEditor* ) override;
void postDraw( UICodeEditor*, const Vector2f& startScroll, const Float& lineHeight,
const TextPosition& cursor ) override;
void drawBeforeLineText( UICodeEditor*, const Int64&, Vector2f, const Float&,
const Float& ) override;
bool onMouseDown( UICodeEditor*, const Vector2i&, const Uint32& ) override;
bool onMouseUp( UICodeEditor*, const Vector2i&, const Uint32& ) override;
bool onMouseDoubleClick( UICodeEditor*, const Vector2i&, const Uint32& ) override;
bool onMouseMove( UICodeEditor*, const Vector2i&, const Uint32& ) override;
void onFileSystemEvent( const FileEvent&, const FileInfo& ) override;
FileSystemListenerOptions getFileSystemListenerOptions() const override;
void onLoadProject( const std::string& projectFolder,
const std::string& projectStatePath ) override;
@@ -1,6 +1,7 @@
#include "debuggerplugin.hpp"
#include "../../jsonhelper.hpp"
#include "../../notificationcenter.hpp"
#include "../../settingspage.hpp"
#include "../../terminalmanager.hpp"
#include "../../uistatusbar.hpp"
#include "../../widgetcommandexecuter.hpp"
@@ -30,6 +31,27 @@ static constexpr auto REQUEST_TYPE_ATTACH = "attach";
namespace ecode {
void DebuggerPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addBool(
"fetch-registers", "/config/fetch_registers",
i18n( "debugger_fetch_registers", "Fetch Registers" ),
i18n( "debugger_fetch_registers_desc", "Request processor registers while debugging." ),
false );
page.addBool(
"fetch-globals", "/config/fetch_globals",
i18n( "debugger_fetch_globals", "Fetch Global Variables" ),
i18n( "debugger_fetch_globals_desc", "Request global variables while debugging." ), false );
page.addBool( "silent", "/config/silent", i18n( "debugger_silent", "Silent Debugger Logs" ),
i18n( "debugger_silent_desc", "Hide non-critical debugger log messages." ),
true );
page.addBool( "load-vscode-launch-config", "/config/load_vscode_launch_config",
i18n( "debugger_load_vscode_launch_config", "Load VS Code Launch Configuration" ),
i18n( "debugger_load_vscode_launch_config_desc",
"Load compatible launch configurations from .vscode/launch.json." ),
true );
}
static constexpr auto INPUT_PATTERN = "%$%{input%:([%w_]+)%}"sv;
static constexpr auto ENV_PATTERN = "%$%{env%:([%w_]+)%}"sv;
static constexpr auto COMMAND_PATTERN = "%$%{command%:([%w_]+)%}"sv;
@@ -67,6 +67,10 @@ class DebuggerPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
void onSaveState( IniFile* state ) override;
void onSaveProject( const std::string& projectFolder, const std::string& projectStatePath,
@@ -150,16 +154,27 @@ class DebuggerPlugin : public PluginBase {
mDoc( doc ), mParent( parent ) {}
virtual void onDocumentTextChanged( const DocumentContentChange& ) {}
virtual void onDocumentUndoRedo( const TextDocument::UndoRedo& ) {}
virtual void onDocumentCursorChange( const TextPosition& ) {}
virtual void onDocumentSelectionChange( const TextRange& ) {}
virtual void onDocumentLineCountChange( const size_t&, const size_t& ) {}
virtual void onDocumentLineChanged( const Int64& ) {}
virtual void onDocumentSaved( TextDocument* ) {}
virtual void onDocumentClosed( TextDocument* doc ) { onDocumentReset( doc ); }
virtual void onDocumentDirtyOnFileSystem( TextDocument* ) {}
virtual void onDocumentMoved( TextDocument* ) {}
virtual void onDocumentReset( TextDocument* ) {}
Client::Type getTextDocumentClientType() { return TextDocument::Client::Auxiliary; }
virtual void onDocumentLineMove( const Int64& fromLine, const Int64& toLine,
@@ -1,4 +1,5 @@
#include "discordRPCplugin.hpp"
#include "../../settingspage.hpp"
using json = nlohmann::json;
@@ -6,6 +7,34 @@ namespace ecode {
static const auto DEFAULT_CLIENT_ID = "1339026777158455336";
static const auto DebounceUniqueId = String::hash( "DiscordRPCplugin::debounce" );
void DiscordRPCplugin::registerSettings( SettingsPage& page ) {
auto general = i18n( "general", "General" );
page.addGroup( general );
page.addText( "application-id", "/config/appID",
i18n( "discordrpc_application_id", "Discord Application ID" ),
i18n( "discordrpc_application_id_desc",
"Discord application identifier used for Rich Presence." ),
DEFAULT_CLIENT_ID, {}, false, general );
page.addBool( "language-icons", "/config/doLanguageIcons",
i18n( "discordrpc_language_icons", "Language Icons" ),
i18n( "discordrpc_language_icons_desc",
"Display an icon for the active document language." ),
true, general );
page.addBool( "git-integration", "/config/doGitIntegration",
i18n( "discordrpc_git_integration", "Git Integration" ),
i18n( "discordrpc_git_integration_desc",
"Include repository information in Rich Presence." ),
true, general );
auto advanced = i18n( "advanced", "Advanced" );
page.addGroup( advanced );
page.addJsonObject( "icon-bindings", "/config/iconBindings",
i18n( "discordrpc_icon_bindings", "Language Icon Bindings" ),
i18n( "discordrpc_icon_bindings_desc",
"JSON object mapping language identifiers to Discord image assets." ),
nlohmann::json::object(), advanced );
}
static const std::vector<std::string> DiscordRPCCommandList = { "discordrpc-reconnect" };
static const auto DISCORDRPC_DEFAULT_ICON =
@@ -43,6 +43,10 @@ class DiscordRPCplugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
protected:
DiscordIPC mIPC;
Mutex mDataMutex;
@@ -1,5 +1,6 @@
#include "formatterplugin.hpp"
#include "../../notificationcenter.hpp"
#include "../../settingspage.hpp"
#include <eepp/system/filesystem.hpp>
#include <eepp/system/iostreamstring.hpp>
#include <eepp/system/lock.hpp>
@@ -18,6 +19,15 @@ using json = nlohmann::json;
namespace ecode {
void FormatterPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addBool( "auto-format-on-save", "/config/auto_format_on_save",
i18n( "formatter_auto_format_on_save", "Format on Save" ),
i18n( "formatter_auto_format_on_save_desc",
"Automatically format supported documents when saving." ),
false );
}
Plugin* FormatterPlugin::New( PluginManager* pluginManager ) {
return eeNew( FormatterPlugin, ( pluginManager, false ) );
}
@@ -50,6 +50,10 @@ class FormatterPlugin : public Plugin {
std::string getDescription() { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
virtual String::HashType getConfigFileHash() { return mConfigHash; }
void onRegister( UICodeEditor* );
+40
View File
@@ -1,4 +1,5 @@
#include "gitplugin.hpp"
#include "../../settingspage.hpp"
#include "gitbranchmodel.hpp"
#include "gitstatusmodel.hpp"
#include <eepp/graphics/image.hpp>
@@ -33,6 +34,45 @@ using json = nlohmann::json;
namespace ecode {
void GitPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addText( "ui-refresh-frequency", "/config/ui_refresh_frequency",
i18n( "git_ui_refresh_frequency", "Refresh Frequency" ),
i18n( "git_ui_refresh_frequency_desc",
"How often Git information in the interface is refreshed." ),
mRefreshFreq.toString(), []( const std::string& text ) {
Time value;
return SettingsPage::parseNonNegativeSettingsTime( text, value );
} );
page.addBool( "statusbar-display-branch", "/config/statusbar_display_branch",
i18n( "git_statusbar_display_branch", "Show Branch in Status Bar" ),
i18n( "git_statusbar_display_branch_desc",
"Display the current Git branch in the status bar." ),
true );
page.addBool( "filetree-highlight-changes", "/config/filetree_highlight_changes",
i18n( "git_filetree_highlight_changes", "Highlight File Tree Changes" ),
i18n( "git_filetree_highlight_changes_desc",
"Highlight files with Git changes in the file tree." ),
true );
page.addText( "filetree-highlight-style-color", "/config/filetree_highlight_style_color",
i18n( "git_filetree_highlight_style_color", "File Tree Highlight Color" ),
i18n( "git_filetree_highlight_style_color_desc",
"CSS color used to highlight changed files." ),
"var(--font-highlight)" );
page.addBool( "statusbar-display-modifications", "/config/statusbar_display_modifications",
i18n( "git_statusbar_display_modifications", "Show Modifications in Status Bar" ),
i18n( "git_statusbar_display_modifications_desc",
"Display repository modification counts in the status bar." ),
true );
page.addBool( "status-recurse-submodules", "/config/status_recurse_submodules",
i18n( "git_status_recurse_submodules", "Recurse into Submodules" ),
i18n( "git_status_recurse_submodules_desc",
"Include submodule changes when collecting repository status." ),
true );
page.addBool( "silent", "/config/silent", i18n( "git_silent", "Silent Git Logs" ),
i18n( "git_silent_desc", "Hide non-critical Git plugin log messages." ), true );
}
static constexpr auto DEFAULT_HIGHLIGHT_COLOR = "var(--font-highlight)"sv;
static constexpr auto GIT_STATUS_UPDATE_TAG = String::hash( "git::status-update" );
@@ -52,6 +52,10 @@ class GitPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
void onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) override;
FileSystemListenerOptions getFileSystemListenerOptions() const override;
@@ -1,5 +1,6 @@
#include "linterplugin.hpp"
#include "../../notificationcenter.hpp"
#include "../../settingspage.hpp"
#include <algorithm>
#include <eepp/graphics/primitives.hpp>
#include <eepp/graphics/text.hpp>
@@ -30,6 +31,41 @@ using json = nlohmann::json;
namespace ecode {
void LinterPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addText( "delay-time", "/config/delay_time", i18n( "linter_delay_time", "Lint Delay" ),
i18n( "linter_delay_time_desc", "Time to wait before linting after an edit." ),
getDelayTime().toString(), []( const std::string& text ) {
Time value;
return SettingsPage::parseNonNegativeSettingsTime( text, value );
} );
page.addBool( "enable-lsp-diagnostics", "/config/enable_lsp_diagnostics",
i18n( "linter_enable_lsp_diagnostics", "Language Server Diagnostics" ),
i18n( "linter_enable_lsp_diagnostics_desc",
"Display diagnostics reported by language servers." ),
true );
page.addBool( "enable-error-lens", "/config/enable_error_lens",
i18n( "linter_enable_error_lens", "Error Lens" ),
i18n( "linter_enable_error_lens_desc",
"Display diagnostic messages inline in the editor." ),
true );
page.addStringList(
"disable-lsp-languages", "/config/disable_lsp_languages",
i18n( "linter_disable_lsp_languages", "Disable LSP Diagnostic Languages" ),
i18n( "linter_disable_lsp_languages_desc",
"Comma-separated language identifiers where LSP diagnostics are disabled." ) );
page.addStringList(
"disable-languages", "/config/disable_languages",
i18n( "linter_disable_languages", "Disable Linter Languages" ),
i18n( "linter_disable_languages_desc",
"Comma-separated language identifiers where linters are disabled." ) );
page.addBool( "goto-ignore-warnings", "/config/goto_ignore_warnings",
i18n( "linter_goto_ignore_warnings", "Ignore Warnings When Navigating" ),
i18n( "linter_goto_ignore_warnings_desc",
"Skip warnings when navigating between diagnostics." ),
false );
}
Plugin* LinterPlugin::New( PluginManager* pluginManager ) {
return eeNew( LinterPlugin, ( pluginManager, false ) );
}
@@ -1145,9 +1181,8 @@ void LinterPlugin::drawAfterLineText( UICodeEditor* editor, const Int64& index,
Color wcolor(
editor->getColorScheme().getEditorSyntaxStyle( "warning"_sst ).color );
if ( mLightbulbIcon == nullptr )
mLightbulbIcon =
editor->getUISceneNode()->getUIIconThemeManager()->findIcon(
"lightbulb-autofix" );
mLightbulbIcon = editor->getUISceneNode()->getUIIconThemeManager()->findIcon(
"lightbulb-autofix" );
if ( nullptr != mLightbulbIcon ) {
const int iconSize = (int)eefloor( lineHeight );
Drawable* drawable = mLightbulbIcon->getSource( iconSize ).get();
@@ -76,6 +76,10 @@ class LinterPlugin : public Plugin {
std::string getDescription() { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
virtual String::HashType getConfigFileHash() { return mConfigHash; }
void onRegister( UICodeEditor* );
@@ -1,5 +1,6 @@
#include "lspclientplugin.hpp"
#include "../../notificationcenter.hpp"
#include "../../settingspage.hpp"
#include "../../version.hpp"
#include <eepp/graphics/primitives.hpp>
#include <eepp/system/filesystem.hpp>
@@ -26,6 +27,52 @@ using json = nlohmann::json;
namespace ecode {
void LSPClientPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addText( "hover-delay", "/config/hover_delay", i18n( "lsp_hover_delay", "Hover Delay" ),
i18n( "lsp_hover_delay_desc",
"Time to wait before showing language server information while hovering." ),
mHoverDelay.toString(), []( const std::string& text ) {
Time value;
return SettingsPage::parseNonNegativeSettingsTime( text, value );
} );
page.addText( "server-close-after-idle-time", "/config/server_close_after_idle_time",
i18n( "lsp_server_close_after_idle_time", "Server Close After Idle Time" ),
i18n( "lsp_server_close_after_idle_time_desc",
"Time to keep an unused language server running before closing it." ),
mClientManager.getLSPDecayTime().toString(), []( const std::string& text ) {
Time value;
return SettingsPage::parseNonNegativeSettingsTime( text, value );
} );
page.addBool( "silent", "/config/silent", i18n( "lsp_silent", "Silent Language Server Logs" ),
i18n( "lsp_silent_desc", "Hide non-critical language server log messages." ),
mSilence );
page.addBool(
"trim-logs", "/config/trim_logs", i18n( "lsp_trim_logs", "Trim Language Server Logs" ),
i18n( "lsp_trim_logs_desc", "Limit each language server log line to 1 KiB." ), mTrimLogs );
page.addBool( "semantic-highlighting", "/config/semantic_highlighting",
i18n( "lsp_semantic_highlighting", "Semantic Highlighting" ),
i18n( "lsp_semantic_highlighting_desc",
"Use semantic tokens provided by language servers when available." ),
mSemanticHighlighting );
page.addStringList(
"disable-semantic-highlighting-lang", "/config/disable_semantic_highlighting_lang",
i18n( "lsp_disable_semantic_highlighting_lang", "Disable Semantic Highlighting Languages" ),
i18n( "lsp_disable_semantic_highlighting_lang_desc",
"Comma-separated language identifiers where semantic highlighting is disabled." ) );
page.addBool( "breadcrumb-navigation", "/config/breadcrumb_navigation",
i18n( "lsp_breadcrumb_navigation", "Breadcrumb Navigation" ),
i18n( "lsp_breadcrumb_navigation_desc",
"Display language-aware breadcrumb navigation above the editor." ),
mBreadcrumb );
page.addText(
"breadcrumb-height", "/config/breadcrumb_height",
i18n( "lsp_breadcrumb_height", "Breadcrumb Height" ),
i18n( "lsp_breadcrumb_height_desc", "Height of the editor breadcrumb using a CSS length." ),
mBreadcrumbHeight.toString(),
[]( const std::string& text ) { return StyleSheetLength::isLength( text ); } );
}
static Action::UniqueID getMouseMoveHash( UICodeEditor* editor ) {
return hashCombine( String::hash( "LSPClientPlugin::onMouseMove-" ),
reinterpret_cast<Action::UniqueID>( editor ) );
@@ -1982,8 +2029,8 @@ void LSPClientPlugin::drawTop( UICodeEditor* editor, const Vector2f& screenStart
Vector2f iconPos( { pos.x, screenStart.y + textOffsetY +
eefloor( ( textHeight - iconHeight ) * 0.5f ) } );
separatorDrawable->draw( iconPos );
pos.x +=
separatorDrawable->getPixelsSize().getWidth() + eefloor( PixelDensity::dpToPx( 8 ) );
pos.x += separatorDrawable->getPixelsSize().getWidth() +
eefloor( PixelDensity::dpToPx( 8 ) );
separatorDrawable->setColor( c );
} else {
pos.x += eefloor( PixelDensity::dpToPx( 16 ) );
@@ -46,6 +46,10 @@ class LSPClientPlugin : public Plugin {
virtual String::HashType getConfigFileHash() { return mConfigHash; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page );
void onRegister( UICodeEditor* );
void onUnregister( UICodeEditor* );
+5
View File
@@ -26,6 +26,7 @@ namespace ecode {
class PluginManager;
class PluginContextProvider;
class SettingsPage;
class Plugin : public UICodeEditorPlugin {
public:
@@ -45,6 +46,10 @@ class Plugin : public UICodeEditorPlugin {
virtual std::string getFileConfigPath();
virtual bool hasSettingsPage() const { return false; }
virtual void registerSettings( SettingsPage& ) {}
PluginManager* getManager() const;
PluginContextProvider* getPluginContext() const;
@@ -1,4 +1,5 @@
#include "spellcheckerplugin.hpp"
#include "../../settingspage.hpp"
#include "eepp/ui/abstract/uiabstractview.hpp"
#include "eepp/ui/models/itemlistmodel.hpp"
#include "eepp/window/engine.hpp"
@@ -15,6 +16,18 @@ using json = nlohmann::json;
namespace ecode {
void SpellCheckerPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addText( "delay-time", "/config/delay_time",
i18n( "spellchecker_delay_time", "Spell Check Delay" ),
i18n( "spellchecker_delay_time_desc",
"Time to wait before checking spelling after an edit." ),
getDelayTime().toString(), []( const std::string& text ) {
Time value;
return SettingsPage::parseNonNegativeSettingsTime( text, value );
} );
}
static constexpr auto SPELL_CHECKER_CMD = "typos";
static constexpr auto SPELL_CHECKER_ARGS = "--format=brief";
@@ -39,6 +39,10 @@ class SpellCheckerPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
void drawAfterLineText( UICodeEditor* editor, const Int64& index, Vector2f position,
const Float& fontSize, const Float& lineHeight ) override;
@@ -98,7 +102,8 @@ class SpellCheckerPlugin : public PluginBase {
void invalidateEditors( TextDocument* doc );
std::optional<SpellCheckerMatch> getMatchFromTextPosition( UICodeEditor* editor, TextPosition pos );
std::optional<SpellCheckerMatch> getMatchFromTextPosition( UICodeEditor* editor,
TextPosition pos );
std::optional<SpellCheckerMatch> getMatchFromScreenPos( UICodeEditor* editor, Vector2f pos );
@@ -109,7 +114,6 @@ class SpellCheckerPlugin : public PluginBase {
void goToNextError( UICodeEditor* editor );
void goToPrevError( UICodeEditor* editor );
};
} // namespace ecode
@@ -1,4 +1,5 @@
#include "xmltoolsplugin.hpp"
#include "../../settingspage.hpp"
#include <eepp/graphics/primitives.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/scopedop.hpp>
@@ -8,6 +9,20 @@ using json = nlohmann::json;
namespace ecode {
void XMLToolsPlugin::registerSettings( SettingsPage& page ) {
page.addGroup( i18n( "general", "General" ) );
page.addBool( "highlight-match", "/config/highlight_match",
i18n( "xmltools_highlight_match", "Highlight Matching Tags" ),
i18n( "xmltools_highlight_match_desc",
"Highlight the matching XML or HTML tag at the cursor." ),
true );
page.addBool( "auto-edit-match", "/config/auto_edit_match",
i18n( "xmltools_auto_edit_match", "Edit Matching Tags" ),
i18n( "xmltools_auto_edit_match_desc",
"Update the matching tag automatically while editing." ),
true );
}
Plugin* XMLToolsPlugin::New( PluginManager* pluginManager ) {
return eeNew( XMLToolsPlugin, ( pluginManager, false ) );
}
@@ -36,6 +36,10 @@ class XMLToolsPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
void registerSettings( SettingsPage& page ) override;
bool getHighlightMatch() const;
bool getAutoEditMatch() const;
@@ -73,17 +77,29 @@ class XMLToolsPlugin : public PluginBase {
mDoc( doc ), mParent( parent ) {}
virtual void onDocumentTextChanged( const DocumentContentChange& );
virtual void onDocumentUndoRedo( const TextDocument::UndoRedo& ) {}
virtual void onDocumentCursorChange( const TextPosition& ) {}
virtual void onDocumentInterestingCursorChange( const TextPosition& ) {}
virtual void onDocumentSelectionChange( const TextRange& );
virtual void onDocumentLineCountChange( const size_t&, const size_t& ) {}
virtual void onDocumentLineChanged( const Int64& ) {}
virtual void onDocumentSaved( TextDocument* ) {}
virtual void onDocumentClosed( TextDocument* doc ) { onDocumentReset( doc ); }
virtual void onDocumentDirtyOnFileSystem( TextDocument* ) {}
virtual void onDocumentMoved( TextDocument* ) {};
virtual void onDocumentReset( TextDocument* ) { mSelections.clear(); }
Client::Type getTextDocumentClientType() { return TextDocument::Client::Auxiliary; }
protected:
+78
View File
@@ -0,0 +1,78 @@
#include "settingsdocument.hpp"
#include <eepp/system/filesystem.hpp>
using namespace EE::System;
namespace ecode {
SettingsDocument::SettingsDocument( std::string path, nlohmann::json data ) :
mPath( std::move( path ) ), mOriginal( data ), mData( std::move( data ) ) {}
std::shared_ptr<SettingsDocument> SettingsDocument::load( std::string path, std::string& error ) {
std::string contents;
if ( !FileSystem::fileGet( path, contents ) ) {
error = "Unable to read settings file";
return {};
}
try {
auto data = nlohmann::json::parse( contents, nullptr, true, true );
if ( !data.is_object() ) {
error = "Settings file root must be an object";
return {};
}
return std::shared_ptr<SettingsDocument>(
new SettingsDocument( std::move( path ), std::move( data ) ) );
} catch ( const nlohmann::json::exception& exception ) {
error = exception.what();
return {};
}
}
bool SettingsDocument::save() {
if ( !mDirty )
return true;
if ( !FileSystem::fileWrite( mPath, mData.dump( 2 ) ) )
return false;
mOriginal = mData;
mDirty = false;
return true;
}
nlohmann::json SettingsDocument::getValue( const std::string& pointer,
nlohmann::json fallback ) const {
try {
const auto path = nlohmann::json::json_pointer( pointer );
if ( mData.contains( path ) )
return mData.at( path );
} catch ( const nlohmann::json::exception& ) {
}
return fallback;
}
bool SettingsDocument::setValue( const std::string& pointer, nlohmann::json value ) {
try {
const auto path = nlohmann::json::json_pointer( pointer );
if ( mData.contains( path ) && mData.at( path ) == value )
return false;
mData[path] = std::move( value );
mDirty = true;
return true;
} catch ( const nlohmann::json::exception& ) {
return false;
}
}
bool SettingsDocument::contains( const std::string& pointer ) const {
try {
return mData.contains( nlohmann::json::json_pointer( pointer ) );
} catch ( const nlohmann::json::exception& ) {
return false;
}
}
void SettingsDocument::discard() {
mData = mOriginal;
mDirty = false;
}
} // namespace ecode
+62
View File
@@ -0,0 +1,62 @@
#ifndef ECODE_SETTINGSDOCUMENT_HPP
#define ECODE_SETTINGSDOCUMENT_HPP
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
namespace ecode {
class SettingsDocument {
public:
static std::shared_ptr<SettingsDocument> load( std::string path, std::string& error );
template <typename T> T get( const std::string& pointer, T fallback ) const {
try {
const auto path = nlohmann::json::json_pointer( pointer );
if ( mData.contains( path ) )
return mData.at( path ).get<T>();
} catch ( const nlohmann::json::exception& ) {
}
return fallback;
}
template <typename T> bool set( const std::string& pointer, T value ) {
try {
const auto path = nlohmann::json::json_pointer( pointer );
if ( mData.contains( path ) && mData.at( path ) == value )
return false;
mData[path] = std::move( value );
mDirty = true;
return true;
} catch ( const nlohmann::json::exception& ) {
return false;
}
}
nlohmann::json getValue( const std::string& pointer, nlohmann::json fallback ) const;
bool setValue( const std::string& pointer, nlohmann::json value );
bool contains( const std::string& pointer ) const;
bool save();
void discard();
bool isDirty() const { return mDirty; }
const std::string& path() const { return mPath; }
private:
SettingsDocument( std::string path, nlohmann::json data );
std::string mPath;
nlohmann::json mOriginal;
nlohmann::json mData;
bool mDirty{ false };
};
} // namespace ecode
#endif
+4
View File
@@ -52,6 +52,7 @@ struct TextSetting {
std::function<std::string()> get;
std::function<bool( const std::string& )> set;
bool commitOnFocusLoss{ false };
bool password{ false };
};
struct FloatSetting {
@@ -129,8 +130,11 @@ class SettingsModel {
}
const std::vector<SettingsCategory>& categories() const { return mCategories; }
const std::vector<SettingsGroup>& groups() const { return mGroups; }
std::vector<SettingDefinition>& settings() { return mSettings; }
const std::vector<SettingDefinition>& settings() const { return mSettings; }
private:
+245
View File
@@ -0,0 +1,245 @@
#include "settingspage.hpp"
namespace ecode {
bool SettingsPage::parseNonNegativeSettingsTime( 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;
}
SettingsPage::SettingsPage( SettingsModel& model, std::shared_ptr<SettingsDocument> document,
std::string category, std::string idPrefix ) :
mModel( model ),
mDocument( std::move( document ) ),
mCategory( std::move( category ) ),
mIdPrefix( std::move( idPrefix ) ) {}
bool SettingsPage::addGroup( String name ) {
return mModel.addGroup( { mCategory, std::move( name ), mModel.settings().size() } );
}
bool SettingsPage::addBool( std::string id, std::string jsonPointer, String name,
String description, bool defaultValue, String group ) {
if ( !mDocument )
return false;
auto document = mDocument;
return mModel.addSetting( { { mIdPrefix + "." + id, mCategory, std::move( name ),
std::move( description ), std::move( group ) },
BoolSetting{ [document, jsonPointer, defaultValue] {
return document->get<bool>( jsonPointer,
defaultValue );
},
[document, jsonPointer]( bool value ) {
document->set( jsonPointer, value );
} } } );
}
bool SettingsPage::addChoice( std::string id, std::string jsonPointer, String name,
String description, std::vector<String> choices,
std::vector<nlohmann::json> values, nlohmann::json defaultValue,
std::vector<String> choiceDescriptions, String group ) {
if ( !mDocument || choices.empty() || choices.size() != values.size() )
return false;
auto document = mDocument;
auto mappedValues = std::make_shared<const std::vector<nlohmann::json>>( std::move( values ) );
return mModel.addSetting(
{ { mIdPrefix + "." + id, mCategory, std::move( name ), std::move( description ),
std::move( group ) },
ChoiceSetting{
std::move( choices ), std::move( choiceDescriptions ),
[document, jsonPointer, mappedValues, defaultValue = std::move( defaultValue )] {
const auto current = document->getValue( jsonPointer, defaultValue );
auto found = std::find( mappedValues->begin(), mappedValues->end(), current );
return found == mappedValues->end()
? size_t{ 0 }
: static_cast<size_t>( std::distance( mappedValues->begin(), found ) );
},
[document, jsonPointer, mappedValues]( size_t selected ) {
if ( selected < mappedValues->size() )
document->setValue( jsonPointer, ( *mappedValues )[selected] );
} } } );
}
bool SettingsPage::addEditableChoice( std::string id, std::string jsonPointer, String name,
String description, std::vector<String> choices,
std::string defaultValue,
std::function<bool( const std::string& )> validate,
String group ) {
if ( !mDocument )
return false;
auto document = mDocument;
return mModel.addSetting(
{ { mIdPrefix + "." + id, mCategory, std::move( name ), std::move( description ),
std::move( group ) },
EditableChoiceSetting{
std::move( choices ),
[document, jsonPointer, defaultValue = std::move( defaultValue )] {
return String::fromUtf8(
document->get<std::string>( jsonPointer, defaultValue ) );
},
[document, jsonPointer, validate = std::move( validate )]( const String& value ) {
const std::string text = value.toUtf8();
if ( validate && !validate( text ) )
return false;
document->set( jsonPointer, text );
return true;
} } } );
}
bool SettingsPage::addInteger( std::string id, std::string jsonPointer, String name,
String description, int min, int max, int defaultValue,
String group ) {
if ( !mDocument || min > max )
return false;
auto document = mDocument;
return mModel.addSetting( { { mIdPrefix + "." + id, mCategory, std::move( name ),
std::move( description ), std::move( group ) },
IntegerSetting{ min, max,
[document, jsonPointer, defaultValue] {
return document->get<int>( jsonPointer,
defaultValue );
},
[document, jsonPointer]( int value ) {
document->set( jsonPointer, value );
} } } );
}
bool SettingsPage::addText( std::string id, std::string jsonPointer, String name,
String description, std::string defaultValue,
std::function<bool( const std::string& )> validate,
bool commitOnFocusLoss, String group ) {
if ( !mDocument )
return false;
auto document = mDocument;
return mModel.addSetting(
{ { mIdPrefix + "." + id, mCategory, std::move( name ), std::move( description ),
std::move( group ) },
TextSetting{ [document, jsonPointer, defaultValue = std::move( defaultValue )] {
return document->get<std::string>( jsonPointer, defaultValue );
},
[document, jsonPointer,
validate = std::move( validate )]( const std::string& value ) {
if ( validate && !validate( value ) )
return false;
document->set( jsonPointer, value );
return true;
},
commitOnFocusLoss } } );
}
bool SettingsPage::addSecret( std::string id, std::string jsonPointer, String name,
String description, std::string defaultValue, String group,
std::string legacyJsonPointer ) {
if ( !mDocument )
return false;
auto document = mDocument;
return mModel.addSetting(
{ { mIdPrefix + "." + id, mCategory, std::move( name ), std::move( description ),
std::move( group ) },
TextSetting{
[document, jsonPointer, legacyJsonPointer, defaultValue = std::move( defaultValue )] {
if ( !legacyJsonPointer.empty() && !document->contains( jsonPointer ) )
return document->get<std::string>( legacyJsonPointer, defaultValue );
return document->get<std::string>( jsonPointer, defaultValue );
},
[document, jsonPointer]( const std::string& value ) {
document->set( jsonPointer, value );
return true;
},
true, true } } );
}
bool SettingsPage::addJsonObject( std::string id, std::string jsonPointer, String name,
String description, nlohmann::json defaultValue, String group ) {
if ( !mDocument || !defaultValue.is_object() )
return false;
auto document = mDocument;
return mModel.addSetting(
{ { mIdPrefix + "." + id, mCategory, std::move( name ), std::move( description ),
std::move( group ) },
TextSetting{ [document, jsonPointer, defaultValue = std::move( defaultValue )] {
return document->getValue( jsonPointer, defaultValue ).dump();
},
[document, jsonPointer]( const std::string& value ) {
auto parsed = nlohmann::json::parse( value, nullptr, false );
if ( !parsed.is_object() )
return false;
document->setValue( jsonPointer, std::move( parsed ) );
return true;
},
true } } );
}
bool SettingsPage::addStringList( std::string id, std::string jsonPointer, String name,
String description, std::vector<std::string> defaultValue,
String group ) {
if ( !mDocument )
return false;
auto document = mDocument;
return mModel.addSetting(
{ { mIdPrefix + "." + id, mCategory, std::move( name ), std::move( description ),
std::move( group ) },
TextSetting{ [document, jsonPointer, defaultValue = std::move( defaultValue )] {
return String::join(
document->get<std::vector<std::string>>( jsonPointer, defaultValue ),
',' );
},
[document, jsonPointer]( const std::string& value ) {
std::vector<std::string> values;
for ( auto& item : String::split( value, "," ) ) {
String::trimInPlace( item );
if ( !item.empty() )
values.emplace_back( std::move( item ) );
}
document->set( jsonPointer, std::move( values ) );
return true;
},
true } } );
}
bool SettingsPage::addFloat( std::string id, std::string jsonPointer, String name,
String description, double min, double max, double step,
double defaultValue, String group ) {
if ( !mDocument || min > max || step <= 0 )
return false;
auto document = mDocument;
return mModel.addSetting( { { mIdPrefix + "." + id, mCategory, std::move( name ),
std::move( description ), std::move( group ) },
FloatSetting{ min, max, step,
[document, jsonPointer, defaultValue] {
return document->get<double>( jsonPointer,
defaultValue );
},
[document, jsonPointer]( double value ) {
document->set( jsonPointer, value );
} } } );
}
bool SettingsPage::addAction( std::string id, String name, String description, String buttonText,
std::function<void()> action, String group ) {
if ( !action )
return false;
return mModel.addSetting( { { mIdPrefix + "." + id, mCategory, std::move( name ),
std::move( description ), std::move( group ) },
ActionSetting{ std::move( buttonText ), std::move( action ) } } );
}
} // namespace ecode
+64
View File
@@ -0,0 +1,64 @@
#ifndef ECODE_SETTINGSPAGE_HPP
#define ECODE_SETTINGSPAGE_HPP
#include "settingsdocument.hpp"
#include "settingsmodel.hpp"
namespace ecode {
class SettingsPage {
public:
static bool parseNonNegativeSettingsTime( const std::string& text, Time& time );
SettingsPage( SettingsModel& model, std::shared_ptr<SettingsDocument> document,
std::string category, std::string idPrefix );
bool addGroup( String name );
bool addBool( std::string id, std::string jsonPointer, String name, String description,
bool defaultValue, String group = {} );
bool addChoice( std::string id, std::string jsonPointer, String name, String description,
std::vector<String> choices, std::vector<nlohmann::json> values,
nlohmann::json defaultValue, std::vector<String> choiceDescriptions = {},
String group = {} );
bool addEditableChoice( std::string id, std::string jsonPointer, String name,
String description, std::vector<String> choices,
std::string defaultValue,
std::function<bool( const std::string& )> validate = {},
String group = {} );
bool addInteger( std::string id, std::string jsonPointer, String name, String description,
int min, int max, int defaultValue, String group = {} );
bool addText( std::string id, std::string jsonPointer, String name, String description,
std::string defaultValue, std::function<bool( const std::string& )> validate = {},
bool commitOnFocusLoss = false, String group = {} );
bool addSecret( std::string id, std::string jsonPointer, String name, String description,
std::string defaultValue = {}, String group = {},
std::string legacyJsonPointer = {} );
bool addJsonObject( std::string id, std::string jsonPointer, String name, String description,
nlohmann::json defaultValue = nlohmann::json::object(), String group = {} );
bool addStringList( std::string id, std::string jsonPointer, String name, String description,
std::vector<std::string> defaultValue = {}, String group = {} );
bool addFloat( std::string id, std::string jsonPointer, String name, String description,
double min, double max, double step, double defaultValue, String group = {} );
bool addAction( std::string id, String name, String description, String buttonText,
std::function<void()> action, String group = {} );
private:
SettingsModel& mModel;
std::shared_ptr<SettingsDocument> mDocument;
std::string mCategory;
std::string mIdPrefix;
};
} // namespace ecode
#endif
+59 -37
View File
@@ -1,6 +1,10 @@
#include "settingspanel.hpp"
#include "datetimecontroller.hpp"
#include "ecode.hpp"
#include "plugins/plugin.hpp"
#include "plugins/pluginmanager.hpp"
#include "settingsdocument.hpp"
#include "settingspage.hpp"
#include "uitreeviewfs.hpp"
#include <deque>
#include <limits>
@@ -220,6 +224,9 @@ static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml(
layout-width: 210dp;
layout-height: wrap_content;
}
.settings_panel .settings_text.error {
border-color: var(--theme-error);
}
.settings_panel .settings_integer {
layout-width: 110dp;
layout-height: wrap_content;
@@ -230,7 +237,7 @@ static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml(
<Splitter id="settings_splitter" lw="mp" lh="mp" orientation="horizontal" splitter-partition="220dp">
<vbox id="settings_sidebar" lw="0" lh="0" min-width="160dp">
<TextInput id="settings_filter" lw="mp" lh="wc" hint="@string(search_settings, Search settings...)" />
<TreeView id="settings_categories" lw="mp" lh="mp" />
<TreeView id="settings_categories" lw="mp" lh="o" lw8="1" />
</vbox>
<ScrollView id="settings_scroll" lw="0" lw8="1" lh="mp" focusable="false">
<vbox id="settings_rows" lw="mp" lh="wc">
@@ -327,6 +334,7 @@ void SettingsPanel::PanelState::reset() {
materializedCategories.clear();
subcategoryHeadings.clear();
model.clear();
documents.clear();
settingViews.clear();
selectedCategory.clear();
categoryFilter.clear();
@@ -426,10 +434,12 @@ void SettingsPanel::create( Scope scope ) {
panel.window->getKeyBindings().addKeybind(
{ KEY_E, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "focusSettingsCategories" );
panel.settings->beginAttributesTransaction();
if ( scope == Scope::User )
if ( scope == Scope::User ) {
addUserSettings( panel );
else
addPluginSettings( panel );
} else {
addProjectSettings( panel );
}
materializeCategory( panel, panel.selectedCategory );
setupCategories( panel );
panel.settings->endAttributesTransaction();
@@ -459,6 +469,10 @@ void SettingsPanel::create( Scope scope ) {
} );
panel.connections +=
panel.window->connect( Event::OnWindowClose, [this, &panel, scope]( const Event* ) {
for ( const auto& document : panel.documents ) {
if ( !document->save() )
Log::error( "Could not save settings document: %s", document->path() );
}
if ( scope == Scope::User )
mApp->saveConfig();
else
@@ -501,7 +515,6 @@ void SettingsPanel::addCategory( PanelState& panel, const std::string& id, const
void SettingsPanel::addSubcategoryHeading( PanelState& panel, const std::string& category,
const String& name ) {
panel.model.addGroup( { category, name, panel.model.settings().size() } );
panel.subcategoryHeadings.push_back( { category, name, nullptr } );
}
void SettingsPanel::setupCategories( PanelState& panel ) {
@@ -636,14 +649,7 @@ void SettingsPanel::materializeCategory( PanelState& panel, const std::string& c
->loadLayoutFromString( SETTINGS_SUBCATEGORY_HEADING_LAYOUT, container->second )
->asType<UITextView>();
heading->setText( group.name );
auto subcategory =
std::find_if( panel.subcategoryHeadings.begin(), panel.subcategoryHeadings.end(),
[&group]( const auto& item ) {
return item.category == group.category &&
item.name == group.name && !item.heading;
} );
if ( subcategory != panel.subcategoryHeadings.end() )
subcategory->heading = heading;
panel.subcategoryHeadings.push_back( { group.category, group.name, heading } );
}
auto& view = panel.settingViews[i];
if ( auto* value = std::get_if<BoolPointerSetting>( &setting.value ) ) {
@@ -712,6 +718,8 @@ void SettingsPanel::materializeCategory( PanelState& panel, const std::string& c
} else if ( auto* value = std::get_if<TextSetting>( &setting.value ) ) {
auto* row = createRow( panel, setting, view, SETTINGS_TEXT_ROW_LAYOUT.root() );
auto* input = row->find<UITextInput>( "setting_control_widget" );
if ( value->password )
input->setMode( UITextInput::TextInputMode::Password );
input->setText( String::fromUtf8( value->get() ) );
auto commit = [input, value]( const Event* ) {
if ( !value->set( input->getText().toUtf8() ) ) {
@@ -787,29 +795,6 @@ static void setNodeTreeEnabled( Node* node, bool enabled ) {
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 ) {
auto& settings = panel.model.settings();
@@ -1633,7 +1618,7 @@ void SettingsPanel::addUserSettings( PanelState& panel ) {
[this] { return mApp->getConfig().editor.cursorBlinkingTime.toString(); },
[this]( const std::string& text ) {
Time value;
if ( !parseNonNegativeTime( text, value ) )
if ( !SettingsPage::parseNonNegativeSettingsTime( text, value ) )
return false;
mApp->getConfig().editor.cursorBlinkingTime = value;
mApp->getSplitter()->forEachEditor(
@@ -1695,7 +1680,8 @@ void SettingsPanel::addUserSettings( PanelState& panel ) {
[this] { return mApp->getConfig().editor.codeFoldingRefreshFreq.toString(); },
[this]( const std::string& text ) {
Time value;
if ( !parseNonNegativeTime( text, value ) || value < Seconds( 1 ) )
if ( !SettingsPage::parseNonNegativeSettingsTime( text, value ) ||
value < Seconds( 1 ) )
return false;
mApp->getConfig().editor.codeFoldingRefreshFreq = value;
mApp->getSplitter()->forEachEditor(
@@ -2036,6 +2022,42 @@ void SettingsPanel::addUserSettings( PanelState& panel ) {
"Keep the terminal scrollbar hidden." ) } );
}
void SettingsPanel::addPluginSettings( PanelState& panel ) {
auto* manager = mApp->getPluginManager();
if ( !manager )
return;
manager->forEachPlugin( [this, &panel]( Plugin* plugin ) {
if ( !plugin || !plugin->isReady() || !plugin->hasSettingsPage() ||
!plugin->hasFileConfig() )
return;
std::string error;
auto document = SettingsDocument::load( plugin->getFileConfigPath(), error );
if ( !document ) {
Log::warning( "Could not load settings for plugin %s: %s", plugin->getId(), error );
return;
}
const std::string category = "plugins." + plugin->getId();
addCategory( panel, category, mApp->i18n( "plugins", "Plugins" ), plugin->getTitle() );
SettingsPage page( panel.model, document, category, plugin->getId() );
plugin->registerSettings( page );
const std::string path = document->path();
addAction( panel,
{ plugin->getId() + ".edit-json", category,
mApp->i18n( "edit_settings_json", "Edit Settings JSON" ),
mApp->i18n( "edit_settings_json_desc",
"Open the complete plugin configuration file." ) },
mApp->i18n( "open_file_ellipsis", "Open File..." ), [this, &panel, path] {
auto* window = panel.window;
window->runOnMainThread( [this, window, path] {
window->closeWindow();
mApp->getUISceneNode()->runOnMainThread(
[this, path] { mApp->focusOrLoadFile( path ); } );
} );
} );
panel.documents.emplace_back( std::move( document ) );
} );
}
void SettingsPanel::addProjectSettings( PanelState& panel ) {
addCategory( panel, "editor.document", mApp->i18n( "editor", "Editor" ),
mApp->i18n( "document", "Document" ) );
+25
View File
@@ -10,6 +10,7 @@
namespace ecode {
class App;
class SettingsDocument;
class SettingsPanel {
public:
@@ -40,6 +41,7 @@ class SettingsPanel {
std::shared_ptr<Model> categoryModel;
UIBindingGroup bindingGroup;
SettingsModel model;
std::vector<std::shared_ptr<SettingsDocument>> documents;
std::vector<std::pair<std::string, std::vector<std::string>>> categoryItems;
UnorderedMap<std::string, std::string> categoryIds;
UnorderedMap<std::string, String> categorySearchText;
@@ -61,42 +63,65 @@ class SettingsPanel {
PanelState mProject;
PanelState& state( Scope scope );
void create( Scope scope );
void selectCategory( PanelState& state, const std::string& category );
void addUserSettings( PanelState& state );
void addPluginSettings( PanelState& state );
void addProjectSettings( PanelState& state );
void addCategory( PanelState& state, const std::string& id, const String& parent,
const String& name );
void addSubcategoryHeading( PanelState& state, const std::string& category,
const String& name );
void setupCategories( PanelState& state );
void addBool( PanelState& state, SettingDescriptor binding, bool* value,
std::function<void( bool )> apply = {} );
void addBool( PanelState& state, SettingDescriptor binding, std::function<bool()> get,
std::function<void( bool )> set );
void addChoice( PanelState& state, SettingDescriptor binding,
const std::vector<String>& choices, std::function<size_t()> get,
std::function<void( size_t )> set,
std::vector<String> choiceDescriptions = {} );
void addEditableChoice( PanelState& state, SettingDescriptor binding,
const std::vector<String>& choices, std::function<String()> get,
std::function<bool( const String& )> set );
void addInteger( PanelState& state, SettingDescriptor binding, int min, int max,
std::function<int()> get, std::function<void( int )> set );
void addText( PanelState& state, SettingDescriptor binding, std::function<std::string()> get,
std::function<bool( const std::string& )> set, bool commitOnFocusLoss = false );
void addFloat( PanelState& state, SettingDescriptor binding, double min, double max,
double step, std::function<double()> get, std::function<void( double )> set );
void addAction( PanelState& state, SettingDescriptor binding, const String& buttonText,
std::function<void()> action );
UIWidget* createRow( PanelState& state, SettingDefinition& setting, SettingView& view,
pugi::xml_node layout );
UICheckBox* createBoolControl( PanelState& state, SettingDefinition& setting,
SettingView& view );
void materializeCategory( PanelState& state, const std::string& category );
void materializeVisibleSettings( PanelState& state, const String& query );
void setCategoryEnabled( PanelState& state, const std::string& category, bool enabled,
const std::string& excludedSetting = {} );
void filter( PanelState& state );
};