From 288aa931feeeeffe69c22000db92d30f12bd884e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 24 Aug 2026 02:34:14 -0300 Subject: [PATCH] ecode: add centralized settings panel Introduce modal user and project settings panels with searchable tree navigation and live configuration updates. - centralize settings registration and presentation - add user and project settings commands and shortcuts - preserve operational actions in the existing settings menu - support boolean, choice, numeric, and action-based settings - restore renderer notifications and choice descriptions - persist settings when closing the panel - add translated settings strings for all supported locales - fix live color preview updates --- bin/assets/i18n/de.xml | 164 +++ bin/assets/i18n/en.xml | 164 +++ bin/assets/i18n/fr.xml | 165 +++ bin/assets/i18n/zh.xml | 189 ++++ src/tools/ecode/ecode.cpp | 19 + src/tools/ecode/ecode.hpp | 11 +- src/tools/ecode/settingsmenu.cpp | 13 +- src/tools/ecode/settingspanel.cpp | 1729 +++++++++++++++++++++++++++++ src/tools/ecode/settingspanel.hpp | 81 ++ 9 files changed, 2532 insertions(+), 3 deletions(-) create mode 100644 src/tools/ecode/settingspanel.cpp create mode 100644 src/tools/ecode/settingspanel.hpp diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index d11d2d18e..cca58cc75 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -969,4 +969,168 @@ Verwendet strftime-Formatbezeichner. Die Dateierweiterung richtet sich nach dem Wenn Tab vor einem konfigurierten Zeichen gedrückt wird, springt der Cursor darüber. Gilt nur für einen einzelnen Cursor ohne Auswahl. Tab-Ausgangszeichen festlegen Legen Sie die Zeichen fest, über die der Cursor beim Drücken von Tab springen kann. + Einstellungen öffnen... + Projekteinstellungen öffnen... + Projekteinstellungen + Einstellungen durchsuchen... + Zeilennummern neben dem Editor anzeigen. + Sichtbare Markierungen für Leerraumzeichen anzeigen. + Die Zeile mit dem primären Cursor hervorheben. + Nachgestellte Leerzeichen beim Speichern entfernen. + Sicherstellen, dass gespeicherte Dateien mit einem Zeilenumbruch enden. + Suchergebnisse + Steuert, wie lange Zeilen im Editor umgebrochen werden. + Spalte für Zeilenumbruch und Breitenführung. 0 deaktiviert sie. + Anzahl der Spalten pro Einrückungsebene. + Anzahl der Spalten zur Darstellung eines Tabulators. + Allgemein + Verhalten + Den Willkommensbildschirm anzeigen, wenn kein Dokument oder Ordner geöffnet ist. + Erscheinungsbild + Navigation + Formatierung + Arbeitsbereich + Dokumentvorgaben + Zeilen am Anzeigebereich oder an der Umbruchspalte umbrechen. + Einrückung in umgebrochenen Fortsetzungszeilen beibehalten. + Syntaxabhängige Codefaltung aktivieren. + Faltsteuerelemente immer am Editorrand anzeigen. + Position neu geöffneter Tabs auswählen. + Detect indentation settings when opening each document. + Indent Using Spaces + Insert spaces instead of tab characters for indentation. + Write a Unicode byte-order mark when saving. + Schließende XML-Tags automatisch einfügen. + Unveränderte Dokumente bei Dateiänderungen neu laden. + Standard-Zeilenende für neue Dateien. + Dokumentvorgaben aus den Benutzereinstellungen übernehmen. + Letzte Sitzung wiederherstellen + Vorherigen Arbeitsbereich und Dokumente beim Start erneut öffnen. + Beim Start nach Aktualisierungen suchen + Nach dem Start auf eine neuere ecode-Version prüfen. + Bereich für neue Terminalsitzungen auswählen. + Terminal-Bildlaufleiste über oder neben dem Inhalt platzieren. + Sichtbarkeit der Terminal-Bildlaufleiste steuern. + Erweitert + Konfigurieren... + Schriftarten & Skalierung + Bildschirmfotos + Grafiksystem + Dokumentkodierung und Zeilenendeninformationen anzeigen. + Extern angeforderte Dokumente im Haupteditorbereich öffnen. + Reihenfolge der Tabnavigation per Tastatur auswählen. + Editorschriftart für Texteingabefelder verwenden. + Schriftgröße der Benutzeroberfläche festlegen. + 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. + Speicherort für Bildschirmfotos auswählen. + Zeitstempel-Dateinamensmuster für Bildschirmfotos konfigurieren. + Bildformat für Bildschirmfotos auswählen. + Bilder pro Sekunde begrenzen. 0 deaktiviert die Begrenzung. + Multisampling festlegen. Neustart erforderlich. + Shell-Programm und Befehlszeilenargumente festlegen. + Anzahl gespeicherter Terminalverlaufszeilen festlegen. + Arbeitsverzeichnis für neue Terminals auswählen. + Form und Animation des Terminalcursors auswählen. + Thema & Sprache + Choose whether the interface follows the system, light, or dark appearance. + Stylesheet der Benutzeroberfläche auswählen. + Sprache der Benutzeroberfläche auswählen. Neustart erforderlich. + Schriftart auswählen... + Proportionale Schriftart der Benutzeroberfläche auswählen. + Nichtproportionale Schriftart für Code-Editoren auswählen. + Nichtproportionale Schriftart für Terminals auswählen. + Schriftart für fehlende Zeichen auswählen. + Farben für Syntaxhervorhebung und Editoroberflächen auswählen. + Farben der integrierten Terminals auswählen. + Grafik-API-Version auswählen. Neustart erforderlich. + Disable multisample anti-aliasing. + Use 2x multisample anti-aliasing. + Use 4x multisample anti-aliasing. + Use 8x multisample anti-aliasing. + Use 16x multisample anti-aliasing. + Keine automatische Einrückung. + Einrückungsebene der vorherigen Zeile beibehalten. + Einrückung beibehalten und zwischen geschlossenen Klammern einrücken. + Immer das helle Oberflächenfarbschema verwenden. + Immer das dunkle Oberflächenfarbschema verwenden. + Terminal-Bildlaufleiste sichtbar lassen. + Terminal-Bildlaufleiste ausgeblendet lassen. + Immer ausgeblendet + Immer sichtbar + Automatisch + Das passende schließende Zeichen automatisch einfügen. + Automatisch erkennen + Automatische Einrückung + Keine + Beibehalten + Intelligent + Steuert die automatische Einrückung nach dem Drücken der Eingabetaste. + Blinkender Balken + Blinkender Block + Blinkender Unterstrich + Chronologisch + Terminal-Tab beim Beenden schließen + Schließt einen Terminal-Tab, wenn sein Hauptprozess beendet wird. + Standardarbeitsverzeichnis des Terminals konfigurieren + Cursorstil + Tab-Umschalter anzeigen + Zeigt in der Mitte jedes Tab-Widgets einen Tab-Umschalter an. + Editor + Editor-Schriftart in Eingabefeldern + Exklusivmodus standardmäßig aktivieren + Deaktiviert globale Tastenkürzel in neu erstellten Terminals. + Dateizuordnungen + Voll + Graustufen + Tableiste ausblenden + Blendet die Tableiste immer aus. + Richtet Text innerhalb der sichtbaren Breite von Einrückungstabs aus. + Wählt das beim Einrücken mit Tab eingefügte Zeichen. + Der lineare Sprungmodus wechselt Tabs in ihrer angezeigten Reihenfolge. + Sprachen + Linear + Position neuer Tabs + Nach aktivem Tab + Erste Position + Letzte Position + Links vom aktiven Tab + Dokumente im Hauptbereich öffnen + Im Statusleistenbereich öffnen + Projekt in neuem Fenster öffnen + Öffnet Ordner in einem neuen Fenster, wenn bereits ein anderes Projekt geladen ist. + Außerhalb + Überlagernd + Schnellvorschau für Bilder + Zeigt Bilder in einer Vorschau, ohne einen dauerhaften Editor-Tab zu öffnen. + Editorauswahl bei Fokus wiederherstellen + Stellt die letzte Cursorposition und Auswahl jedes Editorbereichs beim Fokussieren wieder her. + Bildlaufleistenmodus + Zeigt eine überlagernde Bildlaufleiste bei Mausbewegung oder eine äußere Bildlaufleiste bei scrollbarem Inhalt. + Bildlaufleistentyp + Die Bildlaufleiste hat eigenen Platz und verdeckt nie den Inhalt. + Die Bildlaufleiste wird über dem Inhalt angezeigt. + Speicherpfad für Bildschirmfotos + Leicht + Statischer Balken + Statischer Block + Statischer Unterstrich + Subpixel + Tableiste + Tab-Sprungmodus + Wählt die Standardsprache für mehrdeutige .h-Headerdateien. + Schriftkantenglättung + Legt fest, wie Glyphenkanten geglättet werden. + Schrifthinting + Steuert die Ausrichtung der Glyphen am Pixelraster. + Native Dateidialoge aktivieren + Verwendet Dateidialoge des Betriebssystems, sofern verfügbar. + Synchronisiert die Darstellung mit der Bildwiederholrate. Neustart erforderlich. + Vor dem Schließen des Tabs warnen + Fragt vor dem Schließen eines Terminals mit laufendem Programm nach. diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index d9582ab61..f8d327faf 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -953,4 +953,168 @@ Uses strftime format specifiers. The file extension follows the selected screens Pressing Tab before a configured character moves the cursor past it. Only applies to a single cursor without a selection. Set Tab Out Characters Set the characters the cursor can move past when pressing Tab. + Open Settings... + Open Project Settings... + Project Settings + Search settings... + Display line numbers beside the editor. + Display visible markers for whitespace characters. + Highlight the line containing the primary cursor. + Remove trailing whitespace when saving files. + Ensure saved files end with a newline. + Search Results + Controls how long lines wrap in the editor. + Column used for wrapping and the editor width guide. Set 0 to disable. + Number of columns inserted for one indentation level. + Number of columns used to display a tab character. + General + Behavior + Show the welcome screen when no document or folder is open. + Appearance + Navigation + Formatting + Workspace + Document Defaults + Choose whether lines wrap at the viewport or line breaking column. + Preserve indentation on wrapped continuation lines. + Enable syntax-aware code folding. + Always display fold controls in the editor gutter. + Choose where newly opened tabs are inserted. + Detect indentation settings when opening each document. + Indent Using Spaces + Insert spaces instead of tab characters for indentation. + Write a Unicode byte-order mark when saving. + Automatically insert closing XML tags. + Reload unmodified documents when their files change on disk. + Default line-ending sequence for new files. + Inherit document defaults from the user settings. + Restore Last Session + Reopen the previous workspace and documents at startup. + Check for Updates at Startup + Check whether a newer ecode release is available after startup. + Choose where newly created terminal sessions are opened. + Place the terminal scrollbar over or outside its content. + Control when the terminal scrollbar is visible. + Advanced + Configure... + Fonts & Scale + Screenshots + Renderer + Display document encoding and line-ending information. + Open externally requested documents in the main editor split. + Choose how keyboard navigation cycles through tabs. + Use the editor font for text input controls. + Set the font size used by the application UI. + 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. + Choose where screenshots are saved. + Configure the timestamp-based screenshot filename pattern. + Choose the image format for screenshots. + Limit rendered frames per second. Set 0 to disable. + Set renderer multisampling. Restart required. + Set the shell executable and command-line arguments. + Set the number of terminal history lines retained. + Choose the working directory used by new terminals. + Choose the terminal cursor shape and animation. + Theme & Language + Choose whether the interface follows the system, light, or dark appearance. + Choose the stylesheet used by the application interface. + Choose the interface language. Restart required. + Choose Font... + Choose the proportional font used by the interface. + Choose the monospace font used by code editors. + Choose the monospace font used by terminals. + Choose the font used for missing glyphs. + Choose the colors used for syntax highlighting and editor surfaces. + Choose the colors used by integrated terminals. + Select the graphics API version used by ecode. Restart required. + Disable multisample anti-aliasing. + Use 2x multisample anti-aliasing. + Use 4x multisample anti-aliasing. + Use 8x multisample anti-aliasing. + Use 16x multisample anti-aliasing. + No automatic indentation. + Preserve the indentation level of the previous line. + Preserve indentation and indent between auto-closed brackets. + Always use the light interface color scheme. + Always use the dark interface color scheme. + Keep the terminal scrollbar visible. + Keep the terminal scrollbar hidden. + Always Hidden + Always Visible + Auto + Automatically insert the matching closing character. + Auto-Detect + Auto-Indent + None + Preserve + Smart + Control indentation added after pressing Enter. + Blinking Bar + Blinking Block + Blinking Underline + Chronological + Close Terminal Tab on Exit + Close a terminal tab when its main process exits. + Configure Terminal Default Working Directory + Cursor Style + Display Tab Switcher + Display a tab switcher in the center of each tab widget. + Editor + Editor Font in Input Fields + Enable Exclusive Mode by Default + Disable global keybindings in newly created terminals. + File Associations + Full + Grayscale + Hide Tab Bar + Always hide the tab bar. + Align text within the visual width of indentation tabs. + Choose the character inserted when indenting with Tab. + Linear Jump Mode switches tabs in their displayed order. + Languages + Linear + New Tab Position + After Active Tab + First + Last + Left of Active Tab + Open Documents in Main Split + Open in Status Bar Panel + Open Project in New Window + Open folders in a new window when another project is already loaded. + Outside + Overlay + Quick Preview Images + Preview images without opening a permanent editor tab. + Restore Editor Selection on Focus + Restore each editor split's last cursor and selection when focused. + Scrollbar Mode + Show an overlay scrollbar on mouse movement, or an outside scrollbar when content is scrollable. + Scrollbar Type + The scrollbar has its own space and never covers content. + The scrollbar appears over content. + Screenshot Save Path + Slight + Steady Bar + Steady Block + Steady Underline + Subpixel + Tab Bar + Tab Jump Mode + Choose the default language for ambiguous .h header files. + Font Anti-Aliasing + Choose how glyph edges are smoothed. + Font Hinting + Control glyph alignment to the pixel grid. + Enable Native File Dialogs + Use operating-system file dialogs when available. + Synchronize rendering with the display refresh rate. Restart required. + Warn Before Closing Tab + Ask before closing a terminal while a program is running. diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index a6815adf3..14473157d 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -948,4 +948,169 @@ Utilise les spécificateurs de format strftime. L’extension du fichier corresp Appuyer sur Tab avant un caractère configuré déplace le curseur après celui-ci. S'applique uniquement à un curseur sans sélection. Définir les caractères de sortie Définissez les caractères que le curseur peut franchir en appuyant sur Tab. + Ouvrir les paramètres... + Ouvrir les paramètres du projet... + Paramètres du projet + Rechercher dans les paramètres... + Afficher les numéros de ligne à côté de l'éditeur. + Afficher des marqueurs visibles pour les espaces. + Mettre en évidence la ligne contenant le curseur principal. + Supprimer les espaces de fin lors de l'enregistrement. + S'assurer que les fichiers enregistrés se terminent par un saut de ligne. + Résultats de recherche + Contrôle le retour à la ligne des lignes longues dans l'éditeur. + Colonne utilisée pour le retour à la ligne et le guide de largeur. 0 pour désactiver. + Nombre de colonnes insérées pour un niveau d'indentation. + Nombre de colonnes utilisées pour afficher une tabulation. + Général + Comportement + Afficher l'écran de bienvenue lorsqu'aucun document ou dossier n'est ouvert. + Appearance + Navigation + Formatting + Workspace + Document Defaults + Choose whether lines wrap at the viewport or line breaking column. + Preserve indentation on wrapped continuation lines. + Enable syntax-aware code folding. + Always display fold controls in the editor gutter. + Choose where newly opened tabs are inserted. + Detect indentation settings when opening each document. + Indent Using Spaces + Insert spaces instead of tab characters for indentation. + Write a Unicode byte-order mark when saving. + Automatically insert closing XML tags. + Reload unmodified documents when their files change on disk. + Default line-ending sequence for new files. + Inherit document defaults from the user settings. + Restore Last Session + Reopen the previous workspace and documents at startup. + Check for Updates at Startup + Check whether a newer ecode release is available after startup. + Choose where newly created terminal sessions are opened. + Place the terminal scrollbar over or outside its content. + Control when the terminal scrollbar is visible. + Advanced + Configure... + Fonts & Scale + Screenshots + Renderer + Display document encoding and line-ending information. + Open externally requested documents in the main editor split. + Choose how keyboard navigation cycles through tabs. + Use the editor font for text input controls. + Set the font size used by the application UI. + 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. + Choose where screenshots are saved. + Configure the timestamp-based screenshot filename pattern. + Choose the image format for screenshots. + Limit rendered frames per second. Set 0 to disable. + Set renderer multisampling. Restart required. + Set the shell executable and command-line arguments. + Set the number of terminal history lines retained. + Choose the working directory used by new terminals. + Choose the terminal cursor shape and animation. + Theme & Language + Choose whether the interface follows the system, light, or dark appearance. + Choose the stylesheet used by the application interface. + Choose the interface language. Restart required. + Choose Font... + Choose the proportional font used by the interface. + Choose the monospace font used by code editors. + Choose the monospace font used by terminals. + Choose the font used for missing glyphs. + Choose the colors used for syntax highlighting and editor surfaces. + Choose the colors used by integrated terminals. + Select the graphics API version used by ecode. Restart required. + Disable multisample anti-aliasing. + Use 2x multisample anti-aliasing. + Use 4x multisample anti-aliasing. + Use 8x multisample anti-aliasing. + Use 16x multisample anti-aliasing. + No automatic indentation. + Preserve the indentation level of the previous line. + Preserve indentation and indent between auto-closed brackets. + Always use the light interface color scheme. + Always use the dark interface color scheme. + Keep the terminal scrollbar visible. + Toujours masquer la barre de défilement du terminal. + Toujours masquée + Toujours visible + Automatique + Insérer automatiquement le caractère de fermeture correspondant. + Détection automatique + Indentation automatique + Aucune + Conserver + Intelligente + Contrôle l'indentation ajoutée après avoir appuyé sur Entrée. + Barre clignotante + Bloc clignotant + Soulignement clignotant + Chronologique + Fermer l'onglet du terminal à la sortie + Ferme un onglet de terminal lorsque son processus principal se termine. + Configurer le répertoire de travail par défaut du terminal + Style du curseur + Afficher le sélecteur d'onglets + Affiche un sélecteur d'onglets au centre de chaque groupe d'onglets. + Éditeur + Police de l'éditeur dans les champs de saisie + Activer le mode exclusif par défaut + Désactive les raccourcis globaux dans les nouveaux terminaux. + Associations de fichiers + Complet + Niveaux de gris + Masquer la barre d'onglets + Masque la barre d'onglets lorsqu'elle ne contient qu'un seul élément. + Toujours masquer la barre d'onglets. + Aligne le texte dans la largeur visuelle des tabulations d'indentation. + Choisit le caractère inséré lors de l'indentation avec Tab. + Le mode linéaire parcourt les onglets dans leur ordre d'affichage. + Langages + Linéaire + Position des nouveaux onglets + Après l'onglet actif + En premier + En dernier + À gauche de l'onglet actif + Ouvrir les documents dans la zone principale + Ouvrir dans le panneau de la barre d'état + Ouvrir le projet dans une nouvelle fenêtre + Ouvre les dossiers dans une nouvelle fenêtre lorsqu'un autre projet est déjà chargé. + À l'extérieur + Superposée + Aperçu rapide des images + Prévisualise les images sans ouvrir d'onglet permanent. + Restaurer la sélection de l'éditeur au focus + Restaure la dernière position du curseur et la sélection de chaque zone d'édition lors du focus. + Mode de la barre de défilement + Affiche une barre superposée au mouvement de la souris, ou une barre extérieure lorsque le contenu défile. + Type de barre de défilement + La barre de défilement occupe son propre espace et ne recouvre jamais le contenu. + La barre de défilement apparaît par-dessus le contenu. + Chemin d'enregistrement des captures d'écran + Léger + Barre fixe + Bloc fixe + Soulignement fixe + Sous-pixel + Barre d'onglets + Mode de parcours des onglets + Choisit le langage par défaut des fichiers d'en-tête .h ambigus. + Anticrénelage des polices + Choisit la méthode de lissage des contours des glyphes. + Optimisation des polices + Contrôle l'alignement des glyphes sur la grille de pixels. + Activer les boîtes de dialogue de fichiers natives + Utilise les boîtes de dialogue du système lorsqu'elles sont disponibles. + Synchronise le rendu avec la fréquence de l'écran. Redémarrage requis. + Avertir avant de fermer l'onglet + Demande confirmation avant de fermer un terminal exécutant un programme. diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 9c84db85d..3bfb41d7b 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -727,4 +727,193 @@ file in the directory tree. 在已配置的字符前按 Tab 可将光标移至该字符之后。仅适用于无选区的单个光标。 设置 Tab 跳出字符 设置按 Tab 时光标可以跳过的字符。 + 打开设置... + 打开项目设置... + 项目设置 + 搜索设置... + 在编辑器旁显示行号。 + 显示空白字符的可见标记。 + 突出显示主光标所在行。 + 保存文件时删除行尾空白。 + 确保保存的文件以换行符结尾。 + 搜索结果 + 控制编辑器中长行的换行方式。 + 用于换行和编辑器宽度参考线的列。设置为 0 可禁用。 + 每个缩进级别插入的列数。 + 显示制表符时使用的列数。 + 常规 + 行为 + 未打开文档或文件夹时显示欢迎屏幕。 + Appearance + Navigation + Formatting + Workspace + Document Defaults + Choose whether lines wrap at the viewport or line breaking column. + Preserve indentation on wrapped continuation lines. + Enable syntax-aware code folding. + Always display fold controls in the editor gutter. + Choose where newly opened tabs are inserted. + Detect indentation settings when opening each document. + Indent Using Spaces + Insert spaces instead of tab characters for indentation. + Write a Unicode byte-order mark when saving. + Automatically insert closing XML tags. + Reload unmodified documents when their files change on disk. + Default line-ending sequence for new files. + Inherit document defaults from the user settings. + Restore Last Session + Reopen the previous workspace and documents at startup. + Check for Updates at Startup + Check whether a newer ecode release is available after startup. + Choose where newly created terminal sessions are opened. + Place the terminal scrollbar over or outside its content. + Control when the terminal scrollbar is visible. + Advanced + Configure... + Fonts & Scale + Screenshots + Renderer + Display document encoding and line-ending information. + Open externally requested documents in the main editor split. + Choose how keyboard navigation cycles through tabs. + Use the editor font for text input controls. + Set the font size used by the application UI. + 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. + Choose where screenshots are saved. + Configure the timestamp-based screenshot filename pattern. + Choose the image format for screenshots. + Limit rendered frames per second. Set 0 to disable. + Set renderer multisampling. Restart required. + Set the shell executable and command-line arguments. + Set the number of terminal history lines retained. + Choose the working directory used by new terminals. + Choose the terminal cursor shape and animation. + Theme & Language + Choose whether the interface follows the system, light, or dark appearance. + Choose the stylesheet used by the application interface. + Choose the interface language. Restart required. + Choose Font... + Choose the proportional font used by the interface. + Choose the monospace font used by code editors. + Choose the monospace font used by terminals. + Choose the font used for missing glyphs. + Choose the colors used for syntax highlighting and editor surfaces. + Choose the colors used by integrated terminals. + Select the graphics API version used by ecode. Restart required. + Disable multisample anti-aliasing. + Use 2x multisample anti-aliasing. + Use 4x multisample anti-aliasing. + Use 8x multisample anti-aliasing. + Use 16x multisample anti-aliasing. + No automatic indentation. + Preserve the indentation level of the previous line. + Preserve indentation and indent between auto-closed brackets. + Always use the light interface color scheme. + Always use the dark interface color scheme. + Keep the terminal scrollbar visible. + 始终隐藏终端滚动条。 + 始终隐藏 + 始终显示 + 自动 + 自动插入匹配的闭合字符。 + 自动检测 + 自动缩进 + + 保持 + 智能 + 控制按下 Enter 后添加的自动缩进。 + 磁盘文件更改时自动重新加载 + 闪烁竖线 + 闪烁方块 + 闪烁下划线 + 居中 + 按访问顺序 + 进程退出时关闭终端标签页 + 终端主进程退出时关闭其标签页。 + 代码折叠 + 始终显示折叠控件 + 配置终端默认工作目录 + 光标样式 + 显示标签页切换器 + 在每个标签页组件中央显示标签页切换器。 + 编辑器 + 输入框使用编辑器字体 + 启用 + 默认启用独占模式 + 在新建终端中禁用全局快捷键。 + 文件关联 + 折叠区域刷新频率 + 完全 + 灰度 + 隐藏标签栏 + 始终隐藏标签栏。 + 缩进制表符对齐 + 在缩进制表符的可视宽度内对齐文本。 + 缩进制表字符 + 选择按 Tab 缩进时插入的字符。 + 线性模式按标签页显示顺序切换。 + 保持缩进 + 语言 + 左对齐 + 线性 + 自动换行 + 新标签页位置 + 活动标签页之后 + 最前 + 最后 + 活动标签页左侧 + 不换行 + 在主编辑区打开文档 + 在新窗口中打开文件 + 在状态栏面板中打开 + 在新窗口中打开项目 + 已有项目加载时,在新窗口中打开文件夹。 + 外置 + 叠加 + 快速预览图像 + 无需打开永久编辑器标签页即可预览图像。 + 聚焦时恢复编辑器选区 + 编辑区重新获得焦点时,恢复其上次的光标和选区。 + 右对齐 + 滚动条模式 + 鼠标移动时显示叠加滚动条;内容可滚动时显示外置滚动条。 + 滚动条类型 + 滚动条占用独立空间,不会覆盖内容。 + 滚动条叠加显示在内容上方。 + 会话快照与定期备份 + 保留会话之间未保存的文档更改。 + 截图保存路径 + 设置 + 轻微 + 固定竖线 + 固定方块 + 固定下划线 + 次像素 + 系统 + 标签栏 + 标签页切换模式 + 将 .h 文件视为 + 选择歧义 .h 头文件的默认语言。 + 单击即可在项目树中打开文件。 + 字体抗锯齿 + 选择字形边缘的平滑方式。 + 字体微调 + 控制字形与像素网格的对齐。 + 启用系统原生文件对话框 + 可用时使用操作系统原生文件对话框。 + 视口 + 使渲染与显示器刷新率同步。需要重启。 + 关闭标签页前警告 + 终端中有程序运行时,关闭前请求确认。 + 启用欢迎界面 + 按字符换行 + 换行依据 + 按单词换行 diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 07cd4e5ca..fe8c2ae59 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -9,6 +9,7 @@ #include "plugins/debugger/statusdebuggercontroller.hpp" #include "settingsactions.hpp" #include "settingsmenu.hpp" +#include "settingspanel.hpp" #include "uibuildsettings.hpp" #include "uidownloadwindow.hpp" #include "uirightpanel.hpp" @@ -2123,6 +2124,8 @@ std::map App::getLocalKeybindings() { { { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "debug-widget-tree-view" }, { { KEY_K, KeyMod::getDefaultModifier() }, "open-locatebar" }, { { KEY_P, KeyMod::getDefaultModifier() }, "open-command-palette" }, + { { KEY_COMMA, KeyMod::getDefaultModifier() }, "open-settings" }, + { { KEY_COMMA, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-project-settings" }, { { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-global-search" }, { { KEY_L, KeyMod::getDefaultModifier() }, "go-to-line" }, #if EE_PLATFORM == EE_PLATFORM_MACOS @@ -2206,6 +2209,8 @@ std::vector App::getUnlockedCommands() { "toggle-status-terminal", "toggle-status-app-output", "menu-toggle", + "open-settings", + "open-project-settings", "switch-side-panel", "toggle-status-bar", "download-file-web", @@ -3593,6 +3598,20 @@ void App::toggleSettingsMenu() { mSettings->toggleSettingsMenu(); } +void App::openSettings() { + if ( !mSettingsPanel ) + mSettingsPanel = std::make_unique( this ); + mSettingsPanel->show( SettingsPanel::Scope::User ); +} + +void App::openProjectSettings() { + if ( !projectIsOpen() ) + return; + if ( !mSettingsPanel ) + mSettingsPanel = std::make_unique( this ); + mSettingsPanel->show( SettingsPanel::Scope::Project ); +} + void App::showFolderTreeViewTab() { UITab* tab = mSidePanel->find( "treeview_tab" ); if ( tab ) diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 007d40bcb..3486aeb14 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -34,6 +34,7 @@ class FormatterPlugin; class DateTimeController; class FontPickerController; class SettingsMenu; +class SettingsPanel; class UITreeViewFS; class StatusDebuggerController; class UIRightPanel; @@ -215,6 +216,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void toggleSettingsMenu(); + void openSettings(); + + void openProjectSettings(); + UIStatusBar* getStatusBar() const { return mStatusBar; } void showFolderTreeViewTab(); @@ -243,6 +248,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { t.setCommand( "debug-draw-debug-data", [this] { debugDrawData(); } ); t.setCommand( "debug-widget-tree-view", [this] { createWidgetInspector(); } ); t.setCommand( "menu-toggle", [this] { toggleSettingsMenu(); } ); + t.setCommand( "open-settings", [this] { openSettings(); } ); + t.setCommand( "open-project-settings", [this] { openProjectSettings(); } ); t.setCommand( "switch-side-panel", [this] { switchSidePanel(); } ); t.setCommand( "download-file-web", [this] { downloadFileWebDialog(); } ); t.setCommand( "move-panel-left", [this] { panelPosition( PanelPosition::Left ); } ); @@ -364,8 +371,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { t.setCommand( "ui-panel-font-size", [this] { mSettingsActions->setUIPanelFontSize(); } ); t.setCommand( "sans-serif-font", [this] { openFontDialog( mConfig.ui.sansSerifFont, false ); } ); - t.setCommand( "editor-font", - [this] { openFontDialog( mConfig.ui.monospaceFont, true ); } ); + t.setCommand( "editor-font", [this] { openFontDialog( mConfig.ui.monospaceFont, true ); } ); t.setCommand( "terminal-font", [this] { openFontDialog( mConfig.ui.terminalFont, true, true ); } ); t.setCommand( "fallback-font", [this] { @@ -777,6 +783,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { std::unique_ptr mTerminalManager; std::unique_ptr mPluginManager; std::unique_ptr mSettings; + std::unique_ptr mSettingsPanel; std::unique_ptr mDateTimeController; std::unique_ptr mFontPickerController; std::string mFileToOpen; diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 8102b6a5a..32dfb75b8 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -73,6 +73,16 @@ void SettingsMenu::createSettingsMenu( App* app, UIMenuBar* menuBar ) { getKeybind( "save-all" ) ) ->setId( "save-all" ); mSettingsMenu->addSeparator()->setId( "settings-submenus-sep" ); + mSettingsMenu + ->add( i18n( "open_settings_ellipsis", "Open Settings..." ), findIcon( "settings" ), + getKeybind( "open-settings" ) ) + ->setId( "open-settings" ); + mSettingsMenu + ->add( i18n( "open_project_settings_ellipsis", "Open Project Settings..." ), + findIcon( "folder-settings" ), getKeybind( "open-project-settings" ) ) + ->setId( "open-project-settings" ) + ->setEnabled( mApp->projectIsOpen() ); + mSettingsMenu->addSeparator(); mProjectMenu = UIPopUpMenu::New(); mDocMenu = UIPopUpMenu::New(); @@ -2353,7 +2363,7 @@ UIMenu* SettingsMenu::createViewMenu() { } else if ( item->getId() == "enable-color-preview" ) { mApp->getConfig().editor.colorPreview = item->asType()->isActive(); mSplitter->forEachEditor( [this]( UICodeEditor* editor ) { - editor->setEnableColorPickerOnSelection( mApp->getConfig().editor.colorPreview ); + editor->setColorPreview( mApp->getConfig().editor.colorPreview ); } ); } else if ( item->getId() == "enable-color-picker" ) { mApp->getConfig().editor.colorPickerSelection = @@ -2587,6 +2597,7 @@ void SettingsMenu::toggleSettingsMenu() { } void SettingsMenu::updateProjectSettingsMenu() { + mSettingsMenu->getItemId( "open-project-settings" )->setEnabled( mApp->projectIsOpen() ); mSettingsMenu->getItemId( "project_settings" ) ->setEnabled( !mApp->getCurrentProject().empty() ); diff --git a/src/tools/ecode/settingspanel.cpp b/src/tools/ecode/settingspanel.cpp new file mode 100644 index 000000000..3627f7825 --- /dev/null +++ b/src/tools/ecode/settingspanel.cpp @@ -0,0 +1,1729 @@ +#include "settingspanel.hpp" +#include "ecode.hpp" +#include "uitreeviewfs.hpp" +#include + +using namespace EE::UI::Models; + +namespace ecode { + +class SettingsCategoryModel final : public Model { + public: + struct Node { + std::string id; + std::string text; + Node* parent{ nullptr }; + std::vector children; + std::vector visibleChildren; + }; + + static std::shared_ptr + create( const std::vector>>& categories, + const UnorderedMap& ids ) { + return std::make_shared( categories, ids ); + } + + SettingsCategoryModel( + const std::vector>>& categories, + const UnorderedMap& ids ) { + mNodes.emplace_back(); + mRoot = &mNodes.back(); + for ( const auto& [parent, children] : categories ) { + std::string parentId; + if ( !children.empty() ) { + auto id = ids.find( parent + '/' + children.front() ); + if ( id != ids.end() ) { + auto separator = id->second.find( '.' ); + parentId = id->second.substr( 0, separator ) + ".*"; + } + } + mNodes.push_back( { std::move( parentId ), parent, mRoot } ); + auto* parentNode = &mNodes.back(); + mRoot->children.push_back( parentNode ); + for ( const auto& child : children ) { + auto id = ids.find( parent + '/' + child ); + mNodes.push_back( + { id == ids.end() ? std::string{} : id->second, child, parentNode } ); + parentNode->children.push_back( &mNodes.back() ); + } + } + filter( {}, {} ); + } + + size_t rowCount( const ModelIndex& parent = {} ) const { + auto* node = parent.isValid() ? static_cast( parent.internalData() ) : mRoot; + return node->visibleChildren.size(); + } + + size_t columnCount( const ModelIndex& = {} ) const { return 1; } + + ModelIndex index( int row, int column, const ModelIndex& parent = {} ) const { + auto* node = parent.isValid() ? static_cast( parent.internalData() ) : mRoot; + if ( row < 0 || column != 0 || static_cast( row ) >= node->visibleChildren.size() ) + return {}; + return createIndex( row, column, node->visibleChildren[row] ); + } + + ModelIndex parentIndex( const ModelIndex& index ) const { + if ( !index.isValid() ) + return {}; + auto* node = static_cast( index.internalData() ); + if ( !node->parent || node->parent == mRoot ) + return {}; + auto* parent = node->parent; + auto found = + std::find( mRoot->visibleChildren.begin(), mRoot->visibleChildren.end(), parent ); + return found == mRoot->visibleChildren.end() + ? ModelIndex{} + : createIndex( std::distance( mRoot->visibleChildren.begin(), found ), 0, + parent ); + } + + Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const { + if ( !index.isValid() || role != ModelRole::Display ) + return {}; + return Variant( static_cast( index.internalData() )->text ); + } + + void filter( const std::string_view query, + const UnorderedSet& matchingCategories ) { + mRoot->visibleChildren.clear(); + for ( auto* parent : mRoot->children ) { + parent->visibleChildren.clear(); + const bool parentMatches = query.empty() || String::icontains( parent->text, query ); + for ( auto* child : parent->children ) { + if ( parentMatches || String::icontains( child->text, query ) || + matchingCategories.contains( child->id ) ) + parent->visibleChildren.push_back( child ); + } + if ( !parent->visibleChildren.empty() ) + mRoot->visibleChildren.push_back( parent ); + } + invalidate( Model::UpdateFlag::InvalidateAllIndexes ); + } + + private: + std::deque mNodes; + Node* mRoot{ nullptr }; +}; + +static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml( + + + + + + + + + + + + +)xml"; + +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_BOOL_LAYOUT = R"xml()xml"; +static constexpr const char* SETTINGS_CHOICE_LAYOUT = + R"xml()xml"; +static constexpr const char* SETTINGS_INTEGER_LAYOUT = + R"xml()xml"; +static constexpr const char* SETTINGS_ACTION_LAYOUT = + R"xml()xml"; + +SettingsPanel::SettingsPanel( App* app ) : mApp( app ) {} + +SettingsPanel::PanelState& SettingsPanel::state( Scope scope ) { + return scope == Scope::User ? mUser : mProject; +} + +void SettingsPanel::PanelState::reset() { + bindingGroup.clear(); + connections.clear(); + window = nullptr; + search = nullptr; + categories = nullptr; + scroll = nullptr; + settings = nullptr; + pageTitle = nullptr; + categoryModel.reset(); + categoryItems.clear(); + categoryIds.clear(); + categorySearchText.clear(); + categoryTitles.clear(); + categoryHeadings.clear(); + bindings.clear(); + selectedCategory.clear(); + categoryFilter.clear(); +} + +void SettingsPanel::show( Scope scope ) { + auto& panel = state( scope ); + if ( panel.window ) { + panel.window->show(); + panel.window->toFront(); + panel.categories->setFocus(); + return; + } + create( scope ); +} + +void SettingsPanel::create( Scope scope ) { + auto& panel = state( scope ); + UIWindow::StyleConfig config{ UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON | UI_WIN_MODAL }; + panel.window = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, config ); + panel.window->setId( scope == Scope::User ? "settings_panel" : "project_settings_panel" ); + panel.window->setTitle( scope == Scope::User + ? mApp->i18n( "settings", "Settings" ) + : mApp->i18n( "project_settings", "Project Settings" ) ); + const auto sceneSize = mApp->getUISceneNode()->getPixelsSize(); + panel.window->setPixelsSize( { eeclamp( sceneSize.getWidth() * 0.82f, 720.f, 1200.f ), + eeclamp( sceneSize.getHeight() * 0.82f, 520.f, 850.f ) } ); + panel.window->setMinWindowSize( 640, 440 ); + panel.window->setKeyBindingCommand( "closeWindow", [window = panel.window, this] { + if ( !SceneManager::instance()->isShuttingDown() ) { + window->closeWindow(); + if ( mApp->getSplitter() && mApp->getSplitter()->getCurWidget() ) + mApp->getSplitter()->getCurWidget()->setFocus(); + } + } ); + panel.window->getKeyBindings().addKeybind( { KEY_ESCAPE }, "closeWindow" ); + auto* layout = mApp->getUISceneNode()->loadLayoutFromString( SETTINGS_PANEL_LAYOUT, + panel.window->getContainer() ); + panel.search = layout->find( "settings_filter" ); + panel.categories = layout->find( "settings_categories" ); + panel.settings = layout->find( "settings_rows" ); + panel.pageTitle = layout->find( "settings_page_title" ); + panel.scroll = layout->find( "settings_scroll" ); + panel.scroll->setVerticalScrollMode( ScrollBarMode::Auto ); + panel.scroll->setHorizontalScrollMode( ScrollBarMode::AlwaysOff ); + if ( scope == Scope::User ) + addUserSettings( panel ); + else + addProjectSettings( panel ); + setupCategories( panel ); + filter( panel ); + panel.connections += panel.search->connect( + Event::OnTextChanged, [this, &panel]( const Event* ) { filter( panel ); } ); + panel.connections += panel.window->connect( Event::OnWindowReady, [&panel]( const Event* ) { + auto title = panel.categoryTitles.find( panel.selectedCategory ); + if ( title != panel.categoryTitles.end() ) { + auto index = panel.categories->findRowWithText( + title->second.toUtf8(), true, UIAbstractView::FindRowWithTextMatchKind::Equals ); + if ( index.isValid() ) + panel.categories->setSelection( index ); + } + panel.categories->setFocus(); + } ); + panel.connections += + panel.window->connect( Event::OnWindowClose, [this, &panel, scope]( const Event* ) { + if ( scope == Scope::User ) + mApp->saveConfig(); + else + mApp->saveProject(); + panel.reset(); + } ); + panel.window->center(); + panel.window->showWhenReady(); + panel.categories->setFocus(); +} + +void SettingsPanel::addCategory( PanelState& panel, const std::string& id, const String& parent, + const String& name ) { + const auto parentText = parent.toUtf8(); + const auto nameText = name.toUtf8(); + auto parentItems = + std::find_if( panel.categoryItems.begin(), panel.categoryItems.end(), + [&parentText]( const auto& item ) { return item.first == parentText; } ); + if ( parentItems == panel.categoryItems.end() ) { + panel.categoryItems.emplace_back( parentText, std::vector{ nameText } ); + } else { + parentItems->second.emplace_back( nameText ); + } + panel.categoryIds[parentText + '/' + nameText] = id; + panel.categorySearchText[id] = parent + " " + name; + panel.categoryTitles[id] = name; + auto* heading = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_CATEGORY_HEADING_LAYOUT, panel.settings ) + ->asType(); + heading->setText( name ); + heading->setId( "settings_category_" + id ); + panel.categoryHeadings[id] = heading; + if ( panel.selectedCategory.empty() ) + panel.selectedCategory = id; +} + +void SettingsPanel::setupCategories( PanelState& panel ) { + auto model = SettingsCategoryModel::create( panel.categoryItems, panel.categoryIds ); + panel.categoryModel = model; + panel.categories->setHeadersVisible( false ); + panel.categories->setAutoExpandOnSingleColumn( true ); + panel.categories->setFocusOnSelection( true ); + panel.categories->setModel( model ); + panel.categories->expandAll(); + panel.connections += + panel.categories->connect( Event::OnSelectionChanged, [this, &panel]( const Event* ) { + auto index = panel.categories->getSelection().first(); + if ( !index.isValid() ) + return; + auto* node = static_cast( index.internalData() ); + if ( !node || node->id.empty() ) + return; + panel.selectedCategory = node->id; + panel.pageTitle->setText( node->text ); + filter( panel ); + panel.scroll->getVerticalScrollBar()->setValue( 0, false ); + } ); +} + +UIWidget* SettingsPanel::createRow( PanelState& panel, SettingBinding& binding ) { + auto* row = mApp->getUISceneNode()->loadLayoutFromString( SETTINGS_ROW_LAYOUT, panel.settings ); + row->setId( "setting_" + binding.id ); + row->find( "setting_name" )->setText( binding.name ); + auto* description = row->find( "setting_description" ); + description->setText( binding.description ); + binding.row = row; + return row; +} + +void SettingsPanel::addBool( PanelState& panel, SettingBinding binding, bool* value, + std::function apply ) { + auto* check = createBoolControl( panel, binding ); + auto valueBinding = + UIDataBind::New( value, check, UIValueConverter::converterBool() ); + valueBinding->onValueChangeCb = std::move( apply ); + panel.bindingGroup += std::move( valueBinding ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +void SettingsPanel::addBool( PanelState& panel, SettingBinding binding, std::function get, + std::function set ) { + auto* check = createBoolControl( panel, binding ); + check->setChecked( get() ); + panel.connections += + check->connect( Event::OnValueChange, [check, set = std::move( set )]( const Event* ) { + set( check->isChecked() ); + } ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +UICheckBox* SettingsPanel::createBoolControl( PanelState& panel, SettingBinding& binding ) { + auto* row = createRow( panel, binding ); + row->addClass( "settings_boolean_option" ); + auto* check = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_BOOL_LAYOUT, + row->find( "setting_control" ) ) + ->asType(); + auto toggle = [check]( const Event* ) { check->setChecked( !check->isChecked() ); }; + panel.connections += + row->find( "setting_name" )->connect( Event::MouseClick, toggle ); + panel.connections += row->find( "setting_description" ) + ->connect( Event::MouseClick, std::move( toggle ) ); + return check; +} + +void SettingsPanel::addChoice( PanelState& panel, SettingBinding binding, + const std::vector& choices, std::function get, + std::function set, + std::vector choiceDescriptions ) { + auto* row = createRow( panel, binding ); + auto* dropDown = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_CHOICE_LAYOUT, + row->find( "setting_control" ) ) + ->asType(); + dropDown->getListBox()->addListBoxItems( choices ); + const size_t selected = get(); + dropDown->getListBox()->setSelected( selected ); + if ( selected < choiceDescriptions.size() ) + dropDown->setTooltipText( choiceDescriptions[selected] ); + panel.connections += dropDown->connect( + Event::OnValueChange, [dropDown, set = std::move( set ), + descriptions = std::move( choiceDescriptions )]( const Event* ) { + if ( !dropDown->getListBox()->hasSelection() ) + return; + const size_t selected = dropDown->getListBox()->getItemSelectedIndex(); + if ( selected < descriptions.size() ) + dropDown->setTooltipText( descriptions[selected] ); + set( selected ); + } ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +void SettingsPanel::addInteger( PanelState& panel, SettingBinding binding, int min, int max, + int* value, std::function apply ) { + 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 ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +void SettingsPanel::addAction( PanelState& panel, SettingBinding binding, const String& buttonText, + std::function action ) { + auto* row = createRow( panel, binding ); + auto* button = mApp->getUISceneNode() + ->loadLayoutFromString( SETTINGS_ACTION_LAYOUT, + row->find( "setting_control" ) ) + ->asType(); + button->setText( buttonText ); + panel.connections += button->connect( + Event::MouseClick, [action = std::move( action )]( const Event* ) { action(); } ); + panel.bindings.emplace_back( std::move( binding ) ); +} + +void SettingsPanel::addUserSettings( PanelState& panel ) { + addCategory( panel, "general.behavior", mApp->i18n( "general", "General" ), + mApp->i18n( "behavior", "Behavior" ) ); + addBool( panel, + { "welcomeScreen", "general.behavior", + mApp->i18n( "welcome_screen_enable", "Enable Welcome Screen" ), + mApp->i18n( "welcome_screen_enable_desc", + "Show the welcome screen when no document or folder is open." ) }, + &mApp->getConfig().ui.welcomeScreen ); + addBool( + panel, + { "openFilesInNewWindow", "general.behavior", + mApp->i18n( "open_files_in_new_window_enable", "Open Files in New Window" ), + mApp->i18n( + "open_files_in_new_window_desc", + "Open files received from the file explorer or command line in a new window." ) }, + &mApp->getConfig().ui.openFilesInNewWindow ); + addBool( + panel, + { "openProjectInNewWindow", "general.behavior", + mApp->i18n( "open_project_in_new_window", "Open Project in New Window" ), + mApp->i18n( "open_project_in_new_window_tooltip", + "Open folders in a new window when another project is already loaded." ) }, + &mApp->getConfig().ui.openProjectInNewWindow ); + addBool( panel, + { "nativeFileDialogs", "general.behavior", + mApp->i18n( "use_native_file_dialogs", "Enable Native File Dialogs" ), + mApp->i18n( "use_native_file_dialogs_tooltip", + "Use operating-system file dialogs when available." ) }, + &mApp->getConfig().ui.nativeFileDialogs ); + addBool( panel, + { "imagesQuickPreview", "general.behavior", + mApp->i18n( "quick_preview_images", "Quick Preview Images" ), + mApp->i18n( "quick_preview_images_tooltip", + "Preview images without opening a permanent editor tab." ) }, + &mApp->getConfig().ui.imagesQuickPreview ); + + addCategory( panel, "editor.appearance", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "appearance", "Appearance" ) ); + std::vector editorSchemeNames; + std::vector editorSchemeIds; + for ( const auto& [name, scheme] : mApp->getSplitter()->getColorSchemes() ) { + editorSchemeNames.emplace_back( name ); + editorSchemeIds.emplace_back( name ); + } + auto selectedEditorScheme = + std::find( editorSchemeNames.begin(), editorSchemeNames.end(), + String( mApp->getSplitter()->getCurrentColorSchemeName() ) ); + const size_t selectedEditorSchemeIndex = + selectedEditorScheme == editorSchemeNames.end() + ? 0 + : static_cast( selectedEditorScheme - editorSchemeNames.begin() ); + if ( !editorSchemeNames.empty() ) + addChoice( + panel, + { "editorColorScheme", "editor.appearance", + mApp->i18n( "syntax_color_scheme", "Syntax Color Scheme" ), + mApp->i18n( "syntax_color_scheme_desc", + "Choose the colors used for syntax highlighting and editor surfaces." ) }, + editorSchemeNames, [selectedEditorSchemeIndex] { return selectedEditorSchemeIndex; }, + [this, editorSchemeIds = std::move( editorSchemeIds )]( size_t selected ) { + mApp->getSplitter()->setColorScheme( + editorSchemeIds[std::min( selected, editorSchemeIds.size() - 1 )] ); + } ); + auto addEditorBool = [this, &panel]( std::string id, const char* nameKey, const char* name, + const char* descriptionKey, const char* description, + bool CodeEditorConfig::* member, auto apply ) { + addBool( panel, + { std::move( id ), "editor.appearance", mApp->i18n( nameKey, name ), + mApp->i18n( descriptionKey, description ) }, + &( mApp->getConfig().editor.*member ), [this, apply]( bool value ) { + mApp->getSplitter()->forEachEditor( + [value, apply]( UICodeEditor* editor ) { apply( editor, value ); } ); + } ); + }; + addEditorBool( "showLineNumbers", "show_line_numbers", "Show Line Numbers", + "show_line_numbers_desc", "Display line numbers beside the editor.", + &CodeEditorConfig::showLineNumbers, + []( UICodeEditor* editor, bool value ) { editor->setShowLineNumber( value ); } ); + addEditorBool( + "showWhiteSpaces", "show_white_spaces", "Show White Spaces", "show_white_spaces_desc", + "Display visible markers for whitespace characters.", &CodeEditorConfig::showWhiteSpaces, + []( UICodeEditor* editor, bool value ) { editor->setShowWhitespaces( value ); } ); + addEditorBool( + "highlightCurrentLine", "highlight_current_line", "Highlight Current Line", + "highlight_current_line_desc", "Highlight the line containing the primary cursor.", + &CodeEditorConfig::highlightCurrentLine, + []( UICodeEditor* editor, bool value ) { editor->setHighlightCurrentLine( value ); } ); + addBool( panel, + { "showDocumentInfo", "editor.appearance", + mApp->i18n( "show_doc_info", "Show Document Info" ), + mApp->i18n( "show_doc_info_desc", + "Display document encoding and line-ending information." ) }, + &mApp->getConfig().editor.showDocInfo, [this]( bool value ) { + if ( mApp->getDocInfo() ) + mApp->getDocInfo()->setVisible( value ); + } ); + addEditorBool( + "showLineEndings", "show_line_endings", "Show Line Endings", "show_line_endings_desc", + "Display markers for line-ending characters.", &CodeEditorConfig::showLineEndings, + []( UICodeEditor* editor, bool value ) { editor->setShowLineEndings( value ); } ); + addEditorBool( + "showIndentationGuides", "show_indentation_guides", "Show Indentation Guides", + "show_indentation_guides_desc", "Display vertical guides for indentation levels.", + &CodeEditorConfig::showIndentationGuides, + []( UICodeEditor* editor, bool value ) { editor->setShowIndentationGuides( value ); } ); + addEditorBool( "minimap", "show_minimap", "Show Minimap", "show_minimap_desc", + "Display a compact overview of the document.", &CodeEditorConfig::minimap, + []( UICodeEditor* editor, bool value ) { editor->showMinimap( value ); } ); + addEditorBool( + "relativeLinePositions", "show_lines_relative_position", "Show Lines Relative Position", + "show_lines_relative_position_desc", "Show line numbers relative to the primary cursor.", + &CodeEditorConfig::linesRelativePosition, + []( UICodeEditor* editor, bool value ) { editor->showLinesRelativePosition( value ); } ); + addEditorBool( + "highlightMatchingBracket", "highlight_matching_brackets", "Highlight Matching Bracket", + "highlight_matching_brackets_desc", + "Highlight the bracket paired with the one beside the cursor.", + &CodeEditorConfig::highlightMatchingBracket, + []( UICodeEditor* editor, bool value ) { editor->setHighlightMatchingBracket( value ); } ); + addEditorBool( + "highlightSelectionMatch", "highlight_selection_match", "Highlight Selection Match", + "highlight_selection_match_desc", "Highlight text matching the current selection.", + &CodeEditorConfig::highlightSelectionMatch, + []( UICodeEditor* editor, bool value ) { editor->setHighlightSelectionMatch( value ); } ); + addEditorBool( "verticalScrollbar", "enable_vertical_scrollbar", "Enable Vertical Scrollbar", + "enable_vertical_scrollbar_desc", "Display the editor's vertical scrollbar.", + &CodeEditorConfig::verticalScrollbar, []( UICodeEditor* editor, bool value ) { + editor->setVerticalScrollBarEnabled( value ); + } ); + addEditorBool( "horizontalScrollbar", "enable_horizontal_scrollbar", + "Enable Horizontal Scrollbar", "enable_horizontal_scrollbar_desc", + "Display the editor's horizontal scrollbar.", + &CodeEditorConfig::horizontalScrollbar, []( UICodeEditor* editor, bool value ) { + editor->setHorizontalScrollBarEnabled( value ); + } ); + addEditorBool( "inlineColorBoxes", "enable_inline_color_boxes", "Enable Color Boxes", + "enable_inline_color_boxes_tooltip", + "Display inline color boxes beside recognized color values.", + &CodeEditorConfig::inlineColorBoxes, []( UICodeEditor* editor, bool value ) { + editor->setEnableInlineColorBoxes( value ); + } ); + addEditorBool( "colorPreview", "enable_color_preview", "Enable Color Preview", + "enable_color_preview_tooltip", "Preview selected color values in the editor.", + &CodeEditorConfig::colorPreview, + []( UICodeEditor* editor, bool value ) { editor->setColorPreview( value ); } ); + addEditorBool( "colorPickerSelection", "enable_color_picker", "Enable Color Picker", + "enable_color_picker_tooltip", + "Open the color picker for recognized color selections.", + &CodeEditorConfig::colorPickerSelection, []( UICodeEditor* editor, bool value ) { + editor->setEnableColorPickerOnSelection( value ); + } ); + + addCategory( panel, "editor.navigation", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "navigation", "Navigation" ) ); + addBool( panel, + { "singleClickNavigation", "editor.navigation", + mApp->i18n( "treeview_single_click_nav", "Single Click Navigation" ), + mApp->i18n( "treeview_single_click_nav_tooltip", + "Open project-tree files with a single click." ) }, + &mApp->getConfig().editor.singleClickNavigation, [this]( bool value ) { + if ( mApp->getProjectTreeView() ) + mApp->getProjectTreeView()->setSingleClickNavigation( value ); + } ); + addBool( panel, + { "syncProjectTree", "editor.navigation", + mApp->i18n( "sync_project_tree", "Synchronize Project Tree with Editor" ), + mApp->i18n( "sync_project_tree_tooltip", + "Select the active document in the project tree." ) }, + &mApp->getConfig().editor.syncProjectTreeWithEditor ); + addBool( + panel, + { "restoreSelectionOnFocus", "editor.navigation", + mApp->i18n( "restore_editor_selection_on_focus", "Restore Editor Selection on Focus" ), + mApp->i18n( "restore_editor_selection_on_focus_tooltip", + "Restore each editor split's last cursor and selection when focused." ) }, + &mApp->getConfig().editor.restoreEditorSelectionOnFocus, + [this]( bool value ) { mApp->getSplitter()->setRestoreEditorSelectionOnFocus( value ); } ); + + addCategory( panel, "editor.formatting", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "formatting", "Formatting" ) ); + addChoice( + panel, + { "lineWrapMode", "editor.formatting", mApp->i18n( "line_wrap", "Line Wrap" ), + mApp->i18n( "line_wrap_desc", "Controls how long lines wrap in the editor." ) }, + { mApp->i18n( "no_wrap", "No Wrap" ), mApp->i18n( "wrap_word", "Word Wrap" ), + mApp->i18n( "wrap_letter", "Letter Wrap" ) }, + [this] { + switch ( mApp->getConfig().editor.wrapMode ) { + case LineWrapMode::NoWrap: + return size_t{ 0 }; + case LineWrapMode::Letter: + return size_t{ 2 }; + case LineWrapMode::Word: + default: + return size_t{ 1 }; + } + }, + [this]( size_t selected ) { + auto mode = selected == 0 ? LineWrapMode::NoWrap + : selected == 2 ? LineWrapMode::Letter + : LineWrapMode::Word; + mApp->getConfig().editor.wrapMode = mode; + mApp->getSplitter()->forEachEditor( + [mode]( UICodeEditor* editor ) { editor->setLineWrapMode( mode ); } ); + } ); + addInteger( + panel, + { "lineBreakingColumn", "editor.formatting", + 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 ) { + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setLineBreakingColumn( value ); } ); + } ); + addChoice( + panel, + { "lineWrapType", "editor.formatting", mApp->i18n( "wrap_type", "Wrap Against" ), + mApp->i18n( "wrap_type_desc", + "Choose whether lines wrap at the viewport or line breaking column." ) }, + { mApp->i18n( "viewport", "Viewport" ), + mApp->i18n( "line_breaking_column", "Line Breaking Column" ) }, + [this] { return mApp->getConfig().editor.wrapType == LineWrapType::Viewport ? 0 : 1; }, + [this]( size_t selected ) { + auto value = selected == 0 ? LineWrapType::Viewport : LineWrapType::LineBreakingColumn; + mApp->getConfig().editor.wrapType = value; + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setLineWrapType( value ); } ); + } ); + addBool( panel, + { "wrapKeepIndentation", "editor.formatting", + mApp->i18n( "keep_indentation", "Keep Indentation" ), + mApp->i18n( "keep_indentation_desc", + "Preserve indentation on wrapped continuation lines." ) }, + &mApp->getConfig().editor.wrapKeepIndentation, [this]( bool value ) { + mApp->getSplitter()->forEachEditor( [value]( UICodeEditor* editor ) { + editor->setLineWrapKeepIndentation( value ); + } ); + } ); + + addCategory( panel, "editor.folding", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "code_folding", "Code Folding" ) ); + addBool( panel, + { "codeFoldingEnabled", "editor.folding", mApp->i18n( "enabled", "Enabled" ), + mApp->i18n( "code_folding_enabled_desc", "Enable syntax-aware code folding." ) }, + &mApp->getConfig().editor.codeFoldingEnabled, [this]( bool value ) { + mApp->getSplitter()->forEachDoc( [value]( TextDocument& doc ) { + doc.getFoldRangeService().setEnabled( value ); + } ); + } ); + addBool( panel, + { "codeFoldingAlwaysVisible", "editor.folding", + mApp->i18n( "code_folding_always_display_folds", "Folds Always Visible" ), + mApp->i18n( "code_folding_always_visible_desc", + "Always display fold controls in the editor gutter." ) }, + &mApp->getConfig().editor.codeFoldingAlwaysVisible, [this]( bool value ) { + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setFoldsAlwaysVisible( value ); } ); + } ); + + addCategory( panel, "editor.tabs", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "tab_bar", "Tab Bar" ) ); + addBool( panel, + { "hideTabBar", "editor.tabs", mApp->i18n( "hide_tabbar", "Hide Tab Bar" ), + mApp->i18n( "hide_tabbar_tooltip", "Always hide the tab bar." ) }, + &mApp->getConfig().editor.hideTabBar, + [this]( bool value ) { mApp->getSplitter()->setHideTabBar( value ); } ); + addBool( panel, + { "hideTabBarOnSingleTab", "editor.tabs", + mApp->i18n( "hide_tabbar_on_single_tab", "Hide Tab Bar on Single Tab" ), + mApp->i18n( "hide_tabbar_on_single_tab_tooltip", + "Hide the tab bar when a tab widget contains only one item." ) }, + &mApp->getConfig().editor.hideTabBarOnSingleTab, + [this]( bool value ) { mApp->getSplitter()->setHideTabBarOnSingleTab( value ); } ); + addBool( panel, + { "tabSwitcher", "editor.tabs", + mApp->i18n( "display_tab_switcher", "Display Tab Switcher" ), + mApp->i18n( "display_tab_switcher_tooltip", + "Display a tab switcher in the center of each tab widget." ) }, + &mApp->getConfig().editor.tabSwitcher ); + addBool( panel, + { "openDocumentsInMainSplit", "editor.tabs", + mApp->i18n( "open_documents_in_main_split", "Open Documents in Main Split" ), + mApp->i18n( "open_documents_in_main_split_desc", + "Open externally requested documents in the main editor split." ) }, + &mApp->getConfig().editor.openDocumentsInMainSplit ); + addChoice( + panel, + { "tabJumpMode", "editor.tabs", mApp->i18n( "tab_jump_mode", "Tab Jump Mode" ), + mApp->i18n( "tab_jump_mode_desc", + "Choose how keyboard navigation cycles through tabs." ) }, + { mApp->i18n( "linear", "Linear" ), mApp->i18n( "chronological", "Chronological" ) }, + [this] { + return mApp->getConfig().editor.tabJumpMode == UITabWidget::TabJumpMode::Linear ? 0 : 1; + }, + [this]( size_t selected ) { + mApp->getConfig().editor.tabJumpMode = selected == 0 + ? UITabWidget::TabJumpMode::Linear + : UITabWidget::TabJumpMode::Chronological; + }, + { mApp->i18n( "jump_mode_linear_tooltip", + "Linear Jump Mode will switch tabs in the order they are displayed" ), + mApp->i18n( + "jump_mode_chronological_tooltip", + "Chronological Jump Mode will switch tabs in the last focused / visited order." ) } ); + addChoice( + panel, + { "newTabPosition", "editor.tabs", mApp->i18n( "new_tab_position", "New Tab Position" ), + mApp->i18n( "new_tab_position_desc", "Choose where newly opened tabs are inserted." ) }, + { mApp->i18n( "new_tab_position_after_active", "After Active Tab" ), + mApp->i18n( "new_tab_position_last", "Last" ), + mApp->i18n( "new_tab_position_first", "First" ), + mApp->i18n( "new_tab_position_left_of_active", "Left of Active Tab" ) }, + [this] { + switch ( mApp->getConfig().editor.newTabPosition ) { + case NewTabPosition::AfterActive: + return size_t{ 0 }; + case NewTabPosition::First: + return size_t{ 2 }; + case NewTabPosition::LeftOfActive: + return size_t{ 3 }; + default: + return size_t{ 1 }; + } + }, + [this]( size_t selected ) { + static constexpr NewTabPosition::Position positions[] = { + NewTabPosition::AfterActive, NewTabPosition::Last, NewTabPosition::First, + NewTabPosition::LeftOfActive }; + mApp->getConfig().editor.newTabPosition = positions[std::min( selected, size_t{ 3 } )]; + } ); + + addCategory( panel, "editor.document", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "document_defaults", "Document Defaults" ) ); + auto addDocumentBool = [this, &panel]( std::string id, const char* nameKey, const char* name, + const char* descriptionKey, const char* description, + bool DocumentConfig::* member, + std::function apply = {} ) { + addBool( panel, + { std::move( id ), "editor.document", mApp->i18n( nameKey, name ), + mApp->i18n( descriptionKey, description ) }, + &( mApp->getConfig().doc.*member ), std::move( apply ) ); + }; + addDocumentBool( "autoDetectIndentType", "auto_detect_indent_type_and_width", + "Auto Detect Indent Type & Width", "auto_detect_indent_type_desc", + "Detect indentation settings when opening each document.", + &DocumentConfig::autoDetectIndentType ); + addChoice( + panel, + { "autoIndent", "editor.document", mApp->i18n( "auto_indent", "Auto-Indent" ), + mApp->i18n( "auto_indent_tooltip", "Control indentation added after pressing Enter." ) }, + { mApp->i18n( "auto_indent_none", "None" ), + mApp->i18n( "auto_indent_preserve", "Preserve" ), + mApp->i18n( "auto_indent_smart", "Smart" ) }, + [this] { return static_cast( mApp->getConfig().doc.autoIndent ); }, + [this]( size_t selected ) { + auto value = + static_cast( std::min( selected, size_t{ 2 } ) ); + mApp->getConfig().doc.autoIndent = value; + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->getDocument().setAutoIndent( value ); } ); + }, + { mApp->i18n( "auto_indent_none_desc", "No automatic indentation." ), + mApp->i18n( "auto_indent_preserve_desc", + "Preserve the indentation level of the previous line." ), + mApp->i18n( "auto_indent_smart_desc", + "Preserve indentation and indent between auto-closed brackets." ) } ); + addDocumentBool( "indentSpaces", "indent_spaces", "Indent Using Spaces", "indent_spaces_desc", + "Insert spaces instead of tab characters for indentation.", + &DocumentConfig::indentSpaces ); + addDocumentBool( "trimTrailingWhitespaces", "trim_trailing_whitespaces", + "Trim Trailing Whitespaces", "trim_trailing_whitespaces_desc", + "Remove trailing whitespace when saving files.", + &DocumentConfig::trimTrailingWhitespaces ); + addDocumentBool( "forceNewLineAtEndOfFile", "force_new_line_at_end_of_file", + "Force New Line at End of File", "force_new_line_at_end_of_file_desc", + "Ensure saved files end with a newline.", + &DocumentConfig::forceNewLineAtEndOfFile ); + addDocumentBool( "writeUnicodeBOM", "write_unicode_bom", "Write Unicode BOM", + "write_unicode_bom_desc", "Write a Unicode byte-order mark when saving.", + &DocumentConfig::writeUnicodeBOM ); + addDocumentBool( "tabStops", "tab_stops", "Tab Stops", "tab_stops_desc", + "Align tabs to consistent positions on the tab grid.", + &DocumentConfig::tabStops, [this]( bool value ) { + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setTabStops( value ); } ); + } ); + addDocumentBool( "tabOut", "tab_out", "Tab Out", "tab_out_desc", + "Press Tab before a closing character to move past it.", + &DocumentConfig::tabOutEnabled, [this]( bool value ) { + mApp->getSplitter()->forEachEditor( [value]( UICodeEditor* editor ) { + 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 ); + addChoice( + panel, + { "lineEndings", "editor.document", mApp->i18n( "line_endings", "Line Endings" ), + mApp->i18n( "line_endings_desc", "Default line-ending sequence for new files." ) }, + { "Windows (CR/LF)", "Unix (LF)", "Macintosh (CR)" }, + [this] { + switch ( mApp->getConfig().doc.lineEndings ) { + case TextFormat::LineEnding::CRLF: + return size_t{ 0 }; + case TextFormat::LineEnding::CR: + return size_t{ 2 }; + default: + return size_t{ 1 }; + } + }, + [this]( size_t selected ) { + mApp->getConfig().doc.lineEndings = + selected == 0 + ? TextFormat::LineEnding::CRLF + : ( selected == 2 ? TextFormat::LineEnding::CR : TextFormat::LineEnding::LF ); + } ); + addBool( panel, + { "autoCloseXMLTags", "editor.document", + mApp->i18n( "auto_close_xml_tags", "Auto Close XML Tags" ), + mApp->i18n( "auto_close_xml_tags_desc", "Automatically insert closing XML tags." ) }, + &mApp->getConfig().editor.autoCloseXMLTags, [this]( bool value ) { + mApp->getSplitter()->forEachEditor( + [value]( UICodeEditor* editor ) { editor->setAutoCloseXMLTags( value ); } ); + } ); + auto addAutoClosePair = [this, &panel]( std::string id, const char* nameKey, const char* name, + std::string pair ) { + auto containsPair = [this, pair] { + auto pairs = String::split( mApp->getConfig().editor.autoCloseBrackets, ',' ); + return std::find( pairs.begin(), pairs.end(), pair ) != pairs.end(); + }; + addBool( panel, + { std::move( id ), "editor.document", mApp->i18n( nameKey, name ), + mApp->i18n( "auto_close_pair_desc", + "Automatically insert the matching closing character." ) }, + std::move( containsPair ), [this, pair = std::move( pair )]( bool enabled ) { + auto pairs = String::split( mApp->getConfig().editor.autoCloseBrackets, ',' ); + auto found = std::find( pairs.begin(), pairs.end(), pair ); + if ( enabled && found == pairs.end() ) + pairs.emplace_back( pair ); + else if ( !enabled && found != pairs.end() ) + pairs.erase( found ); + mApp->getConfig().editor.autoCloseBrackets = String::join( pairs, ',' ); + auto closePairs = + mApp->makeAutoClosePairs( mApp->getConfig().editor.autoCloseBrackets ); + mApp->getSplitter()->forEachEditor( [closePairs]( UICodeEditor* editor ) { + editor->getDocument().setAutoCloseBrackets( !closePairs.empty() ); + editor->getDocument().setAutoCloseBracketsPairs( closePairs ); + } ); + } ); + }; + addAutoClosePair( "autoCloseBrackets", "brackets", "Brackets ()", "()" ); + addAutoClosePair( "autoCloseCurlyBrackets", "curly_brackets", "Curly Brackets {}", "{}" ); + addAutoClosePair( "autoCloseSquareBrackets", "square_brakcets", "Square Brackets []", "[]" ); + addAutoClosePair( "autoCloseSingleQuotes", "single_quotes", "Single Quotes ''", "''" ); + addAutoClosePair( "autoCloseDoubleQuotes", "double_quotes", "Double Quotes \"\"", "\"\"" ); + addAutoClosePair( "autoCloseBackQuotes", "back_quotes", "Back Quotes ``", "``" ); + addBool( panel, + { "autoReloadOnDiskChange", "editor.document", + mApp->i18n( "autoreload_on_disk_change", "Auto-Reload on Disk Change" ), + mApp->i18n( "autoreload_on_disk_change_desc", + "Reload unmodified documents when their files change on disk." ) }, + &mApp->getConfig().editor.autoReloadOnDiskChange ); + + addCategory( panel, "general.workspace", mApp->i18n( "general", "General" ), + mApp->i18n( "workspace", "Workspace" ) ); + addBool( panel, + { "sessionSnapshot", "general.workspace", + mApp->i18n( "session_snapshot", "Session Snapshot & Periodic Backup" ), + mApp->i18n( "session_snapshot_desc", + "Preserve unsaved document changes between sessions." ) }, + &mApp->getConfig().workspace.sessionSnapshot ); + addBool( panel, + { "restoreLastSession", "general.workspace", + mApp->i18n( "restore_last_session", "Restore Last Session" ), + mApp->i18n( "restore_last_session_desc", + "Reopen the previous workspace and documents at startup." ) }, + &mApp->getConfig().workspace.restoreLastSession ); + addBool( panel, + { "checkForUpdatesAtStartup", "general.workspace", + mApp->i18n( "check_updates_at_startup", "Check for Updates at Startup" ), + mApp->i18n( "check_updates_at_startup_desc", + "Check whether a newer ecode release is available after startup." ) }, + &mApp->getConfig().workspace.checkForUpdatesAtStartup ); + + addCategory( panel, "appearance.theme", mApp->i18n( "appearance", "Appearance" ), + mApp->i18n( "theme_and_language", "Theme & Language" ) ); + addChoice( + panel, + { "uiColorScheme", "appearance.theme", + mApp->i18n( "ui_prefes_color_scheme", "UI Prefers Color Scheme" ), + mApp->i18n( + "ui_prefers_color_scheme_desc", + "Choose whether the interface follows the system, light, or dark appearance." ) }, + { mApp->i18n( "system", "System" ), mApp->i18n( "light", "Light" ), + mApp->i18n( "dark", "Dark" ) }, + [this] { + switch ( mApp->getUIColorScheme() ) { + case ColorSchemeExtPreference::System: + return size_t{ 0 }; + case ColorSchemeExtPreference::Light: + return size_t{ 1 }; + default: + return size_t{ 2 }; + } + }, + [this]( size_t selected ) { + static constexpr ColorSchemeExtPreference values[] = { ColorSchemeExtPreference::System, + ColorSchemeExtPreference::Light, + ColorSchemeExtPreference::Dark }; + mApp->setUIColorSchemeFromUserInteraction( values[std::min( selected, size_t{ 2 } )] ); + }, + { mApp->i18n( + "prefers_color_scheme_system_tooltip", + "System options will try to pick the system-wide currently preferred color scheme." ), + mApp->i18n( "prefers_color_scheme_light_tooltip", + "Always use the light interface color scheme." ), + mApp->i18n( "prefers_color_scheme_dark_tooltip", + "Always use the dark interface color scheme." ) } ); + std::vector themeNames{ mApp->i18n( "default_theme", "Default Theme" ), + mApp->i18n( "syntax_color_scheme", "Syntax Color Scheme" ) }; + std::vector themeIds{ "default_theme", "syntax_color_scheme" }; + for ( const auto& file : + FileSystem::filesInfoGetInPath( mApp->getThemesPath(), true, true, true ) ) { + if ( file.getExtension() != "css" ) + continue; + auto name = FileSystem::fileRemoveExtension( file.getFileName() ); + themeNames.emplace_back( name ); + themeIds.emplace_back( std::move( name ) ); + } + auto currentTheme = + mApp->getConfig().ui.theme.empty() ? "default_theme" : mApp->getConfig().ui.theme; + auto selectedTheme = std::find( themeIds.begin(), themeIds.end(), currentTheme ); + const size_t selectedThemeIndex = selectedTheme == themeIds.end() + ? 0 + : static_cast( selectedTheme - themeIds.begin() ); + addChoice( + panel, + { "uiTheme", "appearance.theme", mApp->i18n( "ui_thene", "UI Theme" ), + mApp->i18n( "ui_theme_desc", + "Choose the stylesheet used by the application interface." ) }, + themeNames, [selectedThemeIndex] { return selectedThemeIndex; }, + [this, themeIds = std::move( themeIds )]( size_t selected ) { + const auto& id = themeIds[std::min( selected, themeIds.size() - 1 )]; + mApp->getConfig().ui.theme = id == "default_theme" ? "" : id; + mApp->setTheme( + id == "default_theme" + ? mApp->getDefaultThemePath() + : ( id == "syntax_color_scheme" ? id : mApp->getThemesPath() + id + ".css" ) ); + } ); + std::map languages; + for ( const auto& file : + FileSystem::filesInfoGetInPath( mApp->geti18nPath(), false, true, false, true ) ) { + if ( file.getExtension() != "xml" ) + continue; + auto id = FileSystem::fileRemoveExtension( file.getFileName() ); + std::string data; + FileSystem::fileGet( file.getFilepath(), data ); + LuaPattern pattern( "title=\"(.-)\"" ); + PatternMatcher::Range matches[2]; + if ( pattern.matches( data, matches ) ) + languages[data.substr( matches[1].start, matches[1].end - matches[1].start )] = id; + else + languages[id] = id; + } + std::vector languageNames; + std::vector languageIds; + for ( const auto& [name, id] : languages ) { + languageNames.emplace_back( name ); + languageIds.emplace_back( id ); + } + auto currentLanguage = + mApp->getConfig().ui.language.empty() ? "en" : mApp->getConfig().ui.language; + auto selectedLanguage = std::find( languageIds.begin(), languageIds.end(), currentLanguage ); + const size_t selectedLanguageIndex = + selectedLanguage == languageIds.end() + ? 0 + : static_cast( selectedLanguage - languageIds.begin() ); + if ( !languageIds.empty() ) + addChoice( + panel, + { "uiLanguage", "appearance.theme", mApp->i18n( "ui_language", "UI Language" ), + mApp->i18n( "ui_language_desc", + "Choose the interface language. Restart required." ) }, + languageNames, [selectedLanguageIndex] { return selectedLanguageIndex; }, + [this, languageIds = std::move( languageIds )]( size_t selected ) { + mApp->getConfig().ui.language = + languageIds[std::min( selected, languageIds.size() - 1 )]; + } ); + + addCategory( panel, "appearance.fonts", mApp->i18n( "appearance", "Appearance" ), + mApp->i18n( "fonts_and_scale", "Fonts & Scale" ) ); + addAction( + panel, + { "uiFont", "appearance.fonts", + mApp->i18n( "ui_font_and_size_ellipsis", "UI Font & Size..." ), + mApp->i18n( "ui_font_desc", "Choose the proportional font used by the interface." ) }, + mApp->i18n( "choose_font", "Choose Font..." ), + [this] { mApp->runCommand( "sans-serif-font" ); } ); + addAction( + panel, + { "editorFont", "appearance.fonts", + mApp->i18n( "editor_font_and_size_ellipsis", "Editor Font & Size..." ), + mApp->i18n( "editor_font_desc", "Choose the monospace font used by code editors." ) }, + mApp->i18n( "choose_font", "Choose Font..." ), + [this] { mApp->runCommand( "editor-font" ); } ); + addAction( + panel, + { "terminalFont", "appearance.fonts", + mApp->i18n( "terminal_font_and_size_ellipsis", "Terminal Font & Size..." ), + mApp->i18n( "terminal_font_desc", "Choose the monospace font used by terminals." ) }, + mApp->i18n( "choose_font", "Choose Font..." ), + [this] { mApp->runCommand( "terminal-font" ); } ); + addAction( panel, + { "fallbackFont", "appearance.fonts", + mApp->i18n( "fallback_font_ellipsis", "Fallback Font..." ), + 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, + { "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, + { "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(); } ); + addBool( panel, + { "editorFontInInputFields", "appearance.fonts", + mApp->i18n( "editor_font_in_input_fields", "Editor Font in Input Fields" ), + mApp->i18n( "editor_font_in_input_fields_desc", + "Use the editor font for text input controls." ) }, + &mApp->getConfig().ui.editorFontInInputFields ); + addChoice( + panel, + { "fontHinting", "appearance.fonts", mApp->i18n( "ui_font_hint", "Font Hinting" ), + mApp->i18n( "ui_font_hint_desc", "Control glyph alignment to the pixel grid." ) }, + { mApp->i18n( "none", "None" ), mApp->i18n( "slight", "Slight" ), + mApp->i18n( "full", "Full" ) }, + [this] { return static_cast( mApp->getConfig().ui.fontHinting ); }, + [this]( size_t selected ) { + static constexpr FontHinting values[] = { FontHinting::None, FontHinting::Slight, + FontHinting::Full }; + auto value = values[std::min( selected, size_t{ 2 } )]; + mApp->getConfig().ui.fontHinting = value; + defaultResourceScope().getFontService().setHinting( value ); + mApp->getSplitter()->forEachWidgetType( UI_TYPE_TERMINAL, []( UIWidget* widget ) { + widget->asType()->syncFontRenderingConfig(); + } ); + } ); + addChoice( + panel, + { "fontAntialiasing", "appearance.fonts", + mApp->i18n( "ui_font_antialiasing", "Font Anti-Aliasing" ), + mApp->i18n( "ui_font_antialiasing_desc", "Choose how glyph edges are smoothed." ) }, + { mApp->i18n( "none", "None" ), mApp->i18n( "grayscale", "Grayscale" ), + mApp->i18n( "subpixel", "Subpixel" ) }, + [this] { return static_cast( mApp->getConfig().ui.fontAntialiasing ); }, + [this]( size_t selected ) { + static constexpr FontAntialiasing values[] = { + FontAntialiasing::None, FontAntialiasing::Grayscale, FontAntialiasing::Subpixel }; + auto value = values[std::min( selected, size_t{ 2 } )]; + mApp->getConfig().ui.fontAntialiasing = value; + defaultResourceScope().getFontService().setAntialiasing( value ); + mApp->getSplitter()->forEachWidgetType( UI_TYPE_TERMINAL, []( UIWidget* widget ) { + widget->asType()->syncFontRenderingConfig(); + } ); + } ); + auto addFontFeature = [this, &panel]( std::string id, const char* nameKey, const char* name, + const char* descriptionKey, const char* description, + Uint32 feature, bool editorFeature ) { + addBool( + panel, + { std::move( id ), "appearance.fonts", mApp->i18n( nameKey, name ), + mApp->i18n( descriptionKey, description ) }, + [this, feature, editorFeature] { + const Uint32 features = editorFeature ? mApp->getConfig().editor.fontFeatures + : mApp->getConfig().ui.fontFeatures; + return ( features & feature ) != 0; + }, + [this, feature, editorFeature]( bool enabled ) { + Uint32& features = editorFeature ? mApp->getConfig().editor.fontFeatures + : mApp->getConfig().ui.fontFeatures; + if ( enabled ) + features |= feature; + else + features &= ~feature; + if ( editorFeature ) { + mApp->getSplitter()->forEachEditor( [features]( UICodeEditor* editor ) { + editor->setLigatureFeatures( features ); + } ); + } else { + mApp->getUISceneNode()->setDefaultTextHints( features ); + } + } ); + }; + for ( bool editorFeature : { false, true } ) { + const std::string prefix = editorFeature ? "editor" : "ui"; + addFontFeature( prefix + "StandardLigatures", "standard_ligatures", + "Standard Ligatures (liga)", "standard_ligatures_desc", + "Typographic combinations such as fi, fl, and ffi, depending on the font.", + TextHints::StandardLigatures, editorFeature ); + addFontFeature( prefix + "ContextualAlternates", "contextual_alternates", + "Contextual Alternates (calt)", "contextual_alternates_desc", + "Context-dependent alternatives, including many programming ligatures.", + TextHints::ContextualAlternates, editorFeature ); + addFontFeature( prefix + "ContextualLigatures", "contextual_ligatures", + "Contextual Ligatures (clig)", "contextual_ligatures_desc", + "Ligatures applied in specific contexts to improve readability.", + TextHints::ContextualLigatures, editorFeature ); + addFontFeature( prefix + "DiscretionaryLigatures", "discretionary_ligatures", + "Discretionary Ligatures (dlig)", "discretionary_ligatures_desc", + "Optional decorative or stylistic ligatures provided by the font.", + TextHints::DiscretionaryLigatures, editorFeature ); + } + + 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(); } ); + addChoice( + panel, + { "indentTabAlignment", "editor.advanced", + mApp->i18n( "indent_tab_alignment", "Indent Tab Alignment" ), + mApp->i18n( "indent_tab_alignment_desc", + "Align text within the visual width of indentation tabs." ) }, + { mApp->i18n( "left", "Left" ), mApp->i18n( "center", "Center" ), + mApp->i18n( "right", "Right" ) }, + [this] { + return mApp->getConfig().editor.tabIndentAlignment == CharacterAlignment::Left + ? size_t{ 0 } + : ( mApp->getConfig().editor.tabIndentAlignment == CharacterAlignment::Center + ? size_t{ 1 } + : size_t{ 2 } ); + }, + [this]( size_t selected ) { + static constexpr CharacterAlignment values[] = { + CharacterAlignment::Left, CharacterAlignment::Center, CharacterAlignment::Right }; + auto value = values[std::min( selected, size_t{ 2 } )]; + mApp->getConfig().editor.tabIndentAlignment = value; + 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(); } ); + + addCategory( panel, "window.screenshots", mApp->i18n( "window", "Window" ), + mApp->i18n( "screenshots", "Screenshots" ) ); + addAction( panel, + { "screenshotSavePath", "window.screenshots", + mApp->i18n( "set_screenshot_save_path", "Screenshot Save Path" ), + 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( + 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(); } ); + + addCategory( panel, "window.renderer", mApp->i18n( "window", "Window" ), + mApp->i18n( "renderer", "Renderer" ) ); + addBool( + panel, + { "vsync", "window.renderer", mApp->i18n( "vsync", "VSync" ), + mApp->i18n( "vsync_desc", + "Synchronize rendering with the display refresh rate. Restart required." ) }, + &mApp->getConfig().context.VSync, [this]( bool ) { + mApp->saveConfig(); + mApp->getNotificationCenter()->addNotification( + mApp->i18n( "vsync_changed", + "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" ) ); + } ); + std::vector rendererVersions = + Renderer::getAvailableGraphicsLibraryVersions(); + std::vector rendererVersionNames; + rendererVersionNames.reserve( rendererVersions.size() ); + for ( const auto version : rendererVersions ) + rendererVersionNames.emplace_back( Renderer::graphicsLibraryVersionToString( version ) ); + auto selectedRendererVersion = std::find( rendererVersions.begin(), rendererVersions.end(), + mApp->getConfig().context.Version ); + const size_t selectedRendererVersionIndex = + selectedRendererVersion == rendererVersions.end() + ? 0 + : static_cast( selectedRendererVersion - rendererVersions.begin() ); + if ( !rendererVersions.empty() ) + addChoice( + panel, + { "rendererVersion", "window.renderer", + mApp->i18n( "ui_renderer_version", "Renderer Version" ), + mApp->i18n( "ui_renderer_version_desc", + "Select the graphics API version used by ecode. Restart required." ) }, + rendererVersionNames, + [selectedRendererVersionIndex] { return selectedRendererVersionIndex; }, + [this, rendererVersions = std::move( rendererVersions )]( size_t selected ) { + mApp->getConfig().context.Version = + rendererVersions[std::min( selected, rendererVersions.size() - 1 )]; + mApp->saveConfig(); + mApp->getNotificationCenter()->addNotification( + mApp->i18n( "glversion_changed", + "Renderer version changed.\nRestart ecode to see the changes." ) + .unescape() ); + } ); + addChoice( + panel, + { "multisamples", "window.renderer", + mApp->i18n( "ui_multisamples_level", "Multisample Anti-Aliasing Level" ), + mApp->i18n( "ui_multisamples_level_desc", + "Set renderer multisampling. Restart required." ) }, + { "0", "2", "4", "8", "16" }, + [this] { + static constexpr Uint32 values[] = { 0, 2, 4, 8, 16 }; + auto found = std::find( std::begin( values ), std::end( values ), + mApp->getConfig().context.Multisamples ); + return found == std::end( values ) ? size_t{ 0 } + : size_t( found - std::begin( values ) ); + }, + [this]( size_t selected ) { + static constexpr Uint32 values[] = { 0, 2, 4, 8, 16 }; + mApp->getConfig().context.Multisamples = values[std::min( selected, size_t{ 4 } )]; + mApp->saveConfig(); + mApp->getNotificationCenter()->addNotification( + mApp->i18n( "multisamples_changed", "Multisample Anti-Aliasing Level " + "applied.\nRestart ecode to see the changes." ) + .unescape() ); + }, + { mApp->i18n( "multisamples_disabled_desc", "Disable multisample anti-aliasing." ), + mApp->i18n( "multisamples_2x_desc", "Use 2x multisample anti-aliasing." ), + mApp->i18n( "multisamples_4x_desc", "Use 4x multisample anti-aliasing." ), + mApp->i18n( "multisamples_8x_desc", "Use 8x multisample anti-aliasing." ), + mApp->i18n( "multisamples_16x_desc", "Use 16x multisample anti-aliasing." ) } ); + + addCategory( panel, "terminal.behavior", mApp->i18n( "terminal", "Terminal" ), + mApp->i18n( "behavior", "Behavior" ) ); + addChoice( + panel, + { "newTerminalOrientation", "terminal.behavior", + mApp->i18n( "new_terminal_behavior", "New Terminal Behavior" ), + mApp->i18n( "new_terminal_behavior_desc", + "Choose where newly created terminal sessions are opened." ) }, + { mApp->i18n( "open_in_same_tabbar", "Open in Current Tab Bar" ), + mApp->i18n( "open_in_vertical_split", "Open in New Vertical Split" ), + mApp->i18n( "open_in_horizontal_split", "Open in New Horizontal Split" ), + mApp->i18n( "open_in_statusbar_panel", "Open in Status Bar Panel" ) }, + [this] { + switch ( mApp->getConfig().term.newTerminalOrientation ) { + case NewTerminalOrientation::Vertical: + return size_t{ 1 }; + case NewTerminalOrientation::Horizontal: + return size_t{ 2 }; + case NewTerminalOrientation::StatusBarPanel: + return size_t{ 3 }; + default: + return size_t{ 0 }; + } + }, + [this]( size_t selected ) { + static constexpr NewTerminalOrientation::Orientation orientations[] = { + NewTerminalOrientation::Same, NewTerminalOrientation::Vertical, + NewTerminalOrientation::Horizontal, NewTerminalOrientation::StatusBarPanel }; + mApp->getConfig().term.newTerminalOrientation = + orientations[std::min( selected, size_t{ 3 } )]; + } ); + addBool( panel, + { "terminalExclusiveMode", "terminal.behavior", + mApp->i18n( "enable_exclusive_mode_by_default", "Enable Exclusive Mode by Default" ), + mApp->i18n( "enable_exclusive_mode_by_default_tooltip", + "Disable global keybindings in newly created terminals." ) }, + &mApp->getConfig().term.exclusiveMode ); + addBool( panel, + { "closeTerminalTabOnExit", "terminal.behavior", + mApp->i18n( "close_terminal_tab_on_exit", "Close Terminal Tab on Exit" ), + mApp->i18n( "close_terminal_tab_on_exit_tooltip", + "Close a terminal tab when its main process exits." ) }, + &mApp->getConfig().term.closeTerminalTabOnExit, [this]( bool value ) { + mApp->getSplitter()->forEachWidgetType( + UI_TYPE_TERMINAL, [value]( UIWidget* widget ) { + widget->asType()->getTerm()->setKeepAlive( !value ); + } ); + } ); + addBool( panel, + { "warnBeforeClosingTerminal", "terminal.behavior", + mApp->i18n( "warn_before_closing_tab", "Warn Before Closing Tab" ), + mApp->i18n( "warn_before_closing_tab_tooltip", + "Ask before closing a terminal while a program is running." ) }, + &mApp->getConfig().term.warnBeforeClosingTab ); + addAction( panel, + { "terminalShell", "terminal.behavior", + mApp->i18n( "configure_terminal_shell", "Configure Terminal Shell" ), + mApp->i18n( "configure_terminal_shell_desc", + "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" ); } ); + addAction( panel, + { "terminalWorkingDirectory", "terminal.behavior", + mApp->i18n( "configure_terminal_working_dir", + "Configure Terminal Default Working Directory" ), + mApp->i18n( "configure_terminal_working_dir_desc", + "Choose the working directory used by new terminals." ) }, + mApp->i18n( "configure", "Configure..." ), + [this] { mApp->runCommand( "configure-terminal-working-dir" ); } ); + + addCategory( panel, "terminal.appearance", mApp->i18n( "terminal", "Terminal" ), + mApp->i18n( "appearance", "Appearance" ) ); + std::vector terminalSchemeNames; + std::vector terminalSchemeIds; + for ( const auto& [name, scheme] : mApp->getTerminalManager()->getTerminalColorSchemes() ) { + terminalSchemeNames.emplace_back( name ); + terminalSchemeIds.emplace_back( name ); + } + auto selectedTerminalScheme = + std::find( terminalSchemeNames.begin(), terminalSchemeNames.end(), + String( mApp->getTerminalManager()->getTerminalCurrentColorScheme() ) ); + const size_t selectedTerminalSchemeIndex = + selectedTerminalScheme == terminalSchemeNames.end() + ? 0 + : static_cast( selectedTerminalScheme - terminalSchemeNames.begin() ); + if ( !terminalSchemeNames.empty() ) + addChoice( + panel, + { "terminalColorScheme", "terminal.appearance", + mApp->i18n( "terminal_color_scheme", "Terminal Color Scheme" ), + mApp->i18n( "terminal_color_scheme_desc", + "Choose the colors used by integrated terminals." ) }, + terminalSchemeNames, + [selectedTerminalSchemeIndex] { return selectedTerminalSchemeIndex; }, + [this, terminalSchemeIds = std::move( terminalSchemeIds )]( size_t selected ) { + mApp->getTerminalManager()->setTerminalColorScheme( + terminalSchemeIds[std::min( selected, terminalSchemeIds.size() - 1 )] ); + } ); + addChoice( + panel, + { "terminalScrollbarType", "terminal.appearance", + mApp->i18n( "scrollbar_type", "Scrollbar Type" ), + mApp->i18n( "scrollbar_type_desc", + "Place the terminal scrollbar over or outside its content." ) }, + { mApp->i18n( "overlay", "Overlay" ), mApp->i18n( "outside", "Outside" ) }, + [this] { return mApp->getConfig().term.scrollBarType == ScrollViewType::Overlay ? 0 : 1; }, + [this]( size_t selected ) { + auto value = selected == 0 ? ScrollViewType::Overlay : ScrollViewType::Outside; + mApp->getConfig().term.scrollBarType = value; + mApp->getSplitter()->forEachWidgetType( UI_TYPE_TERMINAL, [value]( UIWidget* widget ) { + widget->asType()->setScrollViewType( value ); + } ); + }, + { mApp->i18n( "scroll_overlay_tooltip", "Scrollbar appears over content." ), + mApp->i18n( "scroll_outside_tooltip", + "Scrollbar has its own space, never covers content." ) } ); + addChoice( + panel, + { "terminalCursorStyle", "terminal.appearance", + mApp->i18n( "cursor_style", "Cursor Style" ), + mApp->i18n( "cursor_style_desc", "Choose the terminal cursor shape and animation." ) }, + { mApp->i18n( "blinking_block", "Blinking Block" ), + mApp->i18n( "steady_block", "Steady Block" ), + mApp->i18n( "blink_underline", "Blink Underline" ), + mApp->i18n( "steady_underline", "Steady Underline" ), + mApp->i18n( "blink_bar", "Blink Bar" ), mApp->i18n( "steady_bar", "Steady Bar" ) }, + [this] { + static constexpr TerminalCursorMode modes[] = { + TerminalCursorMode::BlinkingBlock, TerminalCursorMode::SteadyBlock, + TerminalCursorMode::BlinkUnderline, TerminalCursorMode::SteadyUnderline, + TerminalCursorMode::BlinkBar, TerminalCursorMode::SteadyBar }; + auto found = std::find( std::begin( modes ), std::end( modes ), + mApp->getConfig().term.cursorStyle ); + return found == std::end( modes ) ? size_t{ 0 } : size_t( found - std::begin( modes ) ); + }, + [this]( size_t selected ) { + static constexpr TerminalCursorMode modes[] = { + TerminalCursorMode::BlinkingBlock, TerminalCursorMode::SteadyBlock, + TerminalCursorMode::BlinkUnderline, TerminalCursorMode::SteadyUnderline, + TerminalCursorMode::BlinkBar, TerminalCursorMode::SteadyBar }; + auto value = modes[std::min( selected, size_t{ 5 } )]; + mApp->getConfig().term.cursorStyle = value; + mApp->getSplitter()->forEachWidgetType( UI_TYPE_TERMINAL, [value]( UIWidget* widget ) { + widget->asType()->getTerm()->setCursorMode( value ); + } ); + } ); + addChoice( + panel, + { "terminalScrollbarMode", "terminal.appearance", + mApp->i18n( "scrollbar_mode", "Scrollbar Mode" ), + mApp->i18n( "scrollbar_mode_desc", "Control when the terminal scrollbar is visible." ) }, + { mApp->i18n( "auto", "Auto" ), mApp->i18n( "always_visible", "Always Visible" ), + mApp->i18n( "always_hidden", "Always Hidden" ) }, + [this] { + return mApp->getConfig().term.scrollBarMode == ScrollBarMode::Auto + ? 0 + : ( mApp->getConfig().term.scrollBarMode == ScrollBarMode::AlwaysOn ? 1 + : 2 ); + }, + [this]( size_t selected ) { + auto value = selected == 0 ? ScrollBarMode::Auto + : ( selected == 1 ? ScrollBarMode::AlwaysOn + : ScrollBarMode::AlwaysOff ); + mApp->getConfig().term.scrollBarMode = value; + mApp->getSplitter()->forEachWidgetType( UI_TYPE_TERMINAL, [value]( UIWidget* widget ) { + widget->asType()->setVerticalScrollMode( value ); + } ); + }, + { mApp->i18n( "scrollbar_mode_auto_tooltip", + "With an overlay scrollbar, show it when the mouse moves over the terminal. " + "With an outside scrollbar, show it when there is a scrollable area." ), + mApp->i18n( "scrollbar_mode_always_visible_tooltip", + "Keep the terminal scrollbar visible." ), + mApp->i18n( "scrollbar_mode_always_hidden_tooltip", + "Keep the terminal scrollbar hidden." ) } ); +} + +void SettingsPanel::addProjectSettings( PanelState& panel ) { + addCategory( panel, "editor.document", mApp->i18n( "editor", "Editor" ), + mApp->i18n( "document", "Document" ) ); + addBool( panel, + { "useGlobalSettings", "editor.document", + mApp->i18n( "use_global_settings", "Use Global Settings" ), + mApp->i18n( "use_global_settings_desc", + "Inherit document defaults from the user settings." ) }, + &mApp->getProjectConfig().useGlobalSettings ); + auto addProjectBool = [this, &panel]( std::string id, const char* nameKey, const char* name, + const char* descriptionKey, const char* description, + bool DocumentConfig::* member ) { + addBool( panel, + { std::move( id ), "editor.document", mApp->i18n( nameKey, name ), + mApp->i18n( descriptionKey, description ) }, + &( mApp->getProjectConfig().doc.*member ) ); + }; + addProjectBool( "trimTrailingWhitespaces", "trim_trailing_whitespaces", + "Trim Trailing Whitespaces", "trim_trailing_whitespaces_desc", + "Remove trailing whitespace when saving files.", + &DocumentConfig::trimTrailingWhitespaces ); + addProjectBool( "forceNewLineAtEndOfFile", "force_new_line_at_end_of_file", + "Force New Line at End of File", "force_new_line_at_end_of_file_desc", + "Ensure saved files end with a newline.", + &DocumentConfig::forceNewLineAtEndOfFile ); + addProjectBool( "autoDetectIndentType", "auto_detect_indent_type_and_width", + "Auto Detect Indent Type & Width", "auto_detect_indent_type_desc", + "Detect indentation settings when opening each document.", + &DocumentConfig::autoDetectIndentType ); + addChoice( + panel, + { "autoIndent", "editor.document", mApp->i18n( "auto_indent", "Auto-Indent" ), + mApp->i18n( "auto_indent_tooltip", "Control indentation added after pressing Enter." ) }, + { mApp->i18n( "auto_indent_none", "None" ), + mApp->i18n( "auto_indent_preserve", "Preserve" ), + mApp->i18n( "auto_indent_smart", "Smart" ) }, + [this] { return static_cast( mApp->getProjectConfig().doc.autoIndent ); }, + [this]( size_t selected ) { + mApp->getProjectConfig().doc.autoIndent = + static_cast( std::min( selected, size_t{ 2 } ) ); + }, + { mApp->i18n( "auto_indent_none_desc", "No automatic indentation." ), + mApp->i18n( "auto_indent_preserve_desc", + "Preserve the indentation level of the previous line." ), + mApp->i18n( "auto_indent_smart_desc", + "Preserve indentation and indent between auto-closed brackets." ) } ); + addProjectBool( "indentSpaces", "indent_spaces", "Indent Using Spaces", "indent_spaces_desc", + "Insert spaces instead of tab characters for indentation.", + &DocumentConfig::indentSpaces ); + 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, + { "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 ); + addChoice( + panel, + { "lineEndings", "editor.document", mApp->i18n( "line_endings", "Line Endings" ), + mApp->i18n( "line_endings_desc", "Default line-ending sequence for new files." ) }, + { "Windows (CR/LF)", "Unix (LF)", "Macintosh (CR)" }, + [this] { + switch ( mApp->getProjectConfig().doc.lineEndings ) { + case TextFormat::LineEnding::CRLF: + return size_t{ 0 }; + case TextFormat::LineEnding::CR: + return size_t{ 2 }; + default: + return size_t{ 1 }; + } + }, + [this]( size_t selected ) { + mApp->getProjectConfig().doc.lineEndings = + selected == 0 + ? TextFormat::LineEnding::CRLF + : ( selected == 2 ? TextFormat::LineEnding::CR : TextFormat::LineEnding::LF ); + } ); + + addCategory( panel, "languages.file_associations", mApp->i18n( "languages", "Languages" ), + mApp->i18n( "file_associations", "File Associations" ) ); + addChoice( + panel, + { "hExtLanguageType", "languages.file_associations", + mApp->i18n( "treat_h_files_as", "Treat .h Files As" ), + mApp->i18n( "treat_h_files_as_desc", + "Choose the default language for ambiguous .h header files." ) }, + { mApp->i18n( "auto-detect", "Auto-Detect" ), "C", "C++", "Objective-C", "Objective-C++" }, + [this] { + switch ( mApp->getProjectConfig().hExtLanguageType ) { + case HExtLanguageType::C: + return size_t{ 1 }; + case HExtLanguageType::CPP: + return size_t{ 2 }; + case HExtLanguageType::ObjectiveC: + return size_t{ 3 }; + case HExtLanguageType::ObjectiveCPP: + return size_t{ 4 }; + default: + return size_t{ 0 }; + } + }, + [this]( size_t selected ) { + static constexpr HExtLanguageType values[] = { + HExtLanguageType::AutoDetect, HExtLanguageType::C, HExtLanguageType::CPP, + HExtLanguageType::ObjectiveC, HExtLanguageType::ObjectiveCPP }; + auto value = values[std::min( selected, size_t{ 4 } )]; + mApp->getProjectConfig().hExtLanguageType = value; + mApp->getSplitter()->forEachEditor( [this, value]( UICodeEditor* editor ) { + auto& document = editor->getDocument(); + document.setHExtLanguageType( value ); + if ( document.getFileInfo().getExtension() == "h" ) { + editor->resetSyntaxDefinition(); + } + } ); + } ); +} + +void SettingsPanel::filter( PanelState& panel ) { + String query = panel.search ? panel.search->getText() : String{}; + query.trim().toLower(); + if ( !query.empty() ) { + panel.pageTitle->setText( mApp->i18n( "search_results", "Search Results" ) ); + } else if ( auto title = panel.categoryTitles.find( panel.selectedCategory ); + title != panel.categoryTitles.end() ) { + panel.pageTitle->setText( title->second ); + } + UnorderedSet matchingCategories; + const std::string queryUtf8 = query.toUtf8(); + const bool aggregate = String::endsWith( panel.selectedCategory, ".*" ); + const std::string aggregatePrefix = + aggregate ? panel.selectedCategory.substr( 0, panel.selectedCategory.size() - 1 ) + : std::string{}; + if ( !query.empty() ) { + for ( const auto& binding : panel.bindings ) { + if ( String::icontains( binding.name, query ) || + String::icontains( binding.description, query ) || + String::icontains( binding.id, queryUtf8 ) ) + matchingCategories.insert( binding.category ); + } + } + if ( queryUtf8 != panel.categoryFilter ) { + panel.categoryFilter = queryUtf8; + if ( auto model = std::static_pointer_cast( panel.categoryModel ) ) + model->filter( queryUtf8, matchingCategories ); + } + panel.categories->expandAll(); + for ( auto& [category, heading] : panel.categoryHeadings ) + heading->setVisible( query.empty() && aggregate && + String::startsWith( category, aggregatePrefix ) ); + for ( auto& binding : panel.bindings ) { + const auto categoryName = panel.categorySearchText.find( binding.category ); + const bool categoryMatches = categoryName != panel.categorySearchText.end() && + String::icontains( categoryName->second, query ); + const bool matches = + query.empty() + ? binding.category == panel.selectedCategory || + ( aggregate && String::startsWith( binding.category, aggregatePrefix ) ) + : categoryMatches || String::icontains( binding.name, query ) || + String::icontains( binding.description, query ) || + String::icontains( binding.id, queryUtf8 ); + binding.row->setVisible( matches ); + } +} + +} // namespace ecode diff --git a/src/tools/ecode/settingspanel.hpp b/src/tools/ecode/settingspanel.hpp new file mode 100644 index 000000000..abfa37a59 --- /dev/null +++ b/src/tools/ecode/settingspanel.hpp @@ -0,0 +1,81 @@ +#ifndef ECODE_SETTINGSPANEL_HPP +#define ECODE_SETTINGSPANEL_HPP + +#include +#include +#include +#include + +namespace ecode { + +class App; + +class SettingsPanel { + public: + enum class Scope : Uint8 { User, Project }; + + explicit SettingsPanel( App* app ); + + void show( Scope scope ); + + protected: + struct SettingBinding { + std::string id; + std::string category; + String name; + String description; + UIWidget* row{ nullptr }; + }; + + struct PanelState { + UIWindow* window{ nullptr }; + EventConnectionList connections; + UITextInput* search{ nullptr }; + UITreeView* categories{ nullptr }; + UIScrollView* scroll{ nullptr }; + UILinearLayout* settings{ nullptr }; + UITextView* pageTitle{ nullptr }; + std::shared_ptr categoryModel; + UIBindingGroup bindingGroup; + std::vector>> categoryItems; + UnorderedMap categoryIds; + UnorderedMap categorySearchText; + UnorderedMap categoryTitles; + UnorderedMap categoryHeadings; + std::vector bindings; + std::string selectedCategory; + std::string categoryFilter; + + void reset(); + }; + + App* mApp{ nullptr }; + PanelState mUser; + PanelState mProject; + + PanelState& state( Scope scope ); + void create( Scope scope ); + void addUserSettings( PanelState& state ); + void addProjectSettings( PanelState& state ); + void addCategory( PanelState& state, const std::string& id, const String& parent, + const String& name ); + void setupCategories( PanelState& state ); + void addBool( PanelState& state, SettingBinding binding, bool* value, + std::function apply = {} ); + void addBool( PanelState& state, SettingBinding binding, std::function get, + std::function set ); + 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 addAction( PanelState& state, SettingBinding binding, const String& buttonText, + std::function action ); + UIWidget* createRow( PanelState& state, SettingBinding& binding ); + UICheckBox* createBoolControl( PanelState& state, SettingBinding& binding ); + void filter( PanelState& state ); +}; + +} // namespace ecode + +#endif