XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

SpaceEngineersModDev/SpaceEngineersBlueprintEditorInWinUI

优化蓝图属性树显示、搜索与本地化支持

全面提升蓝图属性树的显示、搜索和编辑体验: - 新增属性名本地化,支持中文显示名和本地化 JSON 映射 - 属性树支持多字段模糊搜索,提升查找效率 - 重构属性节点结构,优化子节点加载与循环引用检测 - 编辑控件类型自动适配,提升编辑一致性与友好性 - 增强方块组与集合类型处理,准确显示组坐标和元素信息 - 统一 XAML 模板,优化界面细节与交互动画 - 增加中英文提示文本资源,修正部分命名空间和资源路径 极大提升蓝图属性树的可读性、可编辑性和本地化体验。

843a21e
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

19 个文件 +1143 -298
Added SpaceEngineersBlueprintEditor.SpaceEngineersCore/BlueprintEditing/BlockGroupResolver.cs +227 -0
@@ -0,0 +1,227 @@
1 using Sandbox.Definitions;
2 using VRage;
3 using VRage.Game;
4 using VRageMath;
5
6 namespace SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;
7
8 /// <summary>
9 /// Resolves the positions stored by a Space Engineers block group back to the
10 /// blocks occupying those cells. Group positions are not guaranteed to equal a
11 /// multi-cell block's Min coordinate.
12 /// </summary>
13 public static class BlockGroupResolver
14 {
15 public static IReadOnlyList<MyObjectBuilder_CubeBlock> ResolveBlocks(
16 MyObjectBuilder_CubeGrid grid,
17 MyObjectBuilder_BlockGroup group)
18 {
19 if (grid is null)
20 {
21 throw new ArgumentNullException(nameof(grid));
22 }
23 if (group is null)
24 {
25 throw new ArgumentNullException(nameof(group));
26 }
27 if (grid.CubeBlocks is null || group.Blocks is null)
28 {
29 return Array.Empty<MyObjectBuilder_CubeBlock>();
30 }
31
32 var occupiedCells = BuildOccupiedCellIndex(grid.CubeBlocks);
33 return ResolveBlocks(group.Blocks, occupiedCells);
34 }
35
36 /// <summary>
37 /// Resolves every group in a grid while constructing the occupied-cell index
38 /// only once.
39 /// </summary>
40 public static IReadOnlyDictionary<MyObjectBuilder_BlockGroup, IReadOnlyList<MyObjectBuilder_CubeBlock>>
41 ResolveGroups(MyObjectBuilder_CubeGrid grid)
42 {
43 if (grid is null)
44 {
45 throw new ArgumentNullException(nameof(grid));
46 }
47
48 var result = new Dictionary<MyObjectBuilder_BlockGroup, IReadOnlyList<MyObjectBuilder_CubeBlock>>();
49 if (grid.CubeBlocks is null || grid.BlockGroups is null)
50 {
51 return result;
52 }
53
54 var occupiedCells = BuildOccupiedCellIndex(grid.CubeBlocks);
55 foreach (var group in grid.BlockGroups.Where(group => group is not null))
56 {
57 result[group] = group.Blocks is null
58 ? Array.Empty<MyObjectBuilder_CubeBlock>()
59 : ResolveBlocks(group.Blocks, occupiedCells);
60 }
61
62 return result;
63 }
64
65 private static IReadOnlyList<MyObjectBuilder_CubeBlock> ResolveBlocks(
66 IEnumerable<Vector3I> positions,
67 IReadOnlyDictionary<CellKey, MyObjectBuilder_CubeBlock> occupiedCells)
68 {
69 var result = new List<MyObjectBuilder_CubeBlock>();
70 var addedBlocks = new HashSet<MyObjectBuilder_CubeBlock>();
71 foreach (var position in positions)
72 {
73 if (occupiedCells.TryGetValue(ToKey(position), out var block) && addedBlocks.Add(block))
74 {
75 result.Add(block);
76 }
77 }
78
79 return result;
80 }
81
82 /// <summary>
83 /// Gets the axis-aligned cell dimensions occupied after applying the block's
84 /// Forward/Up orientation to its definition size.
85 /// </summary>
86 public static Vector3I GetOrientedSize(MyObjectBuilder_CubeBlock block)
87 {
88 if (block is null)
89 {
90 throw new ArgumentNullException(nameof(block));
91 }
92
93 var definitionSize = TryGetDefinitionSize(block);
94 var forward = ToVector(block.BlockOrientation.Forward);
95 var up = ToVector(block.BlockOrientation.Up);
96 var right = Cross(forward, up);
97 var backward = -forward;
98 var orientedSize = new Vector3I(
99 Math.Abs(right.X) * definitionSize.X +
100 Math.Abs(up.X) * definitionSize.Y +
101 Math.Abs(backward.X) * definitionSize.Z,
102 Math.Abs(right.Y) * definitionSize.X +
103 Math.Abs(up.Y) * definitionSize.Y +
104 Math.Abs(backward.Y) * definitionSize.Z,
105 Math.Abs(right.Z) * definitionSize.X +
106 Math.Abs(up.Z) * definitionSize.Y +
107 Math.Abs(backward.Z) * definitionSize.Z);
108
109 return orientedSize.X > 0 && orientedSize.Y > 0 && orientedSize.Z > 0
110 ? orientedSize
111 : definitionSize;
112 }
113
114 public static bool Occupies(MyObjectBuilder_CubeBlock block, Vector3I position)
115 {
116 if (block is null)
117 {
118 throw new ArgumentNullException(nameof(block));
119 }
120
121 var size = GetOrientedSize(block);
122 return position.X >= block.Min.X && position.X < block.Min.X + size.X &&
123 position.Y >= block.Min.Y && position.Y < block.Min.Y + size.Y &&
124 position.Z >= block.Min.Z && position.Z < block.Min.Z + size.Z;
125 }
126
127 private static Dictionary<CellKey, MyObjectBuilder_CubeBlock> BuildOccupiedCellIndex(
128 IEnumerable<MyObjectBuilder_CubeBlock> blocks)
129 {
130 var result = new Dictionary<CellKey, MyObjectBuilder_CubeBlock>();
131 var blockList = blocks.Where(block => block is not null).ToArray();
132
133 // Exact Min coordinates take precedence if a malformed blueprint contains
134 // overlapping blocks.
135 foreach (var block in blockList)
136 {
137 result[ToKey(block.Min)] = block;
138 }
139
140 foreach (var block in blockList)
141 {
142 var size = GetOrientedSize(block);
143 for (var x = 0; x < size.X; x++)
144 for (var y = 0; y < size.Y; y++)
145 for (var z = 0; z < size.Z; z++)
146 {
147 var key = new CellKey(block.Min.X + x, block.Min.Y + y, block.Min.Z + z);
148 if (!result.ContainsKey(key))
149 {
150 result.Add(key, block);
151 }
152 }
153 }
154
155 return result;
156 }
157
158 private static Vector3I TryGetDefinitionSize(MyObjectBuilder_CubeBlock block)
159 {
160 try
161 {
162 var size = MyDefinitionManager.Static.GetCubeBlockDefinition(block)?.Size ?? Vector3I.One;
163 return new Vector3I(
164 Math.Max(1, size.X),
165 Math.Max(1, size.Y),
166 Math.Max(1, size.Z));
167 }
168 catch
169 {
170 // Modded or unavailable definitions can still be matched by Min.
171 return Vector3I.One;
172 }
173 }
174
175 private static Vector3I Cross(Vector3I left, Vector3I right) => new(
176 left.Y * right.Z - left.Z * right.Y,
177 left.Z * right.X - left.X * right.Z,
178 left.X * right.Y - left.Y * right.X);
179
180 private static Vector3I ToVector(Base6Directions.Direction direction)
181 {
182 return direction switch
183 {
184 Base6Directions.Direction.Forward => new Vector3I(0, 0, -1),
185 Base6Directions.Direction.Backward => new Vector3I(0, 0, 1),
186 Base6Directions.Direction.Left => new Vector3I(-1, 0, 0),
187 Base6Directions.Direction.Right => new Vector3I(1, 0, 0),
188 Base6Directions.Direction.Up => new Vector3I(0, 1, 0),
189 Base6Directions.Direction.Down => new Vector3I(0, -1, 0),
190 _ => new Vector3I(0, 0, -1)
191 };
192 }
193
194 private static CellKey ToKey(SerializableVector3I position) =>
195 new(position.X, position.Y, position.Z);
196
197 private static CellKey ToKey(Vector3I position) =>
198 new(position.X, position.Y, position.Z);
199
200 private readonly struct CellKey : IEquatable<CellKey>
201 {
202 private readonly int x;
203 private readonly int y;
204 private readonly int z;
205
206 public CellKey(int x, int y, int z)
207 {
208 this.x = x;
209 this.y = y;
210 this.z = z;
211 }
212
213 public bool Equals(CellKey other) => x == other.x && y == other.y && z == other.z;
214
215 public override bool Equals(object? obj) => obj is CellKey other && Equals(other);
216
217 public override int GetHashCode()
218 {
219 unchecked
220 {
221 var hash = x;
222 hash = hash * 397 ^ y;
223 return hash * 397 ^ z;
224 }
225 }
226 }
227 }
Modified SpaceEngineersBlueprintEditor/App.xaml +1 -0
@@ -11,6 +11,7 @@
11 11 <!-- Other merged dictionaries here -->
12 12 <ResourceDictionary Source="/Styles/Thickness.xaml" />
13 13 <ResourceDictionary Source="/Styles/Controls.xaml" />
14 <ResourceDictionary Source="/Styles/Materials.xaml" />
14 15 </ResourceDictionary.MergedDictionaries>
15 16 <!-- Other app resources here -->
16 17 </ResourceDictionary>
Modified SpaceEngineersBlueprintEditor/App.xaml.cs +1 -1
@@ -20,7 +20,6 @@ public partial class App : Application
20 20 if (!gameRootPath.IsNullOrEmpty() && !string.Equals(SystemProfile.GameRootPath, gameRootPath, StringComparison.OrdinalIgnoreCase))
21 21 SystemProfile.GameRootPath = gameRootPath;
22 22
23 MainWindow = new MainWindow();
24 23 this.InitializeComponent();
25 24 PageManager.RegisterPage(typeof(AppShellPage));
26 25 PageManager.RegisterPage(typeof(MainPage));
@@ -60,6 +59,7 @@ public partial class App : Application
60 59 /// <param name="args">Details about the launch request and process.</param>
61 60 protected override void OnLaunched(LaunchActivatedEventArgs args)
62 61 {
62 MainWindow = new MainWindow();
63 63 MainWindow.Content = new AppShellPage();
64 64 MainWindow.Activate();
65 65 }
Added SpaceEngineersBlueprintEditor/Assets/Localization/BlueprintPropertyNames.zh-CN.json +169 -0
@@ -0,0 +1,169 @@
1 {
2 "Id": "标识",
3 "TypeId": "类型标识",
4 "SubtypeId": "子类型标识",
5 "SubtypeName": "子类型名称",
6 "DisplayName": "显示名称",
7 "Description": "描述",
8 "DescriptionArgs": "描述参数",
9 "Name": "名称",
10 "CubeGrids": "网格列表",
11 "CubeGrid": "网格",
12 "CubeBlocks": "方块列表",
13 "BlockGroups": "方块组",
14 "GridSizeEnum": "网格尺寸",
15 "IsStatic": "是否静态",
16 "DestructibleBlocks": "方块可破坏",
17 "Editable": "是否可编辑",
18 "CreatePhysics": "创建物理效果",
19 "EnableSmallToLargeConnections": "允许大小网格连接",
20 "PositionAndOrientation": "位置与方向",
21 "Position": "位置",
22 "Orientation": "方向",
23 "Forward": "前方向",
24 "Up": "上方向",
25 "LinearVelocity": "线速度",
26 "AngularVelocity": "角速度",
27 "X": "X 坐标",
28 "Y": "Y 坐标",
29 "Z": "Z 坐标",
30 "Min": "最小坐标",
31 "Max": "最大坐标",
32 "BlockOrientation": "方块朝向",
33 "ColorMaskHSV": "HSV 颜色",
34 "SkinSubtypeId": "皮肤子类型",
35 "ConstructionInventory": "建造库存",
36 "ConstructionStockpile": "建造储备",
37 "IntegrityPercent": "完整度",
38 "BuildPercent": "建造进度",
39 "BuiltBy": "建造者",
40 "Owner": "所有者",
41 "OwnerId": "所有者标识",
42 "OwnerSteamId": "所有者 Steam 标识",
43 "ShareMode": "共享模式",
44 "EntityId": "实体标识",
45 "PersistentFlags": "持久化标记",
46 "Enabled": "已启用",
47 "CustomName": "自定义名称",
48 "CustomData": "自定义数据",
49 "ShowOnHUD": "显示在 HUD",
50 "ShowInTerminal": "显示在终端",
51 "ShowInToolbarConfig": "显示在工具栏配置",
52 "ShowInInventory": "显示在库存",
53 "ShowParts": "显示部件",
54 "UseConveyorSystem": "使用输送系统",
55 "Inventory": "库存",
56 "Inventories": "库存列表",
57 "ComponentContainer": "组件容器",
58 "ComponentData": "组件数据",
59 "Stockpile": "建造储备",
60 "Toolbar": "工具栏",
61 "BuildToolbar": "建造工具栏",
62 "Slots": "槽位",
63 "SelectedSlot": "已选槽位",
64 "Pilot": "驾驶员",
65 "PilotRelativeWorld": "驾驶员相对世界变换",
66 "Autopilot": "自动驾驶",
67 "PilotGunDefinition": "驾驶员武器定义",
68 "IsInFirstPersonView": "第一人称视角",
69 "OxygenLevel": "氧气水平",
70 "ControlThrusters": "控制推进器",
71 "ControlWheels": "控制轮组",
72 "ControlGyros": "控制陀螺仪",
73 "HandBrake": "手刹",
74 "DampenersOverride": "惯性阻尼",
75 "IsMainCockpit": "主驾驶舱",
76 "TargetData": "目标数据",
77 "TargetingGroup": "目标组",
78 "TargetLocking": "目标锁定",
79 "Range": "范围",
80 "Radius": "半径",
81 "Power": "功率",
82 "PowerConsumption": "功耗",
83 "CurrentStoredPower": "当前储能",
84 "MaxStoredPower": "最大储能",
85 "ChargeMode": "充电模式",
86 "Capacity": "容量",
87 "CurrentCapacity": "当前容量",
88 "AutoRefill": "自动补充",
89 "Depressurize": "减压",
90 "AirVentMode": "通风模式",
91 "Open": "开启",
92 "AnyoneCanUse": "任何人可用",
93 "Status": "状态",
94 "State": "状态",
95 "Mode": "模式",
96 "Broadcast": "广播",
97 "BroadcastRadius": "广播半径",
98 "EnableBroadcasting": "启用广播",
99 "Attached": "已连接",
100 "Locked": "已锁定",
101 "AutoLock": "自动锁定",
102 "SafetyLock": "安全锁",
103 "Velocity": "速度",
104 "Torque": "扭矩",
105 "BrakingTorque": "制动扭矩",
106 "TargetVelocity": "目标速度",
107 "TargetVelocityRPM": "目标转速",
108 "LowerLimit": "下限",
109 "UpperLimit": "上限",
110 "Displacement": "位移",
111 "CurrentPosition": "当前位置",
112 "MaxDistance": "最大距离",
113 "MinDistance": "最小距离",
114 "Acceleration": "加速度",
115 "ThrustOverride": "推力覆盖",
116 "ThrustOverridePercentage": "推力覆盖百分比",
117 "GyroOverride": "陀螺仪覆盖",
118 "GyroPower": "陀螺仪功率",
119 "Yaw": "偏航",
120 "Pitch": "俯仰",
121 "Roll": "滚转",
122 "CurrentAngle": "当前角度",
123 "TargetAngle": "目标角度",
124 "RotorLock": "转子锁定",
125 "UseModelIntersection": "使用模型相交检测",
126 "BlockGeneralDamageModifier": "方块通用伤害倍率",
127 "MultiBlockDefinition": "多方块定义",
128 "MultiBlockId": "多方块标识",
129 "MultiBlockIndex": "多方块索引",
130 "SubBlocks": "子方块列表",
131 "IsFunctional": "功能正常",
132 "IsWorking": "正在工作",
133 "DamageEffect": "损坏效果",
134 "DeformationRatio": "变形比例",
135 "Skeleton": "骨架变形",
136 "ConveyorLines": "输送管线",
137 "TargetingTargets": "锁定目标",
138 "ConnectedEntities": "已连接实体",
139 "MechanicalConnections": "机械连接",
140 "DampenersEnabled": "已启用阻尼",
141 "PlanetSpawnHeightRatio": "行星生成高度比例",
142 "WorkshopId": "创意工坊标识",
143 "WorkshopIds": "创意工坊标识列表",
144 "DLCs": "DLC 列表",
145 "RespawnShip": "重生飞船",
146 "Points": "点数",
147 "Cloud": "云端蓝图",
148 "ExperimentalMode": "实验模式",
149 "AvailableInSurvival": "可在生存模式使用",
150 "Context": "上下文",
151 "Icons": "图标列表",
152 "MaxNPCCount": "最大 NPC 数量",
153 "NPCSpawnPointsOverride": "NPC 出生点覆盖",
154 "PrefabPath": "预制体路径",
155 "Public": "公开",
156 "TooltipImage": "工具提示图像",
157 "DescriptionString": "描述文本",
158 "Author": "作者",
159 "SteamId": "Steam 标识",
160 "Remap": "重新映射",
161 "Save": "保存",
162 "Version": "版本",
163 "LastSaveTime": "最后保存时间",
164 "Scenario": "场景",
165 "EnvironmentType": "环境类型",
166 "NumberOfBlocks": "方块数量",
167 "Mass": "质量",
168 "PCU": "性能成本(PCU)"
169 }
Modified SpaceEngineersBlueprintEditor/Implements/Services/BackgroundImageService.cs +4 -2
@@ -39,8 +39,10 @@ class BackgroundImageService : GlobalServiceBase, IBackgroundImageService
39 39 if (_grid is not null)
40 40 _grid.Background = new AcrylicBrush
41 41 {
42 TintLuminosityOpacity = 0,
43 TintColor = Colors.Transparent
42 TintColor = ColorHelper.FromArgb(255, 24, 24, 24),
43 TintOpacity = 0.48,
44 TintLuminosityOpacity = 0.35,
45 FallbackColor = ColorHelper.FromArgb(255, 32, 32, 32)
44 46 };
45 47 }
46 48 }
Modified SpaceEngineersBlueprintEditor/Implements/Services/BlueprintDropService.cs +3 -2
@@ -81,9 +81,10 @@ internal class BlueprintDropService : IFileDropService
81 81 {
82 82 if (_springAnimation is null)
83 83 {
84 _springAnimation = (_compositor ??= App.MainWindow.Compositor).CreateSpringVector3Animation();
84 _compositor ??= CompositionTarget.GetCompositorForCurrentThread();
85 _springAnimation = _compositor.CreateSpringVector3Animation();
85 86 _springAnimation.Target = "Scale";
86 87 }
87 88 _springAnimation.FinalValue = new Vector3(finalValue);
88 89 }
89 }
90 }
Modified SpaceEngineersBlueprintEditor/Model/BlueprintPropertyViewData.cs +342 -143
@@ -1,6 +1,7 @@
1 using Microsoft.UI.Xaml.Media;
1 using Microsoft.UI.Xaml.Media;
2 using SpaceEngineersBlueprintEditor.Utilities.Localization;
2 3 using System.Collections;
3 using System.Reflection;
4 using System.Globalization;
4 5 using XFEExtension.NetCore.WinUIHelper.Interface.Services;
5 6 using XFEExtension.NetCore.WinUIHelper.Utilities;
6 7 using XFEExtension.NetCore.XFETransform;
@@ -8,238 +9,436 @@ using XFEExtension.NetCore.XFETransform;
8 9 namespace SpaceEngineersBlueprintEditor.Model;
9 10
10 11 /// <summary>
11 /// 蓝图属性视图数据
12 /// A property node used by both blueprint property trees.
13 /// The original member name, translated display name and value setter are kept separate
14 /// so presentation never interferes with reflection or serialization.
12 15 /// </summary>
13 16 public partial class BlueprintPropertyViewData
14 17 {
15 /// <summary>
16 /// 属性值
17 /// </summary>
18 18 public object? Value { get; set; }
19 /// <summary>
20 /// 属性值的字符串形式
21 /// </summary>
22 public string? ValueString => Value?.ToString();
23 /// <summary>
24 /// 自定义数据
25 /// </summary>
19
20 public string ValueString => FormatValue(Value);
21
26 22 public object? CustomData { get; set; }
27 /// <summary>
28 /// 属性类型
29 /// </summary>
23
30 24 public Type? Type { get; set; }
31 /// <summary>
32 /// 属性名称
33 /// </summary>
25
26 /// <summary>The exact CLR field/property name. Never translate this value.</summary>
34 27 public string? Name { get; set; }
35 public string NameTypeString => $"{Name}[{Type?.Name}]";
36 /// <summary>
37 /// 属性枚举值
38 /// </summary>
39 public string[] EnumValues => Type is not null ? Type.IsEnum ? Enum.GetNames(Type) : ["True", "False"] : [];
40 /// <summary>
41 /// 属性的父属性
42 /// </summary>
28
29 /// <summary>An optional label for collection entries and definition objects.</summary>
30 public string? DisplayNameOverride { get; set; }
31
32 public string DisplayName =>
33 DisplayNameOverride ?? BlueprintPropertyNameLocalizer.GetDisplayName(Name);
34
35 public string FriendlyTypeName => GetFriendlyTypeName(Type);
36
37 public string NameTypeString => string.IsNullOrEmpty(FriendlyTypeName)
38 ? DisplayName
39 : $"{DisplayName} · {FriendlyTypeName}";
40
41 public string[] EnumValues => EffectiveType?.IsEnum == true
42 ? Enum.GetNames(EffectiveType)
43 : [];
44
43 45 public BlueprintPropertyViewData? Parent { get; set; }
44 /// <summary>
45 /// 方块图片
46 /// </summary>
46
47 47 public ImageSource? CubeImage { get; set; }
48
48 49 /// <summary>
49 /// 字符串控件
50 /// The setter created while reflecting this member. It deliberately does not rely on
51 /// <see cref="Name"/>, because collection entries and translated labels are not CLR members.
50 52 /// </summary>
51 public TextBox StringControl
53 internal Action<object?>? ValueSetter { get; set; }
54
55 internal Action? ValueChanged { get; set; }
56
57 public bool CanWrite => ValueSetter is not null;
58
59 public bool HasError { get; set; }
60
61 public string? ErrorMessage { get; set; }
62
63 public bool IsAnalyzed { get; set; }
64
65 public List<BlueprintPropertyViewData> Children { get; } = [];
66
67 public Type? EffectiveType => Type is null ? null : Nullable.GetUnderlyingType(Type) ?? Type;
68
69 public bool IsNullable => Type is not null &&
70 (!Type.IsValueType || Nullable.GetUnderlyingType(Type) is not null);
71
72 public bool IsEnumerable => Type is not null &&
73 Type != typeof(string) &&
74 typeof(IEnumerable).IsAssignableFrom(Type);
75
76 public bool IsMultiEnum => EffectiveType?.IsEnum == true &&
77 EffectiveType.IsDefined(typeof(FlagsAttribute), false);
78
79 public bool IsBasicType
52 80 {
53 81 get
54 82 {
55 var textBox = new TextBox
56 {
57 Text = ValueString
58 };
59 textBox.TextChanged += (sender, e) =>
60 {
61 if (sender is TextBox currentTextBox && Type is not null)
62 SetValue(Convert.ChangeType(currentTextBox.Text, Type));
63 };
64 return textBox;
83 if (EffectiveType is not { } type)
84 return false;
85
86 return XFEConverter.IsBasicType(type) ||
87 type.IsEnum ||
88 type == typeof(decimal) ||
89 type == typeof(DateTime) ||
90 type == typeof(DateTimeOffset) ||
91 type == typeof(TimeSpan) ||
92 type == typeof(Guid);
65 93 }
66 94 }
67 /// <summary>
68 /// 数字控件
69 /// </summary>
70 public NumberBox NumberControl
95
96 public bool CanExpand => !IsBasicType && Type is not null && Value is not null && !HasError;
97
98 public object? BestControl
71 99 {
72 100 get
73 101 {
74 var numberBox = new NumberBox
102 if (!IsBasicType)
103 return null;
104 if (!CanWrite)
105 return ReadOnlyControl;
106 if (IsMultiEnum)
107 return MultiEnumControl;
108 if (EffectiveType?.IsEnum == true)
109 return EnumControl;
110 if (EffectiveType == typeof(bool))
111 return BoolControl;
112 return TextControl;
113 }
114 }
115
116 public TextBlock ReadOnlyControl
117 {
118 get
119 {
120 var text = HasError ? ErrorMessage : ValueString;
121 var textBlock = new TextBlock
75 122 {
76 SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact,
77 Value = ValueString is not null ? double.Parse(ValueString) : 0
123 Text = text,
124 TextTrimming = TextTrimming.CharacterEllipsis,
125 IsTextSelectionEnabled = true,
126 VerticalAlignment = VerticalAlignment.Center,
127 HorizontalAlignment = HorizontalAlignment.Stretch
78 128 };
79 numberBox.ValueChanged += (sender, args) =>
129 if (HasError)
130 textBlock.Foreground = new SolidColorBrush(Microsoft.UI.Colors.OrangeRed);
131 ToolTipService.SetToolTip(textBlock, text);
132 return textBlock;
133 }
134 }
135
136 public TextBox TextControl
137 {
138 get
139 {
140 var textBox = new TextBox
80 141 {
81 if (Type is not null)
82 SetValue(Convert.ChangeType(sender.Value, Type));
142 Text = ValueString,
143 HorizontalAlignment = HorizontalAlignment.Stretch,
144 MinWidth = 140
83 145 };
84 return numberBox;
146 ToolTipService.SetToolTip(textBox, ValueString);
147 textBox.LostFocus += (_, _) => CommitText(textBox);
148 return textBox;
85 149 }
86 150 }
87 /// <summary>
88 /// 枚举控件
89 /// </summary>
151
90 152 public ComboBox EnumControl
91 153 {
92 154 get
93 155 {
156 var values = IsNullable
157 ? new[] { string.Empty }.Concat(EnumValues).ToArray()
158 : EnumValues;
94 159 var comboBox = new ComboBox
95 160 {
96 ItemsSource = EnumValues,
97 SelectedItem = ValueString
161 ItemsSource = values,
162 SelectedItem = Value?.ToString() ?? string.Empty,
163 HorizontalAlignment = HorizontalAlignment.Stretch,
164 MinWidth = 140
98 165 };
99 comboBox.SelectionChanged += (sender, e) =>
166 comboBox.SelectionChanged += (_, args) =>
100 167 {
101 if (e.AddedItems.FirstOrDefault() is string value && Type is not null)
102 SetValue(Enum.Parse(Type, value));
168 if (args.AddedItems.FirstOrDefault() is not string value || EffectiveType is null)
169 return;
170
171 if (string.IsNullOrEmpty(value) && IsNullable)
172 SetValue(null);
173 else
174 SetValue(Enum.Parse(EffectiveType, value, true));
103 175 };
104 176 return comboBox;
105 177 }
106 178 }
107 /// <summary>
108 /// 复合枚举控件
109 /// </summary>
179
110 180 public SplitButton MultiEnumControl
111 181 {
112 182 get
113 183 {
114 var stackPanel = new StackPanel();
115 foreach (var enumItem in EnumValues)
184 var enumType = EffectiveType!;
185 var currentBits = ToUInt64(Value);
186 var stackPanel = new StackPanel { Spacing = 4 };
187 foreach (var enumValue in Enum.GetValues(enumType).Cast<object>()
188 .Where(IsSingleFlagValue))
116 189 {
190 var bits = ToUInt64(enumValue);
117 191 stackPanel.Children.Add(new CheckBox
118 192 {
119 Content = enumItem,
120 IsChecked = ValueString?.Contains(enumItem)
193 Content = enumValue.ToString(),
194 Tag = bits,
195 IsChecked = bits == 0 ? currentBits == 0 : (currentBits & bits) == bits
121 196 });
122 197 }
198
123 199 var splitButton = new SplitButton
124 200 {
125 201 Content = ValueString,
126 Flyout = new Flyout
127 {
128 Content = stackPanel
129 }
202 HorizontalAlignment = HorizontalAlignment.Stretch,
203 MinWidth = 140,
204 Flyout = new Flyout { Content = stackPanel }
130 205 };
131 splitButton.Flyout.Closed += (sender, e) =>
206 splitButton.Flyout.Closed += (_, _) =>
132 207 {
133 if (sender is Flyout flyout && Type is not null)
208 ulong selectedBits = 0;
209 foreach (var checkBox in stackPanel.Children.OfType<CheckBox>())
134 210 {
135 var targetValue = string.Join(", ", stackPanel.Children.Where(child => child is CheckBox checkBox && checkBox.IsChecked is not null && checkBox.IsChecked.Value).Cast<CheckBox>().Select(checkBox => checkBox.Content.ToString()));
136 SetValue(Enum.Parse(Type, targetValue));
137 splitButton.Content = targetValue;
211 if (checkBox.IsChecked == true && checkBox.Tag is ulong bits && bits != 0)
212 selectedBits |= bits;
138 213 }
214
215 var targetValue = Enum.ToObject(enumType, selectedBits);
216 if (SetValue(targetValue))
217 splitButton.Content = targetValue.ToString();
139 218 };
140 219 return splitButton;
141 220 }
142 221 }
143 /// <summary>
144 /// 布尔值控件
145 /// </summary>
222
146 223 public ComboBox BoolControl
147 224 {
148 225 get
149 226 {
227 var values = IsNullable
228 ? new[] { string.Empty, bool.TrueString, bool.FalseString }
229 : new[] { bool.TrueString, bool.FalseString };
150 230 var comboBox = new ComboBox
151 231 {
152 ItemsSource = EnumValues,
153 SelectedItem = Value?.ToString()
232 ItemsSource = values,
233 SelectedItem = Value?.ToString() ?? string.Empty,
234 HorizontalAlignment = HorizontalAlignment.Stretch,
235 MinWidth = 140
154 236 };
155 comboBox.SelectionChanged += (sender, e) =>
237 comboBox.SelectionChanged += (_, args) =>
156 238 {
157 if (e.AddedItems.FirstOrDefault() is string value && Type is not null)
158 SetValue(Convert.ChangeType(value, Type));
239 if (args.AddedItems.FirstOrDefault() is not string value)
240 return;
241
242 if (string.IsNullOrEmpty(value) && IsNullable)
243 SetValue(null);
244 else if (bool.TryParse(value, out var parsed))
245 SetValue(parsed);
159 246 };
160 247 return comboBox;
161 248 }
162 249 }
163 /// <summary>
164 /// 自动选择最合适的控件
165 /// </summary>
166 public object? BestControl
250
251 public bool MatchesSearch(string? searchText)
167 252 {
168 get
253 if (string.IsNullOrWhiteSpace(searchText))
254 return true;
255
256 var searchableText = string.Join(
257 '\n',
258 Name,
259 DisplayName,
260 BlueprintPropertyNameLocalizer.GetChineseName(Name),
261 FriendlyTypeName,
262 ValueString,
263 CustomData as string,
264 ErrorMessage);
265
266 return searchText
267 .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
268 .All(term => searchableText.Contains(term, StringComparison.OrdinalIgnoreCase));
269 }
270
271 public bool SetValue(object? targetValue)
272 {
273 if (ValueSetter is null)
274 return false;
275
276 try
169 277 {
170 if (IsBasicType)
171 {
172 if (IsMultiEnum)
173 return MultiEnumControl;
174 else if (Type is not null && Type.IsEnum)
175 return EnumControl;
176 else if (Type == typeof(int) || Type == typeof(double) || Type == typeof(float) || Type == typeof(short) || Type == typeof(byte) || Type == typeof(uint) || Type == typeof(ushort))
177 return NumberControl;
178 else if (Type == typeof(bool))
179 return BoolControl;
180 else
181 return StringControl;
182 }
278 ValueSetter(targetValue);
279 Value = targetValue;
280 Parent?.PropagateBoxedValueType();
281 ValueChanged?.Invoke();
282 return true;
283 }
284 catch (Exception ex)
285 {
286 ShowSetValueError(targetValue, ex.Message);
287 return false;
288 }
289 }
290
291 private void CommitText(TextBox textBox)
292 {
293 if (textBox.Text == ValueString)
294 return;
295
296 var enteredText = textBox.Text;
297 if (TryConvertText(enteredText, out var targetValue, out var error))
298 {
299 if (SetValue(targetValue))
300 ToolTipService.SetToolTip(textBox, ValueString);
183 301 else
184 {
185 return null;
186 }
302 textBox.Text = ValueString;
303 }
304 else
305 {
306 textBox.Text = ValueString;
307 ShowSetValueError(enteredText, error);
187 308 }
188 309 }
189 /// <summary>
190 /// 属性的子属性
191 /// </summary>
192 public List<BlueprintPropertyViewData> Children { get; set; } = [];
193 /// <summary>
194 /// 是否是枚举类型
195 /// </summary>
196 public bool IsEnumerable => Type is not null && Type.IsAssignableTo(typeof(IEnumerable)) && Type != typeof(string);
197 /// <summary>
198 /// 是否是复合枚举类型
199 /// </summary>
200 public bool IsMultiEnum => Type is not null && Type.IsDefined(typeof(FlagsAttribute), false);
201 /// <summary>
202 /// 是否是基本类型
203 /// </summary>
204 public bool IsBasicType => Type is not null && (XFEConverter.IsBasicType(Type) || Type.IsEnum);
205 310
206 /// <summary>
207 /// 设置值
208 /// </summary>
209 /// <param name="targetValue">目标值</param>
210 public void SetValue(object? targetValue)
311 private bool TryConvertText(string text, out object? value, out string error)
211 312 {
313 value = null;
314 error = string.Empty;
315 if (EffectiveType is not { } type)
316 {
317 error = "Unknown property type.";
318 return false;
319 }
320
321 if (type == typeof(string))
322 {
323 value = text;
324 return true;
325 }
326
327 if (string.IsNullOrWhiteSpace(text) && IsNullable)
328 return true;
329
212 330 try
213 331 {
214 if (Parent is BlueprintPropertyViewData parentBlueprintPropertyViewData && Name is not null && parentBlueprintPropertyViewData.Type is not null)
332 if (type == typeof(char))
333 {
334 if (text.Length != 1)
335 throw new FormatException("A character value must contain exactly one character.");
336 value = text[0];
337 }
338 else if (type == typeof(Guid))
215 339 {
216 var memberInfo = parentBlueprintPropertyViewData.Type.GetMember(Name).Where(memberInfo => memberInfo is FieldInfo || memberInfo is PropertyInfo).FirstOrDefault();
217 if (memberInfo is FieldInfo fieldInfo)
340 value = Guid.Parse(text);
341 }
342 else if (type == typeof(TimeSpan))
343 {
344 value = TimeSpan.Parse(text, CultureInfo.InvariantCulture);
345 }
346 else if (type == typeof(DateTime))
347 {
348 value = DateTime.Parse(text, CultureInfo.CurrentCulture, DateTimeStyles.RoundtripKind);
349 }
350 else if (type == typeof(DateTimeOffset))
351 {
352 value = DateTimeOffset.Parse(text, CultureInfo.CurrentCulture, DateTimeStyles.RoundtripKind);
353 }
354 else if (type.IsEnum)
355 {
356 value = Enum.Parse(type, text, true);
357 }
358 else
359 {
360 // Convert directly to the target type. In particular, never route Int64,
361 // UInt64 or Decimal through double, which would silently lose blueprint IDs.
362 try
218 363 {
219 fieldInfo.SetValue(Parent.Value, targetValue);
364 value = Convert.ChangeType(text, type, CultureInfo.InvariantCulture);
220 365 }
221 else if (memberInfo is PropertyInfo propertyInfo)
366 catch (FormatException)
222 367 {
223 propertyInfo.SetValue(Parent.Value, targetValue);
368 value = Convert.ChangeType(text, type, CultureInfo.CurrentCulture);
224 369 }
225 Value = targetValue;
226 if (parentBlueprintPropertyViewData.Type.IsValueType)
227 SetValueType(parentBlueprintPropertyViewData);
228 370 }
371
372 return true;
229 373 }
230 catch (Exception ex)
374 catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException or ArgumentException)
231 375 {
232 ServiceManager.GetGlobalService<IMessageService>()?.ShowMessage($"{"Error_CantSetValue_CantSetProperty".GetLocalized()}{Name}({Type?.Name}){"Error_CantSetValue_ValueTo".GetLocalized()}{targetValue}: {ex.Message}", "Error".GetLocalized(), InfoBarSeverity.Error);
376 error = ex.Message;
377 return false;
233 378 }
234 379 }
235 380
236 private static void SetValueType(BlueprintPropertyViewData blueprintPropertyViewData)
381 private void PropagateBoxedValueType()
237 382 {
238 if (blueprintPropertyViewData.Parent is BlueprintPropertyViewData parentBlueprintViewData)
383 if (Type?.IsValueType != true || ValueSetter is null)
384 return;
385
386 ValueSetter(Value);
387 Parent?.PropagateBoxedValueType();
388 }
389
390 private void ShowSetValueError(object? targetValue, string detail)
391 {
392 ServiceManager.GetGlobalService<IMessageService>()?.ShowMessage(
393 $"{"Error_CantSetValue_CantSetProperty".GetLocalized()}{Name}({FriendlyTypeName})" +
394 $"{"Error_CantSetValue_ValueTo".GetLocalized()}{targetValue}: {detail}",
395 "Error".GetLocalized(),
396 InfoBarSeverity.Error);
397 }
398
399 private static bool IsSingleFlagValue(object value)
400 {
401 var bits = ToUInt64(value);
402 return bits == 0 || (bits & (bits - 1)) == 0;
403 }
404
405 private static ulong ToUInt64(object? value)
406 {
407 if (value is null)
408 return 0;
409
410 try
239 411 {
240 if (blueprintPropertyViewData.Type is not null && blueprintPropertyViewData.Type.IsValueType)
241 blueprintPropertyViewData.SetValue(blueprintPropertyViewData.Value);
242 SetValueType(parentBlueprintViewData);
412 return Convert.ToUInt64(value, CultureInfo.InvariantCulture);
243 413 }
414 catch (OverflowException)
415 {
416 return unchecked((ulong)Convert.ToInt64(value, CultureInfo.InvariantCulture));
417 }
418 }
419
420 private static string FormatValue(object? value) => value switch
421 {
422 null => string.Empty,
423 DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture),
424 DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("O", CultureInfo.InvariantCulture),
425 IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty,
426 _ => value.ToString() ?? string.Empty
427 };
428
429 private static string GetFriendlyTypeName(Type? type)
430 {
431 if (type is null)
432 return string.Empty;
433
434 var nullableType = Nullable.GetUnderlyingType(type);
435 if (nullableType is not null)
436 return $"{GetFriendlyTypeName(nullableType)}?";
437
438 if (!type.IsGenericType)
439 return type.Name;
440
441 var genericName = type.Name.Split('`')[0];
442 return $"{genericName}<{string.Join(", ", type.GetGenericArguments().Select(GetFriendlyTypeName))}>";
244 443 }
245 444 }
Modified SpaceEngineersBlueprintEditor/Strings/en-us/Resources.resw +8 -0
@@ -685,6 +685,14 @@
685 685 <value>Search properties...</value>
686 686 <comment>搜索属性...</comment>
687 687 </data>
688 <data name="BlueprintEditSubPage_AutoSuggestBox_Properties.PlaceholderText" xml:space="preserve">
689 <value>Search block properties or values...</value>
690 <comment>方块属性搜索框</comment>
691 </data>
692 <data name="BlueprintEditSubPage_AutoSuggestBox_BlueprintProperties.PlaceholderText" xml:space="preserve">
693 <value>Search blueprint properties or values...</value>
694 <comment>蓝图属性搜索框</comment>
695 </data>
688 696 <data name="GameDefinitionsViewPage_AutoSuggestBox_Definitions.PlaceholderText" xml:space="preserve">
689 697 <value>Search properties...</value>
690 698 <comment>搜索属性...</comment>
Modified SpaceEngineersBlueprintEditor/Strings/zh-cn/Resources.resw +8 -0
@@ -685,6 +685,14 @@
685 685 <value>搜索属性...</value>
686 686 <comment>搜索属性...</comment>
687 687 </data>
688 <data name="BlueprintEditSubPage_AutoSuggestBox_Properties.PlaceholderText" xml:space="preserve">
689 <value>搜索方块属性或属性值...</value>
690 <comment>方块属性搜索框</comment>
691 </data>
692 <data name="BlueprintEditSubPage_AutoSuggestBox_BlueprintProperties.PlaceholderText" xml:space="preserve">
693 <value>搜索蓝图属性或属性值...</value>
694 <comment>蓝图属性搜索框</comment>
695 </data>
688 696 <data name="GameDefinitionsViewPage_AutoSuggestBox_Definitions.PlaceholderText" xml:space="preserve">
689 697 <value>搜索属性...</value>
690 698 <comment>搜索属性...</comment>
Added SpaceEngineersBlueprintEditor/Styles/Materials.xaml +31 -0
@@ -0,0 +1,31 @@
1 <?xml version="1.0" encoding="utf-8"?>
2 <ResourceDictionary
3 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
5
6 <ResourceDictionary.ThemeDictionaries>
7 <ResourceDictionary x:Key="Default">
8 <AcrylicBrush
9 x:Key="AppAcrylicInAppBrush"
10 TintColor="#FF202020"
11 TintOpacity="0.68"
12 TintLuminosityOpacity="0.45"
13 FallbackColor="#FF202020"/>
14 </ResourceDictionary>
15
16 <ResourceDictionary x:Key="Light">
17 <AcrylicBrush
18 x:Key="AppAcrylicInAppBrush"
19 TintColor="#FFF3F3F3"
20 TintOpacity="0.72"
21 TintLuminosityOpacity="0.85"
22 FallbackColor="#FFF3F3F3"/>
23 </ResourceDictionary>
24
25 <ResourceDictionary x:Key="HighContrast">
26 <SolidColorBrush
27 x:Key="AppAcrylicInAppBrush"
28 Color="{ThemeResource SystemColorWindowColor}"/>
29 </ResourceDictionary>
30 </ResourceDictionary.ThemeDictionaries>
31 </ResourceDictionary>
Modified SpaceEngineersBlueprintEditor/Utilities/Helpers/SpaceEngineersHelper.cs +137 -59
Added SpaceEngineersBlueprintEditor/Utilities/Localization/BlueprintPropertyNameLocalizer.cs +67 -0
Modified SpaceEngineersBlueprintEditor/ViewModels/BlueprintEditSubPageViewModel.cs +67 -40
Modified SpaceEngineersBlueprintEditor/Views/AppShellPage.xaml +1 -1
@@ -5,7 +5,7 @@
5 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6 6 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
7 7 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8 xmlns:add="using:XFEExtension.NetCore.WinUIHelper.Utilities.Addition"
8 xmlns:add="using:XFEExtension.NetCore.WinUIHelper.Utilities.Additions"
9 9 mc:Ignorable="d" Loaded="Page_Loaded">
10 10
11 11 <Grid x:Name="mainGrid">
Modified SpaceEngineersBlueprintEditor/Views/BlueprintEditSubPage.xaml +62 -36
Modified SpaceEngineersBlueprintEditor/Views/BlueprintEditSubPage.xaml.cs +6 -6
@@ -1,4 +1,5 @@
1 1 using Microsoft.UI.Composition;
2 using Microsoft.UI.Xaml.Media;
2 3 using Microsoft.UI.Xaml.Navigation;
3 4 using SpaceEngineersBlueprintEditor.Model;
4 5 using SpaceEngineersBlueprintEditor.ViewModels;
@@ -6,16 +7,16 @@ using SpaceEngineersBlueprintEditor.ViewModels;
6 7 namespace SpaceEngineersBlueprintEditor.Views;
7 8
8 9 /// <summary>
9 /// ��ͼ�༭ҳ����ϸҳ
10 /// 蓝图编辑页的详细页
10 11 /// </summary>
11 12 public sealed partial class BlueprintEditSubPage : Page
12 13 {
13 private readonly Compositor compositor = App.MainWindow.Compositor;
14 14 public BlueprintEditSubPageViewModel ViewModel { get; set; } = new();
15 15
16 16 public BlueprintEditSubPage()
17 17 {
18 18 this.InitializeComponent();
19 var compositor = CompositionTarget.GetCompositorForCurrentThread();
19 20 NavigationCacheMode = NavigationCacheMode.Required;
20 21 ViewModel.BlueprintTreeViewService.Initialize(blueprintPropertyTreeView);
21 22 ViewModel.CubeBlockTreeViewService.Initialize(cubePropertyTreeView);
@@ -56,7 +57,7 @@ public sealed partial class BlueprintEditSubPage : Page
56 57 treeViewNode.Children.Add(new TreeViewNode
57 58 {
58 59 Content = child,
59 HasUnrealizedChildren = !child.IsBasicType && child.Type is not null
60 HasUnrealizedChildren = child.CanExpand
60 61 });
61 62 }
62 63 treeViewNode.HasUnrealizedChildren = false;
@@ -67,9 +68,8 @@ public sealed partial class BlueprintEditSubPage : Page
67 68
68 69 private void TreeView_Collapsed(TreeView sender, TreeViewCollapsedEventArgs args)
69 70 {
70 if (args.Node.Content is BlueprintPropertyViewData blueprintPropertyViewData)
71 blueprintPropertyViewData.Children.Clear();
72 71 args.Node.Children.Clear();
73 args.Node.HasUnrealizedChildren = true;
72 args.Node.HasUnrealizedChildren =
73 args.Node.Content is BlueprintPropertyViewData property && property.CanExpand;
74 74 }
75 75 }
Modified SpaceEngineersBlueprintEditor/Views/BlueprintsViewPage.xaml +4 -4
Modified SpaceEngineersBlueprintEditor/Views/GameDefinitionsViewPage.xaml +2 -2
Modified SpaceEngineersBlueprintEditor/Views/MainPage.xaml.cs +3 -2