返回提交历史
Modified
XFEToolBox/Resources/Editor/editor.html
+115
-10
Modified
XFEToolBox/Resources/Style/StandardControlsStyle.xaml
+130
-4
Modified
XFEToolBox/Utilities/ToolProjectRunService.cs
+36
-1
Modified
XFEToolBox/ViewModel/Windows/MainWindowViewModel.cs
+0
-40
Added
XFEToolBox/Views/Controls/WindowResizeGrip.xaml
+18
-0
Added
XFEToolBox/Views/Controls/WindowResizeGrip.xaml.cs
+55
-0
Modified
XFEToolBox/Views/Windows/MainWindow.xaml
+2
-6
Modified
XFEToolBox/Views/Windows/MainWindow.xaml.cs
+5
-1
Modified
XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml
+8
-7
Modified
XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml.cs
+211
-30
Modified
XFEToolBox/XFEToolBox.Client.csproj
+1
-1
XFEstudio/XFEToolBox
支持图片资源、统一缩放控件、对齐VS快捷键
工程资源管理器支持图片文件预览、拖拽、重命名等操作,新增高质量图片缩放预览。主窗口与工具运行窗口统一使用 WindowResizeGrip 控件,提升缩放体验和一致性。代码编辑器快捷键全面对齐 Visual Studio,优化菜单项提示。新增工程生成(Build)功能,支持仅生成不运行。优化资源管理器操作权限和空状态文案,修复快捷键描述不一致等问题,项目版本号提升至 1.0.0。整体提升编辑器易用性和专业性。
9639638
代码差异
11 个文件
+581
-100
@@ -21,7 +21,14 @@
21
21
background: var(--surface); color: var(--text);
22
22
font-family: "Segoe UI", "Microsoft YaHei UI", sans-serif;
23
23
}
24
body { display: grid; grid-template-rows: 42px 1fr 27px; }
24
/* HWND WebView2 不接受 WPF 父级 Clip,圆角必须由 Chromium 自身完成裁切。 */
25
body {
26
display: grid;
27
grid-template-rows: 42px 1fr 27px;
28
border-radius: 12px;
29
clip-path: inset(0 round 12px);
30
contain: paint;
31
}
25
32
#tabs {
26
33
display: flex; align-items: flex-end; gap: 4px; overflow-x: auto; overflow-y: hidden;
27
34
padding: 5px 7px 0; background: var(--surface-subtle); border-bottom: 1px solid var(--border);
@@ -115,6 +122,9 @@
115
122
let suppressChange = false;
116
123
let currentTheme = 'light';
117
124
let tabOrder = [];
125
let renderAllWhitespace = false;
126
let wordWrapEnabled = false;
127
let allRegionsFolded = false;
118
128
119
129
require.config({ paths: { vs: './monaco/vs' } });
120
130
require(['vs/editor/editor.main'], function () {
@@ -308,18 +318,113 @@
308
318
validateModel(editor.getModel());
309
319
post({ type: 'changed', path: activePath });
310
320
});
311
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => requestHostSave());
312
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyS, () => requestHostSave());
313
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyN, () => post({ type: 'command', command: 'newFile' }));
314
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyN, () => post({ type: 'command', command: 'newProject' }));
315
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyO, () => post({ type: 'command', command: 'openProject' }));
316
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KeyF, () => post({ type: 'command', command: 'format' }));
317
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyE, () => post({ type: 'command', command: 'export' }));
318
editor.addCommand(monaco.KeyCode.F5, () => post({ type: 'command', command: 'run' }));
319
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.F5, () => post({ type: 'command', command: 'preview' }));
321
registerVisualStudioKeybindings();
320
322
post({ type: 'ready' });
321
323
});
322
324
325
function registerVisualStudioKeybindings() {
326
const Ctrl = monaco.KeyMod.CtrlCmd;
327
const Shift = monaco.KeyMod.Shift;
328
const Alt = monaco.KeyMod.Alt;
329
const chord = (first, second) => monaco.KeyMod.chord(first, second);
330
const bind = (keybinding, handler) => editor.addCommand(keybinding, handler);
331
const ctrlKey = key => Ctrl | key;
332
const ctrlShiftKey = key => Ctrl | Shift | key;
333
334
// Visual Studio:文件、项目、生成与运行。
335
bind(ctrlKey(monaco.KeyCode.KeyS), requestHostSave);
336
bind(ctrlShiftKey(monaco.KeyCode.KeyS), requestHostSave);
337
bind(ctrlKey(monaco.KeyCode.KeyN), () => post({ type: 'command', command: 'newFile' }));
338
bind(ctrlShiftKey(monaco.KeyCode.KeyA), () => post({ type: 'command', command: 'newFile' }));
339
bind(ctrlShiftKey(monaco.KeyCode.KeyN), () => post({ type: 'command', command: 'newProject' }));
340
bind(ctrlShiftKey(monaco.KeyCode.KeyO), () => post({ type: 'command', command: 'openProject' }));
341
bind(ctrlShiftKey(monaco.KeyCode.KeyB), () => post({ type: 'command', command: 'build' }));
342
bind(ctrlShiftKey(monaco.KeyCode.KeyE), () => post({ type: 'command', command: 'export' }));
343
bind(monaco.KeyCode.F5, () => post({ type: 'command', command: 'run' }));
344
bind(Ctrl | monaco.KeyCode.F5, () => post({ type: 'command', command: 'run' }));
345
bind(ctrlShiftKey(monaco.KeyCode.KeyW), () => post({ type: 'command', command: 'preview' }));
346
347
// Visual Studio:格式化、代码清理、注释及快速信息。
348
bind(chord(ctrlKey(monaco.KeyCode.KeyK), ctrlKey(monaco.KeyCode.KeyD)), () => runEditorAction('editor.action.formatDocument'));
349
bind(chord(ctrlKey(monaco.KeyCode.KeyK), ctrlKey(monaco.KeyCode.KeyF)), formatCurrentSelection);
350
bind(chord(ctrlKey(monaco.KeyCode.KeyK), ctrlKey(monaco.KeyCode.KeyE)), cleanupCurrentDocument);
351
bind(chord(ctrlKey(monaco.KeyCode.KeyK), ctrlKey(monaco.KeyCode.KeyC)), () => runEditorAction('editor.action.addCommentLine'));
352
bind(chord(ctrlKey(monaco.KeyCode.KeyK), ctrlKey(monaco.KeyCode.KeyU)), () => runEditorAction('editor.action.removeCommentLine'));
353
bind(chord(ctrlKey(monaco.KeyCode.KeyK), ctrlKey(monaco.KeyCode.KeyI)), () => runEditorAction('editor.action.showHover'));
354
355
// Visual Studio:折叠、括号跳转、空白及自动换行。
356
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyM)), () => runEditorAction('editor.toggleFold'));
357
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyA)), () => { allRegionsFolded = true; return runEditorAction('editor.foldAll'); });
358
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyO)), () => { allRegionsFolded = true; return runEditorAction('editor.foldAll'); });
359
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyX)), () => { allRegionsFolded = false; return runEditorAction('editor.unfoldAll'); });
360
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyS)), () => runEditorAction('editor.fold'));
361
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyE)), () => runEditorAction('editor.unfold'));
362
bind(chord(ctrlKey(monaco.KeyCode.KeyM), ctrlKey(monaco.KeyCode.KeyL)), toggleAllFolding);
363
bind(chord(ctrlKey(monaco.KeyCode.KeyE), ctrlKey(monaco.KeyCode.KeyW)), toggleWordWrap);
364
bind(chord(ctrlKey(monaco.KeyCode.KeyR), ctrlKey(monaco.KeyCode.KeyW)), toggleWhitespace);
365
bind(ctrlKey(monaco.KeyCode.BracketRight), () => runEditorAction('editor.action.jumpToBracket'));
366
bind(ctrlShiftKey(monaco.KeyCode.BracketRight), () => runEditorAction('editor.action.selectToBracket'));
367
368
// Visual Studio:补全、参数提示、大小写、重复行与插入行。
369
bind(ctrlKey(monaco.KeyCode.Space), () => runEditorAction('editor.action.triggerSuggest'));
370
bind(ctrlKey(monaco.KeyCode.KeyJ), () => runEditorAction('editor.action.triggerSuggest'));
371
bind(ctrlShiftKey(monaco.KeyCode.Space), () => runEditorAction('editor.action.triggerParameterHints'));
372
bind(ctrlKey(monaco.KeyCode.KeyD), () => runEditorAction('editor.action.duplicateSelection'));
373
bind(ctrlKey(monaco.KeyCode.KeyU), () => runEditorAction('editor.action.transformToLowercase'));
374
bind(ctrlShiftKey(monaco.KeyCode.KeyU), () => runEditorAction('editor.action.transformToUppercase'));
375
bind(ctrlKey(monaco.KeyCode.Enter), () => runEditorAction('editor.action.insertLineBefore'));
376
bind(ctrlShiftKey(monaco.KeyCode.Enter), () => runEditorAction('editor.action.insertLineAfter'));
377
bind(Shift | Alt | monaco.KeyCode.KeyT, () => runEditorAction('editor.action.transpose'));
378
}
379
380
function runEditorAction(actionId) {
381
const action = editor?.getAction(actionId);
382
if (action) return action.run();
383
editor?.trigger('xfe.visualStudioKeybindings', actionId, null);
384
return Promise.resolve();
385
}
386
387
async function cleanupCurrentDocument() {
388
await runEditorAction('editor.action.trimTrailingWhitespace');
389
await runEditorAction('editor.action.formatDocument');
390
}
391
392
function formatCurrentSelection() {
393
const model = editor?.getModel();
394
const selection = editor?.getSelection();
395
if (!model || !selection || selection.isEmpty()) return runEditorAction('editor.action.formatDocument');
396
const language = model.getLanguageId();
397
const selectedText = model.getValueInRange(selection);
398
const formatted = language === 'xml'
399
? formatXml(selectedText)
400
: language === 'csharp'
401
? formatCSharp(selectedText)
402
: selectedText;
403
if (formatted === selectedText) return;
404
editor.executeEdits('xfe.formatSelection', [{ range: selection, text: preserveSelectionIndentation(model, selection, formatted), forceMoveMarkers: true }]);
405
}
406
407
function preserveSelectionIndentation(model, selection, formatted) {
408
const line = model.getLineContent(selection.startLineNumber);
409
const baseIndent = line.slice(0, Math.max(0, selection.startColumn - 1)).match(/^\s*/)?.[0] || '';
410
return formatted.split('\n').map((value, index) => index === 0 || !value ? value : baseIndent + value).join('\n');
411
}
412
413
function toggleAllFolding() {
414
allRegionsFolded = !allRegionsFolded;
415
return runEditorAction(allRegionsFolded ? 'editor.foldAll' : 'editor.unfoldAll');
416
}
417
418
function toggleWordWrap() {
419
wordWrapEnabled = !wordWrapEnabled;
420
editor.updateOptions({ wordWrap: wordWrapEnabled ? 'on' : 'off' });
421
}
422
423
function toggleWhitespace() {
424
renderAllWhitespace = !renderAllWhitespace;
425
editor.updateOptions({ renderWhitespace: renderAllWhitespace ? 'all' : 'selection' });
426
}
427
323
428
function languageFor(path) {
324
429
const lower = path.toLowerCase();
325
430
if (lower.endsWith('.cs')) return 'csharp';
@@ -171,10 +171,10 @@
171
171
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="22"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
172
172
<Border x:Name="Box" Width="18" Height="18" Background="{DynamicResource ToolControlBackgroundBrush}"
173
173
BorderBrush="{DynamicResource ToolControlBorderBrush}" BorderThickness="1" CornerRadius="6" VerticalAlignment="Center"/>
174
<Path x:Name="CheckMark" Width="10" Height="8" Margin="4,0,0,0" HorizontalAlignment="Left" VerticalAlignment="Center"
175
Data="M 1,3.5 L 3.5,6 L 8,1" Stretch="Fill" Stroke="White" StrokeThickness="1.8"
174
<Path x:Name="CheckMark" Width="10" Height="8" HorizontalAlignment="Center" VerticalAlignment="Center"
175
Data="M 1,3.5 L 3.5,6 L 8,1" Stretch="Uniform" Stroke="White" StrokeThickness="1.8"
176
176
StrokeStartLineCap="Round" StrokeEndLineCap="Round" Visibility="Collapsed"/>
177
<Border x:Name="IndeterminateMark" Width="8" Height="2" Margin="5,0,0,0" HorizontalAlignment="Left"
177
<Border x:Name="IndeterminateMark" Width="8" Height="2" HorizontalAlignment="Center"
178
178
VerticalAlignment="Center" Background="White" CornerRadius="1" Visibility="Collapsed"/>
179
179
<ContentPresenter Grid.Column="1" Margin="5,0,0,0" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
180
180
</Grid>
@@ -315,6 +315,74 @@
315
315
</Style>
316
316
<Style TargetType="ListBoxItem" BasedOn="{StaticResource ToolBoxListBoxItemStyle}"/>
317
317
318
<!-- ListView / GridView 同样采用全局默认样式,工具无需单独引用资源键。 -->
319
<Style x:Key="ToolBoxGridViewColumnHeaderStyle" TargetType="GridViewColumnHeader">
320
<Setter Property="Height" Value="44"/><Setter Property="Padding" Value="15,0"/>
321
<Setter Property="Background" Value="{DynamicResource ToolAccentSoftBrush}"/>
322
<Setter Property="Foreground" Value="{DynamicResource ToolTextSecondaryBrush}"/>
323
<Setter Property="BorderBrush" Value="{DynamicResource ToolControlBorderBrush}"/><Setter Property="BorderThickness" Value="0,0,0,1"/>
324
<Setter Property="FontSize" Value="10.5"/><Setter Property="FontWeight" Value="SemiBold"/>
325
<Setter Property="HorizontalContentAlignment" Value="Left"/><Setter Property="VerticalContentAlignment" Value="Center"/>
326
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
327
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="GridViewColumnHeader">
328
<Grid Background="Transparent">
329
<Border x:Name="Surface" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}"
330
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
331
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
332
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" RecognizesAccessKey="True"/>
333
</Border>
334
<Thumb x:Name="PART_HeaderGripper" Width="8" HorizontalAlignment="Right" Background="Transparent" Cursor="SizeWE">
335
<Thumb.Template><ControlTemplate TargetType="Thumb">
336
<Grid Background="Transparent"><Border x:Name="Grip" Width="1" HorizontalAlignment="Right" Margin="0,10,1,10"
337
Background="{DynamicResource ToolControlBorderBrush}" Opacity="0.48"/></Grid>
338
<ControlTemplate.Triggers>
339
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Grip" Property="Background" Value="{DynamicResource MainColor}"/><Setter TargetName="Grip" Property="Opacity" Value="0.8"/></Trigger>
340
</ControlTemplate.Triggers>
341
</ControlTemplate></Thumb.Template>
342
</Thumb>
343
</Grid>
344
<ControlTemplate.Triggers>
345
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolControlHoverBrush}"/></Trigger>
346
<Trigger Property="Role" Value="PaddingHeader"><Setter TargetName="PART_HeaderGripper" Property="Visibility" Value="Collapsed"/></Trigger>
347
</ControlTemplate.Triggers>
348
</ControlTemplate></Setter.Value></Setter>
349
</Style>
350
<Style x:Key="ToolBoxListViewItemStyle" TargetType="ListViewItem">
351
<Setter Property="MinHeight" Value="46"/><Setter Property="Padding" Value="0"/>
352
<Setter Property="HorizontalContentAlignment" Value="Stretch"/><Setter Property="VerticalContentAlignment" Value="Center"/>
353
<Setter Property="Background" Value="Transparent"/><Setter Property="Foreground" Value="{DynamicResource ToolTextPrimaryBrush}"/>
354
<Setter Property="BorderBrush" Value="{DynamicResource ToolDividerBrush}"/><Setter Property="BorderThickness" Value="0,0,0,1"/>
355
<Setter Property="FontSize" Value="10.5"/><Setter Property="FocusVisualStyle" Value="{x:Null}"/>
356
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="ListViewItem">
357
<Border x:Name="Surface" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}"
358
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
359
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
360
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
361
</Border>
362
<ControlTemplate.Triggers>
363
<Trigger Property="ItemsControl.AlternationIndex" Value="1"><Setter TargetName="Surface" Property="Background" Value="#FAFAFD"/></Trigger>
364
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolControlHoverBrush}"/></Trigger>
365
<Trigger Property="IsSelected" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolAccentSelectedBrush}"/><Setter Property="Foreground" Value="#5555A8"/></Trigger>
366
<Trigger Property="IsKeyboardFocusWithin" Value="True"><Setter TargetName="Surface" Property="BorderBrush" Value="{DynamicResource MainColor}"/></Trigger>
367
<Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0.5"/></Trigger>
368
</ControlTemplate.Triggers>
369
</ControlTemplate></Setter.Value></Setter>
370
</Style>
371
<Style x:Key="ToolBoxListViewStyle" TargetType="ListView">
372
<Setter Property="Padding" Value="0"/><Setter Property="Background" Value="{DynamicResource ToolSurfaceBrush}"/>
373
<Setter Property="Foreground" Value="{DynamicResource ToolTextPrimaryBrush}"/><Setter Property="FontSize" Value="10.5"/>
374
<Setter Property="BorderBrush" Value="{DynamicResource ToolControlBorderBrush}"/><Setter Property="BorderThickness" Value="1"/>
375
<Setter Property="AlternationCount" Value="2"/><Setter Property="HorizontalContentAlignment" Value="Stretch"/>
376
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/><Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
377
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/><Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
378
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/><Setter Property="FocusVisualStyle" Value="{x:Null}"/>
379
<Setter Property="ItemContainerStyle" Value="{StaticResource ToolBoxListViewItemStyle}"/>
380
<Setter Property="controls:ControlAssist.CornerRadius" Value="12"/>
381
</Style>
382
<Style TargetType="GridViewColumnHeader" BasedOn="{StaticResource ToolBoxGridViewColumnHeaderStyle}"/>
383
<Style TargetType="ListViewItem" BasedOn="{StaticResource ToolBoxListViewItemStyle}"/>
384
<Style TargetType="ListView" BasedOn="{StaticResource ToolBoxListViewStyle}"/>
385
318
386
<!-- 数据表格。管理页仅保留语义别名,不再复制模板。 -->
319
387
<Style x:Key="ToolBoxDataGridColumnHeaderStyle" TargetType="DataGridColumnHeader">
320
388
<Setter Property="Height" Value="44"/><Setter Property="Padding" Value="16,0"/>
@@ -322,6 +390,39 @@
322
390
<Setter Property="FontSize" Value="10.5"/><Setter Property="FontWeight" Value="SemiBold"/>
323
391
<Setter Property="BorderBrush" Value="{DynamicResource ToolControlBorderBrush}"/><Setter Property="BorderThickness" Value="0,0,0,1"/>
324
392
<Setter Property="HorizontalContentAlignment" Value="Left"/><Setter Property="VerticalContentAlignment" Value="Center"/>
393
<Setter Property="FocusVisualStyle" Value="{x:Null}"/><Setter Property="Cursor" Value="Arrow"/>
394
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="DataGridColumnHeader">
395
<Grid Background="Transparent">
396
<Border x:Name="Surface" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}"
397
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
398
<Grid>
399
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
400
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
401
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" RecognizesAccessKey="True"/>
402
<Path x:Name="SortIndicator" Grid.Column="1" Width="8" Height="5" Margin="8,0,0,0"
403
Data="M 0,5 L 4,1 L 8,5" Stretch="Fill" Stroke="{DynamicResource MainColor}"
404
StrokeThickness="1.4" StrokeStartLineCap="Round" StrokeEndLineCap="Round"
405
Visibility="Collapsed" RenderTransformOrigin="0.5,0.5">
406
<Path.RenderTransform><RotateTransform Angle="0"/></Path.RenderTransform>
407
</Path>
408
</Grid>
409
</Border>
410
<Thumb x:Name="PART_HeaderGripper" Width="8" HorizontalAlignment="Right" Background="Transparent" Cursor="SizeWE">
411
<Thumb.Template><ControlTemplate TargetType="Thumb">
412
<Grid Background="Transparent"><Border x:Name="Grip" Width="1" HorizontalAlignment="Right" Margin="0,10,1,10"
413
Background="{DynamicResource ToolControlBorderBrush}" Opacity="0.48"/></Grid>
414
<ControlTemplate.Triggers>
415
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Grip" Property="Background" Value="{DynamicResource MainColor}"/><Setter TargetName="Grip" Property="Opacity" Value="0.8"/></Trigger>
416
</ControlTemplate.Triggers>
417
</ControlTemplate></Thumb.Template>
418
</Thumb>
419
</Grid>
420
<ControlTemplate.Triggers>
421
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolControlHoverBrush}"/></Trigger>
422
<Trigger Property="SortDirection" Value="Ascending"><Setter TargetName="SortIndicator" Property="Visibility" Value="Visible"/></Trigger>
423
<Trigger Property="SortDirection" Value="Descending"><Setter TargetName="SortIndicator" Property="Visibility" Value="Visible"/><Setter TargetName="SortIndicator" Property="RenderTransform"><Setter.Value><RotateTransform Angle="180"/></Setter.Value></Setter></Trigger>
424
</ControlTemplate.Triggers>
425
</ControlTemplate></Setter.Value></Setter>
325
426
</Style>
326
427
<Style x:Key="ToolBoxDataGridCenteredHeaderStyle" TargetType="DataGridColumnHeader" BasedOn="{StaticResource ToolBoxDataGridColumnHeaderStyle}">
327
428
<Setter Property="HorizontalContentAlignment" Value="Center"/><Setter Property="Padding" Value="8,0"/>
@@ -333,11 +434,33 @@
333
434
<Setter Property="Padding" Value="16,0"/><Setter Property="Foreground" Value="{DynamicResource ToolTextPrimaryBrush}"/>
334
435
<Setter Property="FontSize" Value="10.5"/><Setter Property="BorderBrush" Value="{DynamicResource ToolDividerBrush}"/>
335
436
<Setter Property="BorderThickness" Value="0,0,0,1"/><Setter Property="HorizontalContentAlignment" Value="Stretch"/><Setter Property="VerticalContentAlignment" Value="Center"/><Setter Property="FocusVisualStyle" Value="{x:Null}"/>
437
<Setter Property="Background" Value="Transparent"/>
438
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="DataGridCell">
439
<Border x:Name="CellSurface" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}"
440
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"
441
SnapsToDevicePixels="True">
442
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
443
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="True"/>
444
</Border>
445
<ControlTemplate.Triggers>
446
<Trigger Property="IsKeyboardFocusWithin" Value="True"><Setter TargetName="CellSurface" Property="BorderBrush" Value="{DynamicResource MainColor}"/></Trigger>
447
<Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0.55"/></Trigger>
448
</ControlTemplate.Triggers>
449
</ControlTemplate></Setter.Value></Setter>
336
450
<Style.Triggers><Trigger Property="IsSelected" Value="True"><Setter Property="Background" Value="Transparent"/><Setter Property="Foreground" Value="{DynamicResource ToolTextPrimaryBrush}"/></Trigger></Style.Triggers>
337
451
</Style>
338
452
<Style x:Key="ToolBoxDataGridRowStyle" TargetType="DataGridRow">
339
<Setter Property="MinHeight" Value="46"/><Setter Property="Background" Value="Transparent"/><Setter Property="BorderThickness" Value="0"/>
453
<Setter Property="MinHeight" Value="48"/><Setter Property="Background" Value="Transparent"/><Setter Property="BorderThickness" Value="0"/>
340
454
<Setter Property="SnapsToDevicePixels" Value="True"/>
455
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="DataGridRow">
456
<Border x:Name="RowSurface" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
457
<Grid>
458
<Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
459
<DataGridCellsPresenter Grid.Row="0" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
460
<DataGridDetailsPresenter Grid.Row="1" Visibility="{TemplateBinding DetailsVisibility}"/>
461
</Grid>
462
</Border>
463
</ControlTemplate></Setter.Value></Setter>
341
464
<Style.Triggers>
342
465
<Trigger Property="AlternationIndex" Value="1"><Setter Property="Background" Value="#FAFAFD"/></Trigger>
343
466
<Trigger Property="IsMouseOver" Value="True"><Setter Property="Background" Value="{DynamicResource ToolControlHoverBrush}"/></Trigger>
@@ -375,6 +498,9 @@
375
498
<Style x:Key="ToolBoxDataGridNumericTextStyle" TargetType="TextBlock" BasedOn="{StaticResource ToolBoxDataGridSecondaryTextStyle}">
376
499
<Setter Property="HorizontalAlignment" Value="Right"/><Setter Property="TextAlignment" Value="Right"/><Setter Property="FontFamily" Value="Cascadia Mono, Consolas"/>
377
500
</Style>
501
<Style TargetType="DataGridColumnHeader" BasedOn="{StaticResource ToolBoxDataGridColumnHeaderStyle}"/>
502
<Style TargetType="DataGridCell" BasedOn="{StaticResource ToolBoxDataGridCellStyle}"/>
503
<Style TargetType="DataGridRow" BasedOn="{StaticResource ToolBoxDataGridRowStyle}"/>
378
504
<Style TargetType="DataGrid" BasedOn="{StaticResource ToolBoxDataGridStyle}"/>
379
505
380
506
<!-- 横向和纵向共用的统一滚动条。 -->
@@ -16,6 +16,20 @@ internal static class ToolProjectRunService
16
16
private const long MaximumExtractedPackageBytes = 128L * 1024 * 1024;
17
17
private static readonly JsonSerializerOptions ManifestJsonOptions = new(JsonSerializerDefaults.Web);
18
18
19
public static async Task<ToolRunResult> BuildAsync(
20
string workspaceRoot,
21
ToolPackageManifest manifest,
22
CancellationToken cancellationToken = default)
23
{
24
return await BuildAndRunCoreAsync(
25
workspaceRoot,
26
manifest,
27
$"{manifest.Name} · 生成验证",
28
temporaryWorkspaceRoot: null,
29
launchAfterBuild: false,
30
cancellationToken);
31
}
32
19
33
public static async Task<ToolRunResult> BuildAndRunAsync(
20
34
string workspaceRoot,
21
35
ToolPackageManifest manifest,
@@ -26,6 +40,7 @@ internal static class ToolProjectRunService
26
40
manifest,
27
41
$"{manifest.Name} · 运行预览",
28
42
temporaryWorkspaceRoot: null,
43
launchAfterBuild: true,
29
44
cancellationToken);
30
45
}
31
46
@@ -57,6 +72,7 @@ internal static class ToolProjectRunService
57
72
manifest,
58
73
manifest.Name,
59
74
packageWorkspaceRoot,
75
launchAfterBuild: true,
60
76
cancellationToken);
61
77
}
62
78
catch (Exception exception)
@@ -71,6 +87,7 @@ internal static class ToolProjectRunService
71
87
ToolPackageManifest manifest,
72
88
string windowTitle,
73
89
string? temporaryWorkspaceRoot,
90
bool launchAfterBuild,
74
91
CancellationToken cancellationToken)
75
92
{
76
93
var runtimeRoot = Path.Combine(Path.GetTempPath(), "XFEToolBox", "CodeStudioRuns", Guid.NewGuid().ToString("N"));
@@ -128,6 +145,14 @@ internal static class ToolProjectRunService
128
145
if (!File.Exists(runtimeAssembly))
129
146
throw new FileNotFoundException("编译成功,但没有找到工具运行程序集。", runtimeAssembly);
130
147
148
if (!launchAfterBuild)
149
{
150
TryDeleteDirectory(runtimeRoot);
151
if (temporaryWorkspaceRoot is not null)
152
TryDeleteDirectory(temporaryWorkspaceRoot);
153
return new ToolRunResult(true, "工具工程已成功生成。", null);
154
}
155
131
156
var runInfo = new ProcessStartInfo("dotnet")
132
157
{
133
158
WorkingDirectory = workspaceRoot,
@@ -317,13 +342,23 @@ internal static class ToolProjectRunService
317
342
layout.Children.Add(captionBar);
318
343
layout.Children.Add(contentSurface);
319
344
320
window.Content = new RoundedClipBorder
345
var windowSurface = new RoundedClipBorder
321
346
{
322
347
Margin = new Thickness(5),
323
348
CornerRadius = new CornerRadius(19),
324
349
Background = (Brush)application.FindResource("MainColor"),
325
350
Child = layout
326
351
};
352
var windowRoot = new Grid();
353
windowRoot.Children.Add(windowSurface);
354
if ({{allowResize}})
355
{
356
windowRoot.Children.Add(new WindowResizeGrip
357
{
358
Margin = new Thickness(0, 0, 5, 5)
359
});
360
}
361
window.Content = windowRoot;
327
362
}
328
363
329
364
private static ImageSource LoadWindowIcon()
@@ -15,7 +15,6 @@ namespace XFEToolBox.Client.ViewModel.Windows;
15
15
16
16
public partial class MainWindowViewModel : ObservableObject
17
17
{
18
private bool isDragMouseDown = false;
19
18
private bool isDoubleClickMouseDown = false;
20
19
private int mouseOriginalX = 0;
21
20
private int mouseOriginalY = 0;
@@ -82,45 +81,6 @@ public partial class MainWindowViewModel : ObservableObject
82
81
mouseOriginalY = mousePosition.Y;
83
82
}
84
83
/// <summary>
85
/// 初始化调整窗体参数
86
/// </summary>
87
public void InitializeToResize()
88
{
89
isDragMouseDown = true;
90
InitMousePosition();
91
_ = Task.Run(Resize);
92
}
93
/// <summary>
94
/// 调整窗体大小
95
/// </summary>
96
public void Resize()
97
{
98
while (isDragMouseDown && InputSimulator.GetMouseDown(MouseButton.Left))
99
{
100
var nowMousePoint = InputSimulator.GetMousePosition();
101
ViewPage.Dispatcher.Invoke(() =>
102
{
103
double newWidth = ViewPage.Width + (nowMousePoint.X - mouseOriginalX) / SystemProfile.CurrentWindowDPIScale;
104
double newHeight = ViewPage.Height + (nowMousePoint.Y - mouseOriginalY) / SystemProfile.CurrentWindowDPIScale;
105
if (newWidth > ViewPage.MinWidth)
106
ViewPage.Width = newWidth;
107
else
108
ViewPage.Width = ViewPage.MinWidth;
109
if (newHeight > ViewPage.MinHeight)
110
ViewPage.Height = newHeight;
111
else
112
ViewPage.Height = ViewPage.MinHeight;
113
});
114
mouseOriginalX = nowMousePoint.X;
115
mouseOriginalY = nowMousePoint.Y;
116
}
117
ViewPage.Dispatcher.Invoke(() =>
118
{
119
SystemProfile.MainWindowWidth = ViewPage.Width;
120
SystemProfile.MainWindowHeight = ViewPage.Height;
121
});
122
}
123
/// <summary>
124
84
/// 判断是否双击
125
85
/// </summary>
126
86
/// <param name="delay">间隔</param>
@@ -0,0 +1,18 @@
1
<UserControl x:Class="XFEToolBox.Client.Views.Controls.WindowResizeGrip"
2
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4
Width="25" Height="25" Focusable="False"
5
HorizontalAlignment="Right" VerticalAlignment="Bottom"
6
Cursor="SizeNWSE">
7
<Grid>
8
<Image x:Name="CornerImage" Width="25" Height="25" Stretch="Fill"
9
FocusVisualStyle="{x:Null}" IsHitTestVisible="False"/>
10
<Thumb DragDelta="ResizeThumb_DragDelta" DragCompleted="ResizeThumb_DragCompleted">
11
<Thumb.Template>
12
<ControlTemplate TargetType="Thumb">
13
<Border Style="{StaticResource CornerBorderStyle}"/>
14
</ControlTemplate>
15
</Thumb.Template>
16
</Thumb>
17
</Grid>
18
</UserControl>
@@ -0,0 +1,55 @@
1
using System.Windows;
2
using System.Windows.Controls;
3
using System.Windows.Controls.Primitives;
4
using System.Windows.Media.Imaging;
5
6
namespace XFEToolBox.Client.Views.Controls;
7
8
/// <summary>
9
/// 与主界面一致的右下角窗口缩放控件。
10
/// </summary>
11
public partial class WindowResizeGrip : UserControl
12
{
13
public WindowResizeGrip()
14
{
15
InitializeComponent();
16
var assemblyName = typeof(WindowResizeGrip).Assembly.GetName().Name;
17
CornerImage.Source = new BitmapImage(new Uri(
18
$"pack://application:,,,/{assemblyName};component/Resources/Image/corner.png",
19
UriKind.Absolute));
20
}
21
22
public event EventHandler? ResizeCompleted;
23
24
private void ResizeThumb_DragDelta(object sender, DragDeltaEventArgs e)
25
{
26
var window = Window.GetWindow(this);
27
if (window is null
28
|| window.WindowState != WindowState.Normal
29
|| window.ResizeMode is ResizeMode.NoResize or ResizeMode.CanMinimize)
30
return;
31
32
var currentWidth = double.IsFinite(window.Width) ? window.Width : window.ActualWidth;
33
var currentHeight = double.IsFinite(window.Height) ? window.Height : window.ActualHeight;
34
window.Width = ClampDimension(
35
currentWidth + e.HorizontalChange,
36
window.MinWidth,
37
window.MaxWidth);
38
window.Height = ClampDimension(
39
currentHeight + e.VerticalChange,
40
window.MinHeight,
41
window.MaxHeight);
42
}
43
44
private void ResizeThumb_DragCompleted(object sender, DragCompletedEventArgs e)
45
{
46
if (Window.GetWindow(this) is { WindowState: WindowState.Normal })
47
ResizeCompleted?.Invoke(this, EventArgs.Empty);
48
}
49
50
private static double ClampDimension(double value, double minimum, double maximum)
51
{
52
var normalizedMaximum = double.IsFinite(maximum) ? maximum : double.MaxValue;
53
return Math.Clamp(value, Math.Max(1, minimum), normalizedMaximum);
54
}
55
}
@@ -167,11 +167,7 @@
167
167
</Grid>
168
168
</controls:RoundedClipBorder>
169
169
170
<!-- 拖拽角素材绘制到圆角裁剪层之外,避免其贴边像素被截断。 -->
171
<Image Source="/Resources/Image/corner.png" Width="25" Height="25" Margin="0,0,5,5"
172
HorizontalAlignment="Right" VerticalAlignment="Bottom" FocusVisualStyle="{x:Null}"
173
IsHitTestVisible="False"/>
174
<Border x:Name="cornerBorder" Style="{StaticResource CornerBorderStyle}" Margin="0,0,5,5"
175
MouseDown="CornerBorder_MouseDown"/>
170
<!-- 与工具运行窗口复用同一个白色圆角拖动控件。 -->
171
<controls:WindowResizeGrip Margin="0,0,5,5" ResizeCompleted="WindowResizeGrip_ResizeCompleted"/>
176
172
</Grid>
177
173
</Window>
@@ -52,7 +52,11 @@ public partial class MainWindow : Window
52
52
storyboard.Begin();
53
53
}
54
54
55
private void CornerBorder_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) => ViewModel.InitializeToResize();
55
private void WindowResizeGrip_ResizeCompleted(object? sender, EventArgs e)
56
{
57
SystemProfile.MainWindowWidth = Width;
58
SystemProfile.MainWindowHeight = Height;
59
}
56
60
57
61
private void BackTabBorder_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) => NavigationCenter.GoBack();
58
62
@@ -304,14 +304,15 @@
304
304
<MenuItem Header="文件" Style="{StaticResource EditorTopMenuItem}"
305
305
ItemContainerStyle="{StaticResource EditorTopMenuSubItem}">
306
306
<MenuItem Header="新建工程" InputGestureText="Ctrl+Shift+N" Click="NewProjectButton_Click"/>
307
<MenuItem Header="打开工程" InputGestureText="Ctrl+O" Click="OpenFolderButton_Click"/>
308
<MenuItem Header="新建项" InputGestureText="Ctrl+N" Click="NewFileButton_Click"/>
307
<MenuItem Header="打开工程" InputGestureText="Ctrl+Shift+O" Click="OpenFolderButton_Click"/>
308
<MenuItem Header="新建项" InputGestureText="Ctrl+Shift+A" Click="NewFileButton_Click"/>
309
309
<Separator Style="{StaticResource EditorMenuSeparator}"/>
310
<MenuItem x:Name="SaveButton" Header="保存全部" InputGestureText="Ctrl+S" Click="SaveButton_Click"/>
310
<MenuItem x:Name="SaveButton" Header="保存全部" InputGestureText="Ctrl+Shift+S" Click="SaveButton_Click"/>
311
311
</MenuItem>
312
312
<MenuItem Header="代码" Style="{StaticResource EditorTopMenuItem}"
313
313
ItemContainerStyle="{StaticResource EditorTopMenuSubItem}">
314
<MenuItem Header="格式化当前文件" InputGestureText="Shift+Alt+F" Click="FormatButton_Click"/>
314
<MenuItem Header="格式化文档" InputGestureText="Ctrl+K, Ctrl+D" Click="FormatButton_Click"/>
315
<MenuItem Header="生成工程" InputGestureText="Ctrl+Shift+B" Click="BuildProjectButton_Click"/>
315
316
</MenuItem>
316
317
<MenuItem Header="工具包" Style="{StaticResource EditorTopMenuItem}"
317
318
ItemContainerStyle="{StaticResource EditorTopMenuSubItem}">
@@ -320,10 +321,10 @@
320
321
</MenuItem>
321
322
</Menu>
322
323
<StackPanel Grid.Column="1" Orientation="Horizontal">
323
<Button x:Name="PreviewButton" Margin="0,0,8,0" Click="PreviewButton_Click" ToolTip="预览当前 Markdown、XAML 或文本文件(Ctrl+F5)">
324
<Button x:Name="PreviewButton" Margin="0,0,8,0" Click="PreviewButton_Click" ToolTip="预览当前 Markdown、XAML 或文本文件(Ctrl+Shift+W)">
324
325
<StackPanel Orientation="Horizontal"><TextBlock Text="◫" FontSize="15" Margin="0,0,6,0"/><TextBlock Text="预览" VerticalAlignment="Center"/></StackPanel>
325
326
</Button>
326
<Button x:Name="RunButton" controls:ButtonAssist.IsPrimary="True" Margin="0,0,8,0" Click="RunButton_Click" ToolTip="完整编译并运行工具(F5)">
327
<Button x:Name="RunButton" controls:ButtonAssist.IsPrimary="True" Margin="0,0,8,0" Click="RunButton_Click" ToolTip="完整编译并运行工具(F5 / Ctrl+F5)">
327
328
<StackPanel Orientation="Horizontal"><TextBlock Text="▶" FontSize="12" Margin="0,0,6,0"/><TextBlock Text="运行" VerticalAlignment="Center"/></StackPanel>
328
329
</Button>
329
330
<Button x:Name="EditorThemeButton" Content="◐" Width="38" MinWidth="38" Padding="0" Margin="0,0,8,0" ToolTip="切换代码区明暗主题" Click="ToggleThemeButton_Click"/>
@@ -492,7 +493,7 @@
492
493
</ListBox.ItemTemplate>
493
494
</ListBox>
494
495
<StackPanel x:Name="EmptyExplorer" HorizontalAlignment="Center" VerticalAlignment="Center" IsHitTestVisible="False">
495
<TextBlock Text="暂无可编辑文件" Foreground="#88889A" FontSize="10.5" HorizontalAlignment="Center"/>
496
<TextBlock Text="暂无工程文件" Foreground="#88889A" FontSize="10.5" HorizontalAlignment="Center"/>
496
497
</StackPanel>
497
498
</Grid>
498
499
@@ -15,6 +15,7 @@ using System.Windows.Data;
15
15
using System.Windows.Input;
16
16
using System.Windows.Markup;
17
17
using System.Windows.Media;
18
using System.Windows.Media.Imaging;
18
19
using System.Windows.Threading;
19
20
using System.Xml.Linq;
20
21
using Ganss.Xss;
@@ -55,6 +56,11 @@ public partial class ToolCodeEditorWindow : Window
55
56
".yml", ".yaml", ".config", ".props", ".targets"
56
57
};
57
58
59
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
60
{
61
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico"
62
};
63
58
64
private static readonly HashSet<string> CSharpKeywords = new(StringComparer.Ordinal)
59
65
{
60
66
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class",
@@ -235,14 +241,17 @@ public partial class ToolCodeEditorWindow : Window
235
241
return;
236
242
237
243
var files = new List<EditorFileContent>();
238
foreach (var item in _files)
244
foreach (var item in _files.Where(item => item.IsEditable))
239
245
{
240
246
var fullPath = GetSafeFullPath(item.RelativePath);
241
247
files.Add(new EditorFileContent(item.RelativePath, await File.ReadAllTextAsync(fullPath)));
242
248
}
243
249
250
var requestedActivePath = activePath is not null && EditableExtensions.Contains(Path.GetExtension(activePath))
251
? activePath
252
: null;
244
253
var payload = JsonSerializer.Serialize(
245
new { files, activePath = activePath ?? files.FirstOrDefault()?.Path }, JsonOptions);
254
new { files, activePath = requestedActivePath ?? files.FirstOrDefault()?.Path }, JsonOptions);
246
255
await EditorWebView.ExecuteScriptAsync($"window.editorHost.loadWorkspace({payload})");
247
256
}
248
257
@@ -266,7 +275,7 @@ public partial class ToolCodeEditorWindow : Window
266
275
267
276
var paths = Directory.EnumerateFiles(_workspaceRoot, "*", SearchOption.AllDirectories)
268
277
.Where(path => !IsBuildDirectory(path))
269
.Where(path => EditableExtensions.Contains(Path.GetExtension(path)))
278
.Where(IsExplorerVisibleFile)
270
279
.Select(path => Path.GetRelativePath(_workspaceRoot, path).Replace('\\', '/'))
271
280
.OrderBy(path => path.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
272
281
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase);
@@ -338,6 +347,12 @@ public partial class ToolCodeEditorWindow : Window
338
347
private static string GetParentPath(string relativePath) =>
339
348
Path.GetDirectoryName(relativePath)?.Replace('\\', '/') ?? string.Empty;
340
349
350
private static bool IsExplorerVisibleFile(string path)
351
{
352
var extension = Path.GetExtension(path);
353
return EditableExtensions.Contains(extension) || ImageExtensions.Contains(extension);
354
}
355
341
356
private static string? TryGetXamlPathForCodeBehind(string relativePath) =>
342
357
relativePath.EndsWith(".xaml.cs", StringComparison.OrdinalIgnoreCase)
343
358
? relativePath[..^3]
@@ -604,7 +619,8 @@ public partial class ToolCodeEditorWindow : Window
604
619
if (file.IsFolder)
605
620
{
606
621
var removedPaths = _files
607
.Where(item => item.RelativePath.StartsWith(file.RelativePath.TrimEnd('/') + "/", StringComparison.OrdinalIgnoreCase))
622
.Where(item => item.IsEditable
623
&& item.RelativePath.StartsWith(file.RelativePath.TrimEnd('/') + "/", StringComparison.OrdinalIgnoreCase))
608
624
.Select(item => item.RelativePath)
609
625
.ToArray();
610
626
Directory.Delete(fullPath, recursive: true);
@@ -617,7 +633,7 @@ public partial class ToolCodeEditorWindow : Window
617
633
else
618
634
{
619
635
File.Delete(fullPath);
620
if (_editorReady)
636
if (_editorReady && file.IsEditable)
621
637
await EditorWebView.ExecuteScriptAsync($"window.editorHost.removeFile({JsonSerializer.Serialize(file.RelativePath)})");
622
638
}
623
639
@@ -767,6 +783,12 @@ public partial class ToolCodeEditorWindow : Window
767
783
return;
768
784
}
769
785
786
if (previewPath is not null && ImageExtensions.Contains(Path.GetExtension(previewPath)))
787
{
788
await ShowImagePreviewAsync(previewPath);
789
return;
790
}
791
770
792
if (previewPath is not null && Path.GetExtension(previewPath).Equals(".xaml", StringComparison.OrdinalIgnoreCase))
771
793
{
772
794
var xaml = await ReadEditorOrDiskFileAsync(previewPath);
@@ -886,6 +908,73 @@ public partial class ToolCodeEditorWindow : Window
886
908
private static bool IsPlainTextPreviewPath(string path) =>
887
909
Path.GetExtension(path).ToLowerInvariant() is ".json" or ".xml" or ".resx" or ".txt" or ".yml" or ".yaml" or ".config" or ".props" or ".targets";
888
910
911
private async Task ShowImagePreviewAsync(string relativePath, bool openAsDocument = false)
912
{
913
if (_lastSurfacePath?.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) == true)
914
await FlushManifestDesignerAsync();
915
916
var fullPath = GetSafeFullPath(relativePath);
917
if (!File.Exists(fullPath))
918
throw new FileNotFoundException($"图像文件不存在:{relativePath}", fullPath);
919
920
var bitmap = new BitmapImage();
921
await using (var stream = new FileStream(
922
fullPath,
923
FileMode.Open,
924
FileAccess.Read,
925
FileShare.ReadWrite | FileShare.Delete,
926
81920,
927
FileOptions.Asynchronous | FileOptions.SequentialScan))
928
{
929
bitmap.BeginInit();
930
bitmap.CacheOption = BitmapCacheOption.OnLoad;
931
bitmap.StreamSource = stream;
932
bitmap.EndInit();
933
}
934
bitmap.Freeze();
935
936
_activePath = relativePath;
937
_lastSurfacePath = relativePath;
938
_manifestSourceMode = false;
939
ManifestDesignerPanel.Visibility = Visibility.Collapsed;
940
ManifestViewToggleButton.Visibility = Visibility.Collapsed;
941
UpdateXamlRelatedFileButton(relativePath);
942
ActiveFileText.Text = relativePath;
943
944
ShowWpfPreviewSurface();
945
PreviewErrorPanel.Visibility = Visibility.Collapsed;
946
PreviewTitleText.Text = $"{relativePath} · 图像预览";
947
PreviewSubtitleText.Text = $"{bitmap.PixelWidth} × {bitmap.PixelHeight} · {Path.GetExtension(relativePath).TrimStart('.').ToUpperInvariant()}";
948
var previewScale = Math.Min(1d, Math.Min(360d / bitmap.PixelWidth, 360d / bitmap.PixelHeight));
949
var image = new Image
950
{
951
Source = bitmap,
952
Stretch = Stretch.Uniform,
953
Width = Math.Max(1, bitmap.PixelWidth * previewScale),
954
Height = Math.Max(1, bitmap.PixelHeight * previewScale),
955
HorizontalAlignment = HorizontalAlignment.Center,
956
VerticalAlignment = VerticalAlignment.Center,
957
SnapsToDevicePixels = true
958
};
959
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
960
PreviewContent.Content = image;
961
962
if (openAsDocument)
963
{
964
_previewDocumentTabOpen = true;
965
PreviewDocumentTabButton.Content = $"预览 · {Path.GetFileName(relativePath)}";
966
PreviewDocumentTabStrip.Visibility = Visibility.Visible;
967
ShowPreviewDocumentSurface();
968
}
969
else
970
{
971
ShowPreviewPanel();
972
}
973
974
UpdateEditorWebViewVisibility();
975
SetHostStatus($"已预览图像 {relativePath}");
976
}
977
889
978
private static HtmlSanitizer CreateMarkdownSanitizer()
890
979
{
891
980
var sanitizer = new HtmlSanitizer();
@@ -995,6 +1084,39 @@ public partial class ToolCodeEditorWindow : Window
995
1084
}
996
1085
}
997
1086
1087
private async void BuildProjectButton_Click(object sender, RoutedEventArgs e)
1088
{
1089
if (!_editorReady)
1090
{
1091
SetHostStatus("编辑器仍在初始化");
1092
return;
1093
}
1094
1095
RunButton.IsEnabled = false;
1096
try
1097
{
1098
SetHostStatus("正在保存并生成工具工程…");
1099
var manifest = await SaveAndReadManifestAsync();
1100
var result = await ToolProjectRunService.BuildAsync(_workspaceRoot, manifest);
1101
if (!result.Success)
1102
{
1103
SetHostStatus("工具生成失败");
1104
await ShowAlertAsync("生成工具失败", result.Message);
1105
return;
1106
}
1107
1108
SetHostStatus("工具工程生成成功 · 0 个错误");
1109
}
1110
catch (Exception exception)
1111
{
1112
await ReportOperationFailureAsync("生成工具失败", exception);
1113
}
1114
finally
1115
{
1116
RunButton.IsEnabled = true;
1117
}
1118
}
1119
998
1120
private async Task<ToolPackageManifest> SaveAndReadManifestAsync()
999
1121
{
1000
1122
await SaveFilesAsync(await GetEditorFilesAsync());
@@ -1179,6 +1301,11 @@ public partial class ToolCodeEditorWindow : Window
1179
1301
try
1180
1302
{
1181
1303
ActiveFileText.Text = file.RelativePath;
1304
if (file.IsImage)
1305
{
1306
await ShowImagePreviewAsync(file.RelativePath);
1307
return;
1308
}
1182
1309
await EditorWebView.ExecuteScriptAsync(
1183
1310
$"window.editorHost.activateFile({JsonSerializer.Serialize(file.RelativePath)})");
1184
1311
}
@@ -1634,13 +1761,28 @@ public partial class ToolCodeEditorWindow : Window
1634
1761
FileList.SelectedItem = item;
1635
1762
}
1636
1763
1637
private void ExplorerItem_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
1764
private async void ExplorerItem_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
1638
1765
{
1639
if (e.ClickCount != 2 || sender is not FrameworkElement { DataContext: EditorExplorerItem { IsFolder: true } folder })
1766
if (e.ClickCount != 2 || sender is not FrameworkElement { DataContext: EditorExplorerItem item })
1640
1767
return;
1641
1768
1642
folder.IsExpanded = !folder.IsExpanded;
1643
RefreshExplorerView();
1769
if (item.IsFolder)
1770
{
1771
item.IsExpanded = !item.IsExpanded;
1772
RefreshExplorerView();
1773
}
1774
else if (item.IsImage)
1775
{
1776
SelectExplorerItem(item.RelativePath);
1777
try
1778
{
1779
await ShowImagePreviewAsync(item.RelativePath, openAsDocument: true);
1780
}
1781
catch (Exception exception)
1782
{
1783
await ReportOperationFailureAsync("打开图像失败", exception);
1784
}
1785
}
1644
1786
e.Handled = true;
1645
1787
}
1646
1788
@@ -1669,6 +1811,11 @@ public partial class ToolCodeEditorWindow : Window
1669
1811
try
1670
1812
{
1671
1813
ActiveFileText.Text = item.RelativePath;
1814
if (item.IsImage)
1815
{
1816
await ShowImagePreviewAsync(item.RelativePath, openAsDocument: true);
1817
return;
1818
}
1672
1819
await EditorWebView.ExecuteScriptAsync(
1673
1820
$"window.editorHost.activateFile({JsonSerializer.Serialize(item.RelativePath)})");
1674
1821
}
@@ -1791,8 +1938,8 @@ public partial class ToolCodeEditorWindow : Window
1791
1938
return "请输入新名称。";
1792
1939
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || name.Contains('/') || name.Contains('\\'))
1793
1940
return "名称包含无效字符。";
1794
if (!item.IsFolder && !EditableExtensions.Contains(Path.GetExtension(name)))
1795
return "重命名后的文件类型不受编辑器支持。";
1941
if (!item.IsFolder && !IsExplorerVisibleFile(name))
1942
return "重命名后的文件类型不受工程资源管理器支持。";
1796
1943
1797
1944
var parent = Path.GetDirectoryName(item.RelativePath)?.Replace('\\', '/');
1798
1945
var destination = string.IsNullOrWhiteSpace(parent) ? name : $"{parent}/{name}";
@@ -1937,6 +2084,8 @@ public partial class ToolCodeEditorWindow : Window
1937
2084
await ToolProjectWorkspaceService.SaveExplorerOrderAsync(_workspaceRoot, _explorerOrder);
1938
2085
await ReloadFileListAsync(destinationPath);
1939
2086
await SendWorkspaceAsync(source.IsFolder ? null : destinationPath);
2087
if (!source.IsFolder && ImageExtensions.Contains(Path.GetExtension(destinationPath)))
2088
await ShowImagePreviewAsync(destinationPath);
1940
2089
SetHostStatus($"已移动到 {destinationPath}");
1941
2090
}
1942
2091
catch (Exception exception)
@@ -1950,21 +2099,33 @@ public partial class ToolCodeEditorWindow : Window
1950
2099
var manifestPath = Path.Combine(_workspaceRoot, "manifest.json");
1951
2100
if (!File.Exists(manifestPath))
1952
2101
return;
1953
var root = JsonNode.Parse(await File.ReadAllTextAsync(manifestPath));
1954
if (root?["entry"] is not JsonObject entry)
2102
if (JsonNode.Parse(await File.ReadAllTextAsync(manifestPath)) is not JsonObject root)
1955
2103
return;
1956
2104
1957
2105
var changed = false;
1958
foreach (var key in new[] { "viewXaml", "viewCodeBehind", "viewModel" })
2106
if (root["icon"]?.GetValue<string>() is { } iconPath)
1959
2107
{
1960
var value = entry[key]?.GetValue<string>();
1961
if (value is null)
1962
continue;
1963
var replacement = ReplaceMovedPath(value.Replace('\\', '/'), sourcePath, destinationPath, isFolder);
1964
if (replacement.Equals(value, StringComparison.Ordinal))
1965
continue;
1966
entry[key] = replacement;
1967
changed = true;
2108
var replacement = ReplaceMovedPath(iconPath.Replace('\\', '/'), sourcePath, destinationPath, isFolder);
2109
if (!replacement.Equals(iconPath, StringComparison.Ordinal))
2110
{
2111
root["icon"] = replacement;
2112
changed = true;
2113
}
2114
}
2115
2116
if (root["entry"] is JsonObject entry)
2117
{
2118
foreach (var key in new[] { "viewXaml", "viewCodeBehind", "viewModel" })
2119
{
2120
var value = entry[key]?.GetValue<string>();
2121
if (value is null)
2122
continue;
2123
var replacement = ReplaceMovedPath(value.Replace('\\', '/'), sourcePath, destinationPath, isFolder);
2124
if (replacement.Equals(value, StringComparison.Ordinal))
2125
continue;
2126
entry[key] = replacement;
2127
changed = true;
2128
}
1968
2129
}
1969
2130
1970
2131
if (changed)
@@ -2062,6 +2223,8 @@ public partial class ToolCodeEditorWindow : Window
2062
2223
_dirty = false;
2063
2224
await ReloadFileListAsync(selectedPath);
2064
2225
await SendWorkspaceAsync(activePath);
2226
if (activePath is not null && ImageExtensions.Contains(Path.GetExtension(activePath)))
2227
await ShowImagePreviewAsync(activePath);
2065
2228
SetHostStatus("文件列表已刷新");
2066
2229
}
2067
2230
catch (Exception exception)
@@ -2124,6 +2287,9 @@ public partial class ToolCodeEditorWindow : Window
2124
2287
case "format":
2125
2288
FormatButton_Click(this, new RoutedEventArgs());
2126
2289
break;
2290
case "build":
2291
BuildProjectButton_Click(this, new RoutedEventArgs());
2292
break;
2127
2293
case "export":
2128
2294
PackageButton_Click(this, new RoutedEventArgs());
2129
2295
break;
@@ -2433,15 +2599,17 @@ public partial class ToolCodeEditorWindow : Window
2433
2599
}
2434
2600
else if (e.Key == Key.F5 && modifiers == ModifierKeys.Control)
2435
2601
{
2436
PreviewButton_Click(this, new RoutedEventArgs());
2602
RunButton_Click(this, new RoutedEventArgs());
2437
2603
e.Handled = true;
2438
2604
}
2439
else if (modifiers == ModifierKeys.Control && e.Key == Key.S)
2605
else if ((modifiers == ModifierKeys.Control
2606
|| modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
2607
&& e.Key == Key.S)
2440
2608
{
2441
2609
SaveButton_Click(this, new RoutedEventArgs());
2442
2610
e.Handled = true;
2443
2611
}
2444
else if (modifiers == ModifierKeys.Control && e.Key == Key.O)
2612
else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.O)
2445
2613
{
2446
2614
OpenFolderButton_Click(this, new RoutedEventArgs());
2447
2615
e.Handled = true;
@@ -2456,15 +2624,24 @@ public partial class ToolCodeEditorWindow : Window
2456
2624
NewProjectButton_Click(this, new RoutedEventArgs());
2457
2625
e.Handled = true;
2458
2626
}
2459
else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.E)
2627
else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.A)
2460
2628
{
2461
PackageButton_Click(this, new RoutedEventArgs());
2629
NewFileButton_Click(this, new RoutedEventArgs());
2630
e.Handled = true;
2631
}
2632
else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.B)
2633
{
2634
BuildProjectButton_Click(this, new RoutedEventArgs());
2462
2635
e.Handled = true;
2463
2636
}
2464
else if (modifiers == (ModifierKeys.Shift | ModifierKeys.Alt)
2465
&& (e.Key == Key.F || e.SystemKey == Key.F))
2637
else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.W)
2466
2638
{
2467
FormatButton_Click(this, new RoutedEventArgs());
2639
PreviewButton_Click(this, new RoutedEventArgs());
2640
e.Handled = true;
2641
}
2642
else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.E)
2643
{
2644
PackageButton_Click(this, new RoutedEventArgs());
2468
2645
e.Handled = true;
2469
2646
}
2470
2647
}
@@ -2574,6 +2751,8 @@ public partial class ToolCodeEditorWindow : Window
2574
2751
".md" or ".markdown" or ".mdown" or ".mkd" or ".mkdn" =>
2575
2752
new(path, false, "M↓", "#F0F0FA", "#6868A6"),
2576
2753
".yml" or ".yaml" => new(path, false, "Y", "#FFF0F3", "#B96274"),
2754
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" =>
2755
new(path, false, "▧", "#EAF8FC", "#4D8FA6"),
2577
2756
_ => new(path, false, "·", "#F1F1F6", "#77778B")
2578
2757
};
2579
2758
@@ -2591,6 +2770,8 @@ public partial class ToolCodeEditorWindow : Window
2591
2770
2592
2771
public string RelativePath { get; } = relativePath;
2593
2772
public bool IsFolder { get; } = isFolder;
2773
public bool IsEditable => !IsFolder && EditableExtensions.Contains(Path.GetExtension(RelativePath));
2774
public bool IsImage => !IsFolder && ImageExtensions.Contains(Path.GetExtension(RelativePath));
2594
2775
public string Icon { get; } = icon;
2595
2776
public string IconBackground { get; } = iconBackground;
2596
2777
public string IconForeground { get; } = iconForeground;
@@ -7,7 +7,7 @@
7
7
<ImplicitUsings>enable</ImplicitUsings>
8
8
<UseWPF>true</UseWPF>
9
9
<ApplicationIcon>Resources\Icon\XFEToolBoxIcon.ico</ApplicationIcon>
10
<Version>0.2.0</Version>
10
<Version>1.0.0</Version>
11
11
<AssemblyTitle>XFEToolBox</AssemblyTitle>
12
12
<AssemblyName>XFEToolBox</AssemblyName>
13
13
</PropertyGroup>