XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

支持图片资源、统一缩放控件、对齐VS快捷键

工程资源管理器支持图片文件预览、拖拽、重命名等操作,新增高质量图片缩放预览。主窗口与工具运行窗口统一使用 WindowResizeGrip 控件,提升缩放体验和一致性。代码编辑器快捷键全面对齐 Visual Studio,优化菜单项提示。新增工程生成(Build)功能,支持仅生成不运行。优化资源管理器操作权限和空状态文案,修复快捷键描述不一致等问题,项目版本号提升至 1.0.0。整体提升编辑器易用性和专业性。

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

代码差异

11 个文件 +581 -100
Modified XFEToolBox/Resources/Editor/editor.html +115 -10
@@ -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';
Modified XFEToolBox/Resources/Style/StandardControlsStyle.xaml +130 -4
@@ -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 <!-- 横向和纵向共用的统一滚动条。 -->
Modified XFEToolBox/Utilities/ToolProjectRunService.cs +36 -1
@@ -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()
Modified XFEToolBox/ViewModel/Windows/MainWindowViewModel.cs +0 -40
@@ -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>
Added XFEToolBox/Views/Controls/WindowResizeGrip.xaml +18 -0
@@ -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>
Added XFEToolBox/Views/Controls/WindowResizeGrip.xaml.cs +55 -0
@@ -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 }
Modified XFEToolBox/Views/Windows/MainWindow.xaml +2 -6
@@ -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>
Modified XFEToolBox/Views/Windows/MainWindow.xaml.cs +5 -1
@@ -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
Modified XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml +8 -7
@@ -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
Modified XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml.cs +211 -30
@@ -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;
Modified XFEToolBox/XFEToolBox.Client.csproj +1 -1
@@ -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>