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>
This commit is contained in:
DHR60
2026-03-20 08:32:02 +00:00
committed by GitHub
parent 0cec5986cd
commit dd94199bbb
14 changed files with 256 additions and 47 deletions

View File

@@ -924,6 +924,42 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Copy 的本地化字符串。
/// </summary>
public static string menuEditCopy {
get {
return ResourceManager.GetString("menuEditCopy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Format 的本地化字符串。
/// </summary>
public static string menuEditFormat {
get {
return ResourceManager.GetString("menuEditFormat", resourceCulture);
}
}
/// <summary>
/// 查找类似 Paste 的本地化字符串。
/// </summary>
public static string menuEditPaste {
get {
return ResourceManager.GetString("menuEditPaste", resourceCulture);
}
}
/// <summary>
/// 查找类似 Select all 的本地化字符串。
/// </summary>
public static string menuEditSelectAll {
get {
return ResourceManager.GetString("menuEditSelectAll", resourceCulture);
}
}
/// <summary>
/// 查找类似 Edit 的本地化字符串。
/// </summary>

View File

@@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>کپی</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>انتخاب همه</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1665,4 +1665,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Copier</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Tout sélect</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Másolás</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Összes kijelölése</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Copy</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Select all</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1668,4 +1668,16 @@
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Группировка по регионам</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Скопировать</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Выбрать все</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1665,4 +1665,16 @@
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按地区分组</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>复制</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>全选</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>粘贴</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>格式化</value>
</data>
</root>

View File

@@ -1665,4 +1665,16 @@
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按區域分組</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>複製</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>全選</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -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}" />
<TextBox
<views:JsonEditor
x:Name="txtExtra"
Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Classes="TextArea"
MinLines="6"
TextWrapping="Wrap" />
VerticalAlignment="Center" />
</StackPanel>
</Flyout>
</Button.Flyout>
@@ -749,13 +747,13 @@
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBox
<views:JsonEditor
x:Name="txtFinalmask"
Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
Classes="TextArea"
TextWrapping="NoWrap" />
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
</StackPanel>
</Flyout>
</Button.Flyout>

View File

@@ -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">
<TextBox
Name="txtnormalDNSCompatible"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<local:JsonEditor Name="txtnormalDNSCompatible" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</DockPanel>
</TabItem>
@@ -473,12 +469,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="HTTP/SOCKS">
<TextBox
Name="txtnormalDNS2Compatible"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<local:JsonEditor Name="txtnormalDNS2Compatible" VerticalAlignment="Stretch" />
</HeaderedContentControl>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
@@ -488,12 +479,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="{x:Static resx:ResUI.TbSettingsTunMode}">
<TextBox
Name="txttunDNS2Compatible"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<local:JsonEditor Name="txttunDNS2Compatible" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</Grid>

View File

@@ -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">
<TextBox
x:Name="rayFullConfigTemplate"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<views:JsonEditor x:Name="rayFullConfigTemplate" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</DockPanel>
</TabItem>
@@ -166,12 +162,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="sing-box config template json">
<TextBox
x:Name="sbFullConfigTemplate"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<views:JsonEditor x:Name="sbFullConfigTemplate" VerticalAlignment="Stretch" />
</HeaderedContentControl>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
@@ -181,12 +172,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="sing-box tun config template json">
<TextBox
x:Name="sbFullTunConfigTemplate"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<views:JsonEditor x:Name="sbFullTunConfigTemplate" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</Grid>
</DockPanel>

View File

@@ -0,0 +1,23 @@
<UserControl
x:Class="v2rayN.Desktop.Views.JsonEditor"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ae="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib">
<ae:TextEditor
Name="Editor"
FontFamily="Cascadia Code,Consolas,Monospace"
FontSize="14"
ShowLineNumbers="True">
<ae:TextEditor.ContextMenu>
<ContextMenu>
<MenuItem Click="FormatJson_Click" Header="{x:Static resx:ResUI.menuEditFormat}" />
<Separator />
<MenuItem Click="Copy_Click" Header="{x:Static resx:ResUI.menuEditCopy}" />
<MenuItem Click="Paste_Click" Header="{x:Static resx:ResUI.menuEditPaste}" />
<MenuItem Click="SelectAll_Click" Header="{x:Static resx:ResUI.menuEditSelectAll}" />
</ContextMenu>
</ae:TextEditor.ContextMenu>
</ae:TextEditor>
</UserControl>

View File

@@ -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<IHighlightingDefinition> SHighlightingDark =
new(() => BuildHighlighting(dark: true), isThreadSafe: true);
private static readonly Lazy<IHighlightingDefinition> SHighlightingLight =
new(() => BuildHighlighting(dark: false), isThreadSafe: true);
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<JsonEditor, string>(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 = $"""
<?xml version="1.0"?>
<SyntaxDefinition name="JSON" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Key" foreground="{keyColor}" />
<Color name="String" foreground="{strColor}" />
<Color name="Number" foreground="{numColor}" />
<Color name="Keyword" foreground="{kwColor}" fontWeight="bold" />
<RuleSet>
<Rule color="Key">"([^"\\]|\\.)*"(?=\s*:)</Rule>
<Rule color="String">"([^"\\]|\\.)*"</Rule>
<Rule color="Number">-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?</Rule>
<Keywords color="Keyword">
<Word>true</Word>
<Word>false</Word>
<Word>null</Word>
</Keywords>
</RuleSet>
</SyntaxDefinition>
""";
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();
}

View File

@@ -79,8 +79,8 @@
<MenuItem
x:Name="menuMsgViewSelectAll"
Click="menuMsgViewSelectAll_Click"
InputGesture="Ctrl+A"
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" />
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}"
InputGesture="Ctrl+A" />
<MenuItem
x:Name="menuMsgViewCopy"
Click="menuMsgViewCopy_Click"