From dd94199bbbe2269de0471fbceb264b56dd084536 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 20 Mar 2026 08:32:02 +0000 Subject: [PATCH] Json editor with syntax highlighting (#8932) * Add json editor * Replace JsonEditor with TextBox * Replace JsonEditor with TextBox * Remove TextMateSharp.Grammars * Fix two way bind * Update ResUI.ru.resx --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com> --- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 36 +++++++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 12 +++ v2rayN/ServiceLib/Resx/ResUI.fr.resx | 12 +++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 12 +++ v2rayN/ServiceLib/Resx/ResUI.resx | 12 +++ v2rayN/ServiceLib/Resx/ResUI.ru.resx | 12 +++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 12 +++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 12 +++ .../Views/AddServerWindow.axaml | 16 ++-- .../Views/DNSSettingWindow.axaml | 22 +---- .../Views/FullConfigTemplateWindow.axaml | 22 +---- v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml | 23 +++++ .../v2rayN.Desktop/Views/JsonEditor.axaml.cs | 96 +++++++++++++++++++ v2rayN/v2rayN.Desktop/Views/MsgView.axaml | 4 +- 14 files changed, 256 insertions(+), 47 deletions(-) create mode 100644 v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml create mode 100644 v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 5a1ea759..c75715ae 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -924,6 +924,42 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Copy 的本地化字符串。 + /// + public static string menuEditCopy { + get { + return ResourceManager.GetString("menuEditCopy", resourceCulture); + } + } + + /// + /// 查找类似 Format 的本地化字符串。 + /// + public static string menuEditFormat { + get { + return ResourceManager.GetString("menuEditFormat", resourceCulture); + } + } + + /// + /// 查找类似 Paste 的本地化字符串。 + /// + public static string menuEditPaste { + get { + return ResourceManager.GetString("menuEditPaste", resourceCulture); + } + } + + /// + /// 查找类似 Select all 的本地化字符串。 + /// + public static string menuEditSelectAll { + get { + return ResourceManager.GetString("menuEditSelectAll", resourceCulture); + } + } + /// /// 查找类似 Edit 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 1dd09e85..30f663e8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + کپی + + + انتخاب همه + + + Paste + + + Format + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index c873db43..061d6dad 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1665,4 +1665,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + Copier + + + Tout sélect + + + Paste + + + Format + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index f59a652b..4d652587 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + Másolás + + + Összes kijelölése + + + Paste + + + Format + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index f299a877..787e7bd9 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + Copy + + + Select all + + + Paste + + + Format + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index a20fa3b1..ba2ef074 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1668,4 +1668,16 @@ Группировка по регионам + + Скопировать + + + Выбрать все + + + Paste + + + Format + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index d655c30d..707609d0 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1665,4 +1665,16 @@ 按地区分组 + + 复制 + + + 全选 + + + 粘贴 + + + 格式化 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 65b1dbfb..4e417308 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1665,4 +1665,16 @@ 按區域分組 + + 複製 + + + 全選 + + + Paste + + + Format + \ No newline at end of file diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index 477eca5d..e9ded8aa 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" + xmlns:views="clr-namespace:v2rayN.Desktop.Views" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" Title="{x:Static resx:ResUI.menuServers}" Width="900" @@ -658,16 +659,13 @@ Margin="{StaticResource Margin4}" VerticalAlignment="Center" Text="{x:Static resx:ResUI.TransportExtraTip}" /> - + VerticalAlignment="Center" /> @@ -749,13 +747,13 @@ - + HorizontalAlignment="Stretch" + VerticalAlignment="Center" /> diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index 705e79e2..3335785b 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -3,6 +3,7 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:local="using:v2rayN.Desktop.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" @@ -399,12 +400,7 @@ BorderBrush="Gray" BorderThickness="1" Header="HTTP/SOCKS"> - + @@ -473,12 +469,7 @@ BorderBrush="Gray" BorderThickness="1" Header="HTTP/SOCKS"> - + @@ -488,12 +479,7 @@ BorderBrush="Gray" BorderThickness="1" Header="{x:Static resx:ResUI.TbSettingsTunMode}"> - + diff --git a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml index 63f29ed0..1a2b482c 100644 --- a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" + xmlns:views="clr-namespace:v2rayN.Desktop.Views" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" Title="{x:Static resx:ResUI.menuFullConfigTemplate}" Width="900" @@ -94,12 +95,7 @@ BorderBrush="Gray" BorderThickness="1" Header="xray config template json"> - + @@ -166,12 +162,7 @@ BorderBrush="Gray" BorderThickness="1" Header="sing-box config template json"> - + @@ -181,12 +172,7 @@ BorderBrush="Gray" BorderThickness="1" Header="sing-box tun config template json"> - + diff --git a/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml new file mode 100644 index 00000000..cad0021d --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs new file mode 100644 index 00000000..98620126 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs @@ -0,0 +1,96 @@ +using System.Text.Json; +using System.Xml; +using AvaloniaEdit.Highlighting; +using AvaloniaEdit.Highlighting.Xshd; + +namespace v2rayN.Desktop.Views; + +public partial class JsonEditor : UserControl +{ + private static readonly JsonSerializerOptions SIndentedOptions = new() { WriteIndented = true }; + + private static readonly Lazy SHighlightingDark = + new(() => BuildHighlighting(dark: true), isThreadSafe: true); + + private static readonly Lazy SHighlightingLight = + new(() => BuildHighlighting(dark: false), isThreadSafe: true); + + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text), defaultValue: string.Empty); + + public string Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public JsonEditor() + { + InitializeComponent(); + var isDark = Application.Current?.ActualThemeVariant != ThemeVariant.Light; + Editor.SyntaxHighlighting = isDark ? SHighlightingDark.Value : SHighlightingLight.Value; + Editor.TextArea.TextView.Options.EnableHyperlinks = false; + + Editor.TextChanged += (_, _) => + { + if (Text != Editor.Text) + { + SetCurrentValue(TextProperty, Editor.Text); + } + }; + + this.GetObservable(TextProperty).Subscribe(text => + { + if (Editor.Text != text) + { + Editor.Text = text ?? string.Empty; + } + }); + } + + private static IHighlightingDefinition BuildHighlighting(bool dark) + { + var keyColor = dark ? "#9CDCFE" : "#0451A5"; + var strColor = dark ? "#CE9178" : "#A31515"; + var numColor = dark ? "#B5CEA8" : "#098658"; + var kwColor = dark ? "#569CD6" : "#0000FF"; + var xshd = $""" + + + + + + + + "([^"\\]|\\.)*"(?=\s*:) + "([^"\\]|\\.)*" + -?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)? + + true + false + null + + + + """; + using var reader = XmlReader.Create(new StringReader(xshd)); + return HighlightingLoader.Load(reader, HighlightingManager.Instance); + } + + private void FormatJson_Click(object? sender, RoutedEventArgs e) + { + try + { + var obj = JsonUtils.ParseJson(Editor.Text); + Editor.Text = JsonUtils.Serialize(obj, SIndentedOptions); + } + catch + { + // ignored + } + } + + private void Copy_Click(object? sender, RoutedEventArgs e) => Editor.Copy(); + private void Paste_Click(object? sender, RoutedEventArgs e) => Editor.Paste(); + private void SelectAll_Click(object? sender, RoutedEventArgs e) => Editor.SelectAll(); +} diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml index 90dcfeb7..aabe80fa 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml @@ -79,8 +79,8 @@ + Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" + InputGesture="Ctrl+A" />