Forge
csharpdeeb25a2
1using System.Reflection;
2using System.Text.Json;
3using System.Text.Json.Nodes;
4using System.Text.Json.Serialization;
5using Avalonia;
6using Avalonia.Controls;
7using Avalonia.Controls.ApplicationLifetimes;
8using Avalonia.Media;
9using Avalonia.VisualTree;
10using CascadeIDE.Features.Documents;
11using CascadeIDE.Features.Editor.Presentation;
12using CascadeIDE.ViewModels;
13using CascadeIDE.Views;
14
15namespace CascadeIDE.Services;
16
17/// <summary>
18/// Расширенный слой для <see cref="UiThemeSnapshot"/>: разрешённые ресурсы под текущей темой,
19/// рамка главного окна, именованные регионы лэйаута, открытые документы дока и смонтированные Monaco host.
20/// </summary>
21internal static class UiThemeDeepSnapshot
22{
23 /// <summary>Общий лимит для <c>text_preview</c> и <c>model_text_preview</c>.</summary>
24 private const int DockTextPreviewMaxChars = 240;
25
26 private static readonly JsonSerializerOptions NodeOptions = new()
27 {
28 WriteIndented = true,
29 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
30 };
31
32 /// <summary>Ключи из <c>App.axaml</c>, которых нет в <see cref="UiThemeApply.Keys"/>.</summary>
33 private static readonly string[] AdditionalCascadeThemeKeys =
34 [
35 "CascadeTheme.PowerSolutionTreePanelBackground",
36 "CascadeTheme.PowerSolutionTreeForeground",
37 "CascadeTheme.PowerSolutionTreeItemSelectedBackground",
38 "CascadeTheme.PowerSolutionTreeItemPointerOverBackground",
39 "CascadeTheme.PowerSolutionPanelBorderSubtle",
40 "CascadeTheme.PowerSolutionTaskQueueBubbleBackground",
41 "CascadeTheme.PowerEditorIslandFrameBrush",
42 "CascadeTheme.PowerChatIslandFrameBrush",
43 "CascadeTheme.PowerSolutionIslandFrameBrush"
44 ];
45
46 /// <summary>Имена из разметки — острова, доки, чат, хост стека MFD, бейдж режима.</summary>
47 private static readonly string[] LayoutRegionNames =
48 [
49 "RootWindow",
50 "MainGrid",
51 "DockIslandInner",
52 "DocumentsDock",
53 "SolutionIslandInner",
54 "ChatIslandInner",
55 "ChatPanelRoot",
56 "MfdContourStackHost",
57 "ModeBadge",
58 "UiModeBloomOverlay",
59 "IntercomSkiaSurface",
60 "IntegratedTerminalControl"
61 ];
62
63 public static void MergeIntoJsonRoot(JsonObject root, Application app)
64 {
65 root["cascade_theme_resolved"] = JsonSerializer.SerializeToNode(BuildCascadeThemeResolved(app), NodeOptions);
66 if (app.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } mw })
67 {
68 root["window_frame"] = JsonSerializer.SerializeToNode(BuildWindowFrame(app, mw), NodeOptions);
69 root["layout_regions"] = JsonSerializer.SerializeToNode(BuildLayoutRegions(mw), NodeOptions);
70 var vm = mw.DataContext as MainWindowViewModel;
71 var dockEditors = BuildAllDockTextEditors(mw, vm);
72 root["dock_text_editors"] = JsonSerializer.SerializeToNode(dockEditors, NodeOptions);
73 var materializedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
74 foreach (var row in dockEditors)
75 {
76 if (row.TryGetValue("file_path", out var fp) && fp is string path && path.Length > 0)
77 materializedPaths.Add(path);
78 }
79
80 root["dock_open_documents"] = JsonSerializer.SerializeToNode(BuildDockOpenDocuments(vm, materializedPaths), NodeOptions);
81 root["top_levels"] = JsonSerializer.SerializeToNode(BuildTopLevels(app, mw), NodeOptions);
82 }
83 else
84 {
85 root["window_frame"] = null;
86 root["layout_regions"] = null;
87 root["dock_text_editors"] = null;
88 root["dock_open_documents"] = null;
89 root["top_levels"] = null;
90 }
91 }
92
93 private static SortedDictionary<string, string?> BuildCascadeThemeResolved(Application app)
94 {
95 var keys = new HashSet<string>(StringComparer.Ordinal);
96 foreach (var f in typeof(UiThemeApply.Keys).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
97 {
98 if (f.GetValue(null) is string s && s.StartsWith("CascadeTheme.", StringComparison.Ordinal))
99 keys.Add(s);
100 }
101 foreach (var k in AdditionalCascadeThemeKeys)
102 keys.Add(k);
103
104 var variant = app.ActualThemeVariant;
105 var map = new SortedDictionary<string, string?>(StringComparer.Ordinal);
106 foreach (var key in keys)
107 {
108 if (!app.TryGetResource(key, variant, out var raw) || raw is null)
109 {
110 map[key] = null;
111 continue;
112 }
113
114 map[key] = raw switch
115 {
116 IBrush b => UiThemeSnapshot.FormatBrushForJson(b),
117 _ => raw.ToString()
118 };
119 }
120
121 return map;
122 }
123
124 private static Dictionary<string, object?> BuildWindowFrame(Application app, Window w)
125 {
126 var bg = w.Background is IBrush wb ? UiThemeSnapshot.FormatBrushForJson(wb) : null;
127 return new Dictionary<string, object?>
128 {
129 ["title"] = w.Title,
130 ["bounds_w"] = Math.Round(w.Bounds.Width, 1),
131 ["bounds_h"] = Math.Round(w.Bounds.Height, 1),
132 ["client_width"] = Math.Round(w.ClientSize.Width, 1),
133 ["client_height"] = Math.Round(w.ClientSize.Height, 1),
134 ["extend_client_area_to_decorations"] = w.ExtendClientAreaToDecorationsHint,
135 ["window_decorations"] = w.WindowDecorations.ToString(),
136 ["extend_client_area_title_bar_height_hint"] = w.ExtendClientAreaTitleBarHeightHint,
137 ["transparency_level_hint"] = w.TransparencyLevelHint.ToString(),
138 ["actual_theme_variant"] = app.ActualThemeVariant.Key.ToString(),
139 ["background_brush"] = bg
140 };
141 }
142
143 /// <summary>Все открытые <see cref="Window"/> процесса (главное, окно-хост зоны Mfd, настройки, превью) — для MCP и паритета при мультиоконности (ADR 0017).</summary>
144 private static List<Dictionary<string, object?>> BuildTopLevels(Application app, Window mainWindow)
145 {
146 var list = new List<Dictionary<string, object?>>();
147 if (app.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
148 return list;
149
150 foreach (var w in desktop.Windows)
151 {
152 if (w is not Window win)
153 continue;
154
155 var role = ReferenceEquals(win, mainWindow)
156 ? "main"
157 : win is MfdHostWindow
158 ? "mfd_host"
159 : "other";
160
161 list.Add(new Dictionary<string, object?>
162 {
163 ["role"] = role,
164 ["window_type"] = win.GetType().Name,
165 ["title"] = win.Title,
166 ["position_x"] = win.Position.X,
167 ["position_y"] = win.Position.Y,
168 ["client_width"] = Math.Round(win.ClientSize.Width, 1),
169 ["client_height"] = Math.Round(win.ClientSize.Height, 1),
170 ["window_state"] = win.WindowState.ToString(),
171 ["is_active"] = win.IsActive
172 });
173 }
174
175 return list;
176 }
177
178 private static Dictionary<string, Dictionary<string, object?>?> BuildLayoutRegions(TopLevel topLevel)
179 {
180 var dict = new Dictionary<string, Dictionary<string, object?>?>(StringComparer.Ordinal);
181 foreach (var name in LayoutRegionNames)
182 {
183 dict[name] = UiControlAppearance.TryBuildNamedRegionSnapshot(topLevel, name);
184 }
185
186 return dict;
187 }
188
189 private static List<Dictionary<string, object?>> BuildAllDockTextEditors(Visual root, MainWindowViewModel? vm)
190 {
191 var list = new List<Dictionary<string, object?>>();
192 foreach (var v in EnumerateVisualDescendants(root))
193 {
194 if (v is not DockDocumentView dockView)
195 continue;
196 if (dockView.FindControl<MonacoEditorHostControl>("MonacoHost") is not { } host)
197 continue;
198 if (dockView.DataContext is not DockDocumentViewModel dvm)
199 continue;
200
201 var doc = dvm.Doc;
202 var currentPath = vm?.CurrentFilePath;
203 var isActivePath = !string.IsNullOrEmpty(currentPath)
204 && string.Equals(doc.FilePath, currentPath, StringComparison.OrdinalIgnoreCase);
205 var modelLen = doc.Content?.Length ?? 0;
206 host.Session.ReadSnapshot(out _, out var editorText, out _, out _, out _);
207 var editorLen = editorText.Length;
208 var (effBg, effFg) = UiControlAppearance.GetEffectiveColors(host);
209
210 double x = 0, y = 0, w = host.Bounds.Width, h = host.Bounds.Height;
211 var topLeft = host.TranslatePoint(new Point(0, 0), root);
212 if (topLeft is { } p)
213 {
214 x = p.X;
215 y = p.Y;
216 }
217
218 var preview = editorLen <= DockTextPreviewMaxChars
219 ? editorText
220 : editorText[..DockTextPreviewMaxChars] + "…";
221 var lineCount = editorLen == 0 ? 0 : editorText.Split('\n').Length;
222
223 list.Add(new Dictionary<string, object?>
224 {
225 ["file_path"] = doc.FilePath,
226 ["dock_title"] = dvm.Title,
227 ["matches_main_window_current_file"] = isActivePath,
228 ["editor_visible"] = host.IsVisible,
229 ["document_length"] = editorLen,
230 ["model_content_length"] = modelLen,
231 ["length_matches_model"] = editorLen == modelLen,
232 ["line_count"] = lineCount,
233 ["bounds"] = new Dictionary<string, double>
234 {
235 ["x"] = Math.Round(x, 1),
236 ["y"] = Math.Round(y, 1),
237 ["w"] = Math.Round(w, 1),
238 ["h"] = Math.Round(h, 1)
239 },
240 ["name"] = (host as StyledElement)?.Name ?? "",
241 ["font_family"] = null,
242 ["font_size"] = 0.0,
243 ["foreground"] = UiThemeSnapshot.FormatBrushForJson(host.Foreground),
244 ["background"] = UiThemeSnapshot.FormatBrushForJson(host.Background),
245 ["effective_background"] = effBg,
246 ["effective_foreground"] = effFg,
247 ["text_preview"] = preview
248 });
249 }
250
251 return list;
252 }
253
254 /// <summary>
255 /// Все открытые вкладки из <see cref="DocumentsWorkspaceViewModel.DockDocuments"/> (модель дока).
256 /// Неактивные вкладки часто без Monaco host в визуальном дереве — смотри <c>editor_in_visual_tree</c>.
257 /// </summary>
258 private static List<Dictionary<string, object?>> BuildDockOpenDocuments(MainWindowViewModel? vm, HashSet<string> editorMaterializedPaths)
259 {
260 var list = new List<Dictionary<string, object?>>();
261 if (vm is null)
262 return list;
263
264 var current = vm.CurrentFilePath;
265 var docs = vm.Documents.DockDocuments;
266 for (var i = 0; i < docs.Count; i++)
267 {
268 if (docs[i] is not DockDocumentViewModel dvm)
269 continue;
270
271 var doc = dvm.Doc;
272 var path = doc.FilePath;
273 var isActive = !string.IsNullOrEmpty(current)
274 && string.Equals(path, current, StringComparison.OrdinalIgnoreCase);
275 var modelText = doc.Content ?? "";
276 var modelLen = modelText.Length;
277 var modelPreview = modelLen <= DockTextPreviewMaxChars
278 ? modelText
279 : modelText[..DockTextPreviewMaxChars] + "…";
280
281 list.Add(new Dictionary<string, object?>
282 {
283 ["tab_index"] = i,
284 ["file_path"] = path,
285 ["dock_title"] = dvm.Title,
286 ["display_title"] = doc.DisplayTitle,
287 ["is_active"] = isActive,
288 ["is_dirty"] = doc.IsDirty,
289 ["model_content_length"] = modelLen,
290 ["model_text_preview"] = modelPreview,
291 ["editor_in_visual_tree"] = editorMaterializedPaths.Contains(path)
292 });
293 }
294
295 return list;
296 }
297
298 private static IEnumerable<Visual> EnumerateVisualDescendants(Visual node)
299 {
300 foreach (var child in node.GetVisualChildren())
301 {
302 yield return child;
303 foreach (var d in EnumerateVisualDescendants(child))
304 yield return d;
305 }
306 }
307
308}
309
View only · write via MCP/CIDE