返回提交历史
Added
SpaceEngineersBlueprintEditor.SpaceEngineersCore/BlueprintEditing/BlockGroupResolver.cs
+227
-0
Modified
SpaceEngineersBlueprintEditor/App.xaml
+1
-0
Modified
SpaceEngineersBlueprintEditor/App.xaml.cs
+1
-1
Added
SpaceEngineersBlueprintEditor/Assets/Localization/BlueprintPropertyNames.zh-CN.json
+169
-0
Modified
SpaceEngineersBlueprintEditor/Implements/Services/BackgroundImageService.cs
+4
-2
Modified
SpaceEngineersBlueprintEditor/Implements/Services/BlueprintDropService.cs
+3
-2
Modified
SpaceEngineersBlueprintEditor/Model/BlueprintPropertyViewData.cs
+342
-143
Modified
SpaceEngineersBlueprintEditor/Strings/en-us/Resources.resw
+8
-0
Modified
SpaceEngineersBlueprintEditor/Strings/zh-cn/Resources.resw
+8
-0
Added
SpaceEngineersBlueprintEditor/Styles/Materials.xaml
+31
-0
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
Modified
SpaceEngineersBlueprintEditor/Views/BlueprintEditSubPage.xaml
+62
-36
Modified
SpaceEngineersBlueprintEditor/Views/BlueprintEditSubPage.xaml.cs
+6
-6
Modified
SpaceEngineersBlueprintEditor/Views/BlueprintsViewPage.xaml
+4
-4
Modified
SpaceEngineersBlueprintEditor/Views/GameDefinitionsViewPage.xaml
+2
-2
Modified
SpaceEngineersBlueprintEditor/Views/MainPage.xaml.cs
+3
-2
SpaceEngineersModDev/SpaceEngineersBlueprintEditorInWinUI
优化蓝图属性树显示、搜索与本地化支持
全面提升蓝图属性树的显示、搜索和编辑体验: - 新增属性名本地化,支持中文显示名和本地化 JSON 映射 - 属性树支持多字段模糊搜索,提升查找效率 - 重构属性节点结构,优化子节点加载与循环引用检测 - 编辑控件类型自动适配,提升编辑一致性与友好性 - 增强方块组与集合类型处理,准确显示组坐标和元素信息 - 统一 XAML 模板,优化界面细节与交互动画 - 增加中英文提示文本资源,修正部分命名空间和资源路径 极大提升蓝图属性树的可读性、可编辑性和本地化体验。
843a21e
代码差异
19 个文件
+1143
-298
@@ -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
}
@@ -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>
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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>
@@ -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>
@@ -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>
@@ -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">
@@ -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
}