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

XFEWorkflow

【WinUI】XFE基于windows的工作流

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

XFEstudio/XFEWorkflow

完善工作流列表编辑入口与编辑器标签整体布局

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

代码差异

14 个文件 +204 -34
Modified XFEWorkflow/XFEWorkflow/Services/UiSmokeTest.Documents.cs +83 -20
@@ -2,6 +2,10 @@ using XFEWorkflow.Abstractions;
2 2 using XFEWorkflow.Core.Runtime;
3 3 using XFEWorkflow.ViewModels;
4 4 using XFEWorkflow.Views;
5 using Microsoft.UI.Xaml.Automation;
6 using Microsoft.UI.Xaml.Automation.Peers;
7 using Microsoft.UI.Xaml.Automation.Provider;
8 using Microsoft.UI.Xaml.Media;
5 9
6 10 namespace XFEWorkflow.Services;
7 11
@@ -10,28 +14,87 @@ internal static partial class UiSmokeTest
10 14 private static async Task SmokeWorkflowDocumentsAsync(string output)
11 15 {
12 16 var services = AppServices.Current; var navigation = AppShellPage.Current!.ViewModel.NavigationViewService;
17 var root = (FrameworkElement)App.MainWindow.Content; var originalSize = App.MainWindow.AppWindow.Size; var originalTheme = root.RequestedTheme;
18 var paletteSize = services.State.PaneSize("workflow.palette", 260);
19 var scale = root.XamlRoot.RasterizationScale; App.MainWindow.AppWindow.Resize(new((int)(1600 * scale), (int)(1000 * scale)));
13 20 var first = WorkflowTemplates.Welcome(); first.Name = "标签页 A"; first.Enabled = false; first.Triggers.Add(new() { Kind = TriggerKind.Interval, Enabled = true, IntervalSeconds = 3600 });
14 21 var second = WorkflowTemplates.Welcome(); second.Name = "标签页 B"; second.Enabled = false;
15 22 await services.Workflows.SaveAsync(first); await services.Workflows.SaveAsync(second);
16 navigation.NavigateTo<WorkflowListPage>(); await WaitManagementAsync(() => Descendants<Frame>(App.MainWindow.Content).Any(x => x.Content is WorkflowListPage));
17 await Task.Delay(300); await CaptureWindowAsync(Path.Combine(output, "workflow-list.png"));
18 navigation.NavigateTo<WorkflowEditorPage>(new WorkflowNavigationTarget(first.Id));
19 await WaitManagementAsync(() => WorkflowEditorPage.Current?.ActiveEditor?.ViewModel.SelectedWorkflow?.Id == first.Id);
20 var hub = WorkflowEditorPage.Current!; var a = hub.ActiveEditor!; a.ViewModel.SelectedWorkflow!.Description = "A 独立修改";
21 var b = await hub.OpenAsync(second.Id); b.ViewModel.SelectedWorkflow!.Description = "B 独立修改";
22 await hub.OpenAsync(first.Id);
23 if (!ReferenceEquals(hub.ActiveEditor, a) || hub.Documents.TabItems.Count != 2 || a.ViewModel.SelectedWorkflow!.Description != "A 独立修改") throw new InvalidOperationException("多标签页未保留独立编辑状态或重复打开");
24 if (Descendants<CommandBar>(a).Any(x => x.Visibility == Visibility.Visible) || Descendants<Expander>(a).Any(x => x.Name == "WorkflowLibraryExpander" && x.Visibility == Visibility.Visible)) throw new InvalidOperationException("编辑器仍展示工作流列表和旧溢出工具栏");
25 if (!Descendants<MenuBar>(hub).Single().Items.Select(x => x.Title).SequenceEqual(new[] { "文件", "编辑", "工作流", "视图", "工具" })) throw new InvalidOperationException("顶部菜单不完整");
26 if (!a.ViewModel.PaletteGroups.Any(x => x.Name == "鼠标操作" && x.Glyph == "\uE962") || !a.ViewModel.PaletteGroups.Any(x => x.Name == "键盘操作" && x.Glyph == "\uE765")) throw new InvalidOperationException("鼠标或键盘分类图标错误");
27 await services.Workflows.SetActivationAsync(first.Id, true, 9); await hub.SaveAllAsync();
28 if ((await services.Workflows.GetAsync(first.Id)).Enabled != true) throw new InvalidOperationException("编辑器保存覆盖了列表的启停状态");
29 await CaptureWindowAsync(Path.Combine(output, "workflow-editor-multiple-tabs-menus.png"));
30 navigation.NavigateTo<WorkflowListPage>(); await Task.Delay(300);
31 var frame = Descendants<Frame>(App.MainWindow.Content).Single(x => x.Content is WorkflowListPage); var list = (WorkflowListPage)frame.Content;
32 await list.SetEnabledAsync(first.Id, false);
33 if (services.Scheduler.StatusFor(await services.Workflows.GetAsync(first.Id)).Any(x => x.Enabled)) throw new InvalidOperationException("列表关闭后仍在调度");
34 navigation.NavigateTo<WorkflowEditorPage>(); await Task.Delay(150); if (hub.Documents.TabItems.Count != 2) throw new InvalidOperationException("离开编辑器后标签页丢失");
35 await WorkflowEditorPage.CloseOpenAsync(first.Id); await WorkflowEditorPage.CloseOpenAsync(second.Id); navigation.NavigateTo<MainPage>();
23 try
24 {
25 navigation.NavigateTo<WorkflowListPage>(); await WaitManagementAsync(() => Descendants<Frame>(App.MainWindow.Content).Any(x => x.Content is WorkflowListPage));
26 await WaitManagementAsync(() => Descendants<Button>(App.MainWindow.Content).Any(x => AutomationProperties.GetAutomationId(x) == "EditWorkflow-" + first.Id));
27 var firstRow = Descendants<Expander>(App.MainWindow.Content).Single(x => x.Tag as string == first.Id); firstRow.IsExpanded = false;
28 await Task.Delay(300); await CaptureWindowAsync(Path.Combine(output, "workflow-list.png"));
29 await InvokeButtonAsync(Descendants<Button>(firstRow).Single(x => AutomationProperties.GetAutomationId(x) == "EditWorkflow-" + first.Id));
30 await WaitManagementAsync(() => WorkflowEditorPage.Current?.ActiveEditor?.ViewModel.SelectedWorkflow?.Id == first.Id);
31 if (firstRow.IsExpanded) throw new InvalidOperationException("列表右侧编辑按钮不应展开工作流卡片");
32 var editorNavigation = Descendants<NavigationViewItem>(AppShellPage.Current!).Single(x => x.Content as string == "工作流编辑器");
33 if (editorNavigation.Icon is not FontIcon { Glyph: "\uE943" }) throw new InvalidOperationException("工作流编辑器入口没有使用代码图标");
34 var hub = WorkflowEditorPage.Current!; var a = hub.ActiveEditor!; a.ViewModel.SelectedWorkflow!.Description = "A 独立修改";
35 var b = await hub.OpenAsync(second.Id); b.ViewModel.SelectedWorkflow!.Description = "B 独立修改";
36 await hub.OpenAsync(first.Id);
37 if (!ReferenceEquals(hub.ActiveEditor, a) || hub.Documents.TabItems.Count != 2 || a.ViewModel.SelectedWorkflow!.Description != "A 独立修改") throw new InvalidOperationException("多标签页未保留独立编辑状态或重复打开");
38 if (Descendants<CommandBar>(a).Any(x => x.Visibility == Visibility.Visible) || Descendants<Expander>(a).Any(x => x.Name == "WorkflowLibraryExpander" && x.Visibility == Visibility.Visible)) throw new InvalidOperationException("编辑器仍展示工作流列表和旧溢出工具栏");
39 if (!Descendants<MenuBar>(hub).Single().Items.Select(x => x.Title).SequenceEqual(new[] { "文件", "编辑", "工作流", "视图", "工具" })) throw new InvalidOperationException("顶部菜单不完整");
40 if (!a.ViewModel.PaletteGroups.Any(x => x.Name == "鼠标操作" && x.Glyph == "\uE962") || !a.ViewModel.PaletteGroups.Any(x => x.Name == "键盘操作" && x.Glyph == "\uE765")) throw new InvalidOperationException("鼠标或键盘分类图标错误");
41 var dockingEvidence = new List<object>(); var colors = new Dictionary<ElementTheme, Windows.UI.Color>();
42 async Task VerifyDockingAsync(string stage)
43 {
44 await Task.Delay(200);
45 var palette = Descendants<Border>(hub).Single(x => x.Name == "PalettePanel");
46 var header = Descendants<Grid>(hub.ActiveEditor!).Single(x => x.Name == "EditorModeHeader");
47 var selected = (TabViewItem)hub.Documents.SelectedItem;
48 var stripOrigin = hub.Documents.TransformToVisual(hub).TransformPoint(new(0, 0));
49 var headerOrigin = header.TransformToVisual(hub).TransformPoint(new(0, 0));
50 var tabBottom = selected.TransformToVisual(hub).TransformPoint(new(0, selected.ActualHeight)).Y;
51 var paletteOrigin = palette.TransformToVisual(hub).TransformPoint(new(0, 0));
52 if (Math.Abs(stripOrigin.X - headerOrigin.X) > 1 || Math.Abs(tabBottom - headerOrigin.Y) > 1 || stripOrigin.X - paletteOrigin.X < 200 || Math.Abs(paletteOrigin.Y - stripOrigin.Y) > 2)
53 throw new InvalidOperationException($"工作流标签与编辑区没有对齐或存在间隙:{stage} · strip={stripOrigin} · header={headerOrigin} · tabBottom={tabBottom}");
54 var selectedPath = Descendants<Microsoft.UI.Xaml.Shapes.Path>(selected).Single(x => x.Name == "SelectedBackgroundPath");
55 var color = ((SolidColorBrush)header.Background).Color;
56 if (selectedPath.Fill is not SolidColorBrush fill || fill.Color != color || hub.DocumentSurface.Background is not SolidColorBrush surface || surface.Color != color)
57 throw new InvalidOperationException("工作流选项卡与编辑窗口未形成统一主题背景:" + stage);
58 colors[root.RequestedTheme] = color;
59 dockingEvidence.Add(new { stage, theme = root.RequestedTheme.ToString(), stripLeft = stripOrigin.X, headerLeft = headerOrigin.X, tabBottom, headerTop = headerOrigin.Y, paletteWidth = palette.ActualWidth, background = color.ToString() });
60 }
61 root.RequestedTheme = ElementTheme.Dark; await VerifyDockingAsync("dark");
62 var dockedSplitter = Descendants<Controls.PaneSplitter>(hub).Single(x => AutomationProperties.GetName(x) == "调整节点库宽度");
63 var oldPaletteWidth = Descendants<Border>(hub).Single(x => x.Name == "PalettePanel").ActualWidth;
64 services.State.SetPaneSize("workflow.palette", oldPaletteWidth + 48); dockedSplitter.ReloadPreference(); await VerifyDockingAsync("resized");
65 if (Math.Abs(Descendants<Border>(hub).Single(x => x.Name == "PalettePanel").ActualWidth - oldPaletteWidth - 48) > 4) throw new InvalidOperationException("调整节点库宽度后标签与编辑器未随动");
66 await hub.OpenAsync(second.Id); await VerifyDockingAsync("second-tab");
67 await hub.OpenAsync(first.Id); await VerifyDockingAsync("first-tab-restored");
68 await CaptureWindowAsync(Path.Combine(output, "workflow-editor-connected-tabs-dark.png"));
69 root.RequestedTheme = ElementTheme.Light; await VerifyDockingAsync("light");
70 await CaptureWindowAsync(Path.Combine(output, "workflow-editor-connected-tabs-light.png"));
71 if (colors[ElementTheme.Dark] == colors[ElementTheme.Light]) throw new InvalidOperationException("工作流编辑区没有适配深浅主题");
72 await Infrastructure.Storage.AtomicJson.WriteAsync(Path.Combine(output, "workflow-document-docking.json"), dockingEvidence);
73 root.RequestedTheme = originalTheme;
74 var search = Descendants<TextBox>(hub).Single(x => x.Name == "PaletteSearch"); search.Text = "输出日志";
75 await WaitManagementAsync(() => a.ViewModel.PaletteRows.OfType<PaletteItem>().Any(x => x.Descriptor.Id == "core.log"));
76 var paletteList = Descendants<ListView>(hub).Single(x => x.Name == "PaletteList"); var logItem = a.ViewModel.PaletteRows.OfType<PaletteItem>().Single(x => x.Descriptor.Id == "core.log");
77 paletteList.ScrollIntoView(logItem); await WaitManagementAsync(() => paletteList.ContainerFromItem(logItem) is FrameworkElement);
78 var nodeCount = a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count;
79 var container = (ListViewItem)paletteList.ContainerFromItem(logItem);
80 var peer = FrameworkElementAutomationPeer.FromElement(container) ?? new ListViewItemAutomationPeer(container);
81 if (peer.GetPattern(PatternInterface.Invoke) is not IInvokeProvider invoke) throw new InvalidOperationException("节点库项缺少自动化调用入口");
82 invoke.Invoke();
83 await WaitManagementAsync(() => a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count == nodeCount + 1);
84 await a.ExecuteEditorCommandAsync("undo"); if (a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count != nodeCount) throw new InvalidOperationException("独立节点库添加与撤销未作用到当前工作流"); search.Text = "";
85 await services.Workflows.SetActivationAsync(first.Id, true, 9); await hub.SaveAllAsync();
86 if ((await services.Workflows.GetAsync(first.Id)).Enabled != true) throw new InvalidOperationException("编辑器保存覆盖了列表的启停状态");
87 await CaptureWindowAsync(Path.Combine(output, "workflow-editor-multiple-tabs-menus.png"));
88 navigation.NavigateTo<WorkflowListPage>(); await Task.Delay(300);
89 var frame = Descendants<Frame>(App.MainWindow.Content).Single(x => x.Content is WorkflowListPage); var list = (WorkflowListPage)frame.Content;
90 await list.SetEnabledAsync(first.Id, false);
91 if (services.Scheduler.StatusFor(await services.Workflows.GetAsync(first.Id)).Any(x => x.Enabled)) throw new InvalidOperationException("列表关闭后仍在调度");
92 navigation.NavigateTo<WorkflowEditorPage>(); await Task.Delay(150); if (hub.Documents.TabItems.Count != 2) throw new InvalidOperationException("离开编辑器后标签页丢失");
93 }
94 finally
95 {
96 await WorkflowEditorPage.CloseOpenAsync(first.Id); await WorkflowEditorPage.CloseOpenAsync(second.Id); navigation.NavigateTo<MainPage>();
97 services.State.SetPaneSize("workflow.palette", paletteSize); await services.State.SaveAsync(); root.RequestedTheme = originalTheme; App.MainWindow.AppWindow.Resize(originalSize);
98 }
36 99 }
37 100 }
Modified XFEWorkflow/XFEWorkflow/Views/AppShellPage.xaml +1 -1
@@ -56,7 +56,7 @@
56 56 <FontIcon Glyph="&#xEF90;"/>
57 57 </NavigationViewItem.Icon>
58 58 </NavigationViewItem>
59 <NavigationViewItem Content="工作流编辑器" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.WorkflowEditorPage"><NavigationViewItem.Icon><FontIcon Glyph="&#xE70F;"/></NavigationViewItem.Icon></NavigationViewItem>
59 <NavigationViewItem Content="工作流编辑器" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.WorkflowEditorPage"><NavigationViewItem.Icon><FontIcon Glyph="&#xE943;"/></NavigationViewItem.Icon></NavigationViewItem>
60 60 <NavigationViewItem Content="内置浏览器" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.BrowserPage" Icon="World"/>
61 61 <NavigationViewItem Content="插件中心" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.PluginsPage" Icon="Library"/>
62 62 <NavigationViewItem Content="运行记录" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.RunsPage" Icon="Clock"/>
Modified XFEWorkflow/XFEWorkflow/Views/MainPage.Document.cs +23 -0
@@ -4,13 +4,35 @@ namespace XFEWorkflow.Views;
4 4
5 5 public sealed partial class MainPage
6 6 {
7 private bool documentLayout;
8 private ColumnDefinition? documentPaletteColumn;
9 private Grid? documentWorkspace;
10 private double PaletteWidth => documentPaletteColumn?.ActualWidth ?? (documentLayout ? PaletteSplitter.PreferredSize : PaletteColumn.ActualWidth);
11 private double EditorWorkspaceWidth => documentWorkspace?.ActualWidth ?? (EditorLayout.ActualWidth + (documentLayout ? PaletteWidth + 8 : 0));
7 12 internal async Task OpenDocumentAsync(WorkflowDefinition workflow)
8 13 {
9 14 initialized = true;
10 15 LegacyHeader.Visibility = WorkflowLibraryExpander.Visibility = LegacyCommands.Visibility = Visibility.Collapsed;
11 16 WorkflowPageLayout.RowSpacing = 0;
17 documentLayout = true; DocumentBackground.Visibility = Visibility.Visible;
18 EditorLayout.Children.Remove(PalettePanel); EditorLayout.Children.Remove(PaletteSplitter);
19 PaletteColumn.Width = EditorLayout.ColumnDefinitions[1].Width = new(0);
20 EditorSurface.BorderThickness = new(0); EditorSurface.CornerRadius = new(0);
12 21 await ViewModel.LoadAsync(); ViewModel.SelectedWorkflow = workflow; ready.TrySetResult(); ActivateDocument();
13 22 }
23 internal void AttachDocumentPalette(Grid workspace, ColumnDefinition width)
24 {
25 documentWorkspace = workspace; documentPaletteColumn = width;
26 workspace.Children.Add(PalettePanel); workspace.Children.Add(PaletteSplitter);
27 PaletteSplitter.ReloadPreference(); ApplyPanelSizes();
28 }
29 internal void DetachDocumentPalette()
30 {
31 if (documentWorkspace is { } workspace) { workspace.Children.Remove(PalettePanel); workspace.Children.Remove(PaletteSplitter); }
32 documentPaletteColumn = null; documentWorkspace = null;
33 }
34 private void SetPaletteWidth(double value)
35 { if (documentPaletteColumn is { } column) column.Width = new(value); else if (!documentLayout) PaletteColumn.Width = new(value); }
14 36 internal void ActivateDocument()
15 37 { if (disposed) return; Current = this; ViewModel.IsRunning = services.Runs.IsRunning(ViewModel.SelectedWorkflow?.Id ?? ""); CodeEditor.IsAnalysisEnabled = CodeEditor.Visibility == Visibility.Visible; }
16 38 internal void DeactivateDocument() => CodeEditor.IsAnalysisEnabled = false;
@@ -37,6 +59,7 @@ public sealed partial class MainPage
37 59 case "paste": await CanvasEditor.PasteAsync(); break;
38 60 case "delete": CanvasEditor.Delete(); break;
39 61 case "selectAll": CanvasEditor.SelectAll(); break;
62 case "search": PaletteSearch.Focus(FocusState.Keyboard); PaletteSearch.SelectAll(); break;
40 63 case "properties": Properties_Click(this, new()); break;
41 64 case "output": ToggleOutput_Click(this, new()); break;
42 65 case "center": CenterCanvas_Click(this, new()); break;
Modified XFEWorkflow/XFEWorkflow/Views/MainPage.Panels.cs +2 -2
@@ -8,8 +8,8 @@ public sealed partial class MainPage
8 8 private bool updatingPaletteFilters;
9 9 private void InitializeResizablePanels()
10 10 {
11 PaletteSplitter.Bind("workflow.palette", 260, 216, 440, () => PaletteColumn.ActualWidth, value => PaletteColumn.Width = new(value),
12 () => EditorLayout.ActualWidth - 8 - 360 - (PropertySplit.DisplayMode == SplitViewDisplayMode.Inline && PropertySplit.IsPaneOpen ? PropertySplit.OpenPaneLength : 0));
11 PaletteSplitter.Bind("workflow.palette", 260, 216, 440, () => PaletteWidth, SetPaletteWidth,
12 () => EditorWorkspaceWidth - 8 - 360 - (PropertySplit.DisplayMode == SplitViewDisplayMode.Inline && PropertySplit.IsPaneOpen ? PropertySplit.OpenPaneLength : 0));
13 13 PropertySplitter.Bind("workflow.properties", 296, 248, 520, () => PropertySplit.OpenPaneLength, value => PropertySplit.OpenPaneLength = value,
14 14 () => PropertySplit.ActualWidth - (PropertySplit.DisplayMode == SplitViewDisplayMode.Inline ? 360 : 72));
15 15 OutputSplitter.Bind("workflow.output", 156, 80, 520, () => OutputPanelRow.ActualHeight, value => { if (!outputCollapsed) OutputPanelRow.Height = new(value); },
Modified XFEWorkflow/XFEWorkflow/Views/MainPage.xaml +3 -2
@@ -40,6 +40,7 @@
40 40 <Page.KeyboardAccelerators><KeyboardAccelerator Key="K" Modifiers="Control" Invoked="PaletteSearchShortcut_Invoked"/></Page.KeyboardAccelerators>
41 41 <Grid x:Name="WorkflowPageLayout" RowSpacing="10" SizeChanged="WorkflowPageLayout_SizeChanged">
42 42 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
43 <Border x:Name="DocumentBackground" Grid.RowSpan="5" Background="{ThemeResource TabViewItemHeaderBackgroundSelected}" Visibility="Collapsed"/>
43 44 <Grid x:Name="LegacyHeader" Margin="0,0,0,2" ColumnSpacing="16">
44 45 <Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
45 46 <StackPanel Spacing="3">
@@ -154,10 +155,10 @@
154 155 </Border>
155 156 </Grid>
156 157 </SplitView.Pane>
157 <Border BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
158 <Border x:Name="EditorSurface" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
158 159 <Grid>
159 160 <Grid.RowDefinitions><RowDefinition Height="48"/><RowDefinition Height="*"/></Grid.RowDefinitions>
160 <Grid Padding="8,0" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,0,0,1" ColumnSpacing="8">
161 <Grid x:Name="EditorModeHeader" Padding="8,0" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,0,0,1" ColumnSpacing="8" Background="{ThemeResource TabViewItemHeaderBackgroundSelected}">
161 162 <Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
162 163 <Border Background="{ThemeResource LayerFillColorDefaultBrush}" CornerRadius="6" Padding="2" VerticalAlignment="Center" ToolTipService.ToolTip="切换到 C# 时从当前图形生成代码;手动修改的代码会保留">
163 164 <StackPanel Orientation="Horizontal" Spacing="2">
Modified XFEWorkflow/XFEWorkflow/Views/MainPage.xaml.cs +1 -1
@@ -160,7 +160,7 @@ public sealed partial class MainPage : Page
160 160 private void EditorLayout_SizeChanged(object sender, SizeChangedEventArgs e)
161 161 {
162 162 if (PropertySplit is null) return;
163 var mode = e.NewSize.Width >= 1080 ? SplitViewDisplayMode.Inline : SplitViewDisplayMode.Overlay;
163 var mode = EditorWorkspaceWidth >= 1080 ? SplitViewDisplayMode.Inline : SplitViewDisplayMode.Overlay;
164 164 if (PropertySplit.DisplayMode != mode) { PropertySplit.DisplayMode = mode; PropertySplit.IsPaneOpen = mode == SplitViewDisplayMode.Inline; }
165 165 ApplyPanelSizes();
166 166 }
Modified XFEWorkflow/XFEWorkflow/Views/WorkflowEditorPage.cs +19 -4
@@ -10,8 +10,13 @@ namespace XFEWorkflow.Views;
10 10 public sealed partial class WorkflowEditorPage : Page
11 11 {
12 12 public static WorkflowEditorPage? Current { get; private set; }
13 internal TabView Documents { get; } = new() { TabWidthMode = TabViewWidthMode.SizeToContent, CanReorderTabs = true, AllowDrop = true };
13 internal TabView Documents { get; } = new() { Name = "WorkflowDocumentTabs", TabWidthMode = TabViewWidthMode.SizeToContent, CanReorderTabs = true, AllowDropTabs = true, VerticalAlignment = VerticalAlignment.Stretch, Padding = new(0) };
14 14 public MainPage? ActiveEditor => (Documents.SelectedItem as TabViewItem)?.Content as MainPage;
15 internal Border DocumentSurface { get; }
16 private readonly Grid workspace = new();
17 private readonly ColumnDefinition paletteColumn = new() { Width = new(0) };
18 private readonly ColumnDefinition paletteSplitterColumn = new() { Width = new(0) };
19 private MainPage? paletteOwner;
15 20 private readonly InfoBar error = new() { Severity = InfoBarSeverity.Error, IsClosable = true, Visibility = Visibility.Collapsed };
16 21 private readonly Button run = new() { Content = "▶ 运行", Style = (Style)Application.Current.Resources["AccentButtonStyle"], Padding = new(18, 7, 18, 7) };
17 22 private readonly Button stop = new() { Content = "□ 停止" };
@@ -40,12 +45,16 @@ public sealed partial class WorkflowEditorPage : Page
40 45 var quick = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, VerticalAlignment = VerticalAlignment.Center }; quick.Children.Add(run); quick.Children.Add(stop); Grid.SetColumn(quick, 1); toolbar.Children.Add(quick); root.Children.Add(toolbar);
41 46 run.Click += async (_, _) => await GuardAsync(async () => { if (ActiveEditor is { } editor) await editor.ExecuteEditorCommandAsync("run"); });
42 47 stop.Click += async (_, _) => await GuardAsync(async () => { if (ActiveEditor is { } editor) await editor.ExecuteEditorCommandAsync("stop"); });
43 Grid.SetRow(error, 1); root.Children.Add(error); var workspace = new Grid(); workspace.Children.Add(Documents); workspace.Children.Add(empty); Grid.SetRow(workspace, 2); root.Children.Add(workspace); Content = root;
48 Grid.SetRow(error, 1); root.Children.Add(error);
49 workspace.ColumnDefinitions.Add(paletteColumn); workspace.ColumnDefinitions.Add(paletteSplitterColumn); workspace.ColumnDefinitions.Add(new());
50 var documentContent = new Grid(); documentContent.Children.Add(Documents); documentContent.Children.Add(empty);
51 DocumentSurface = new Border { Name = "WorkflowDocumentSurface", Style = (Style)Resources["WorkflowDocumentSurfaceStyle"], Child = documentContent };
52 Grid.SetColumn(DocumentSurface, 2); workspace.Children.Add(DocumentSurface); Grid.SetRow(workspace, 2); root.Children.Add(workspace); Content = root;
44 53 Documents.TabItemsChanged += (_, _) => UpdateChrome(); Documents.SelectionChanged += (_, _) => UpdateChrome();
45 54 Documents.AddTabButtonClick += async (_, _) => await GuardAsync(NewAsync);
46 55 Documents.TabCloseRequested += async (_, args) => await GuardAsync(() => CloseAsync(args.Tab));
47 56 error.Closed += (_, _) => error.Visibility = Visibility.Collapsed;
48 foreach (var pair in new[] { (VirtualKey.S, (Func<Task>)SaveAllAsync), (VirtualKey.N, (Func<Task>)NewAsync), (VirtualKey.W, (Func<Task>)(async () => { if (Documents.SelectedItem is TabViewItem item) await CloseAsync(item); })) })
57 foreach (var pair in new[] { (VirtualKey.S, (Func<Task>)SaveAllAsync), (VirtualKey.N, (Func<Task>)NewAsync), (VirtualKey.W, (Func<Task>)(async () => { if (Documents.SelectedItem is TabViewItem item) await CloseAsync(item); })), (VirtualKey.K, (Func<Task>)(() => ActiveEditor?.ExecuteEditorCommandAsync("search") ?? Task.CompletedTask)) })
49 58 { var accelerator = new KeyboardAccelerator { Key = pair.Item1, Modifiers = VirtualKeyModifiers.Control }; accelerator.Invoked += async (_, e) => { e.Handled = true; await GuardAsync(pair.Item2); }; KeyboardAccelerators.Add(accelerator); }
50 59 Loaded += (_, _) => UpdateChrome(); Unloaded += async (_, _) => { foreach (var tab in documents.Values) ((MainPage)tab.Content).DeactivateDocument(); await GuardAsync(SaveAllAsync); }; UpdateChrome();
51 60 }
@@ -68,7 +77,7 @@ public sealed partial class WorkflowEditorPage : Page
68 77 {
69 78 if (documents.TryGetValue(id, out var existing)) { Documents.SelectedItem = existing; return (MainPage)existing.Content; }
70 79 var flow = await AppServices.Current.Workflows.GetAsync(id); var editor = new MainPage(); await editor.OpenDocumentAsync(flow);
71 var tab = new TabViewItem { Header = flow.Name, Content = editor, IconSource = new FontIconSource { Glyph = "\uEF90" }, IsClosable = true, Tag = id };
80 var tab = new TabViewItem { Header = flow.Name, Content = editor, IconSource = new FontIconSource { Glyph = "\uEF90" }, IsClosable = true, Tag = id, HorizontalContentAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Stretch };
72 81 editor.ViewModel.PropertyChanged += (_, _) =>
73 82 { tab.Header = (editor.ViewModel.SelectedWorkflow?.Name ?? flow.Name) + (editor.ViewModel.Status.Contains("未保存", StringComparison.Ordinal) ? " •" : ""); if (ReferenceEquals(ActiveEditor, editor)) UpdateRunButtons(); };
74 83 documents[id] = tab; Documents.TabItems.Add(tab); Documents.SelectedItem = tab; UpdateChrome(); return editor;
@@ -78,6 +87,12 @@ public sealed partial class WorkflowEditorPage : Page
78 87 private void UpdateChrome()
79 88 {
80 89 empty.Visibility = documents.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
90 if (!ReferenceEquals(paletteOwner, ActiveEditor))
91 {
92 paletteOwner?.DetachDocumentPalette(); paletteOwner = ActiveEditor;
93 paletteSplitterColumn.Width = new(paletteOwner is null ? 0 : 8); paletteColumn.Width = new(paletteOwner is null ? 0 : 260);
94 paletteOwner?.AttachDocumentPalette(workspace, paletteColumn);
95 }
81 96 foreach (var tab in documents.Values) { var editor = (MainPage)tab.Content; if (ReferenceEquals(editor, ActiveEditor)) editor.ActivateDocument(); else editor.DeactivateDocument(); }
82 97 UpdateRunButtons();
83 98 }
Modified XFEWorkflow/XFEWorkflow/Views/WorkflowEditorPage.xaml +10 -1
@@ -1,3 +1,12 @@
1 1 <Page x:Class="XFEWorkflow.Views.WorkflowEditorPage"
2 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"/>
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
4 <Page.Resources>
5 <Style x:Key="WorkflowDocumentSurfaceStyle" TargetType="Border">
6 <Setter Property="Background" Value="{ThemeResource TabViewItemHeaderBackgroundSelected}"/>
7 <Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
8 <Setter Property="BorderThickness" Value="1"/>
9 <Setter Property="CornerRadius" Value="8"/>
10 </Style>
11 </Page.Resources>
12 </Page>
Modified XFEWorkflow/XFEWorkflow/Views/WorkflowListPage.cs +5 -2
@@ -36,12 +36,15 @@ public sealed partial class WorkflowListPage : ManagementPage
36 36 var filtered = workflows.Where(x => x.Name.Contains(search.Text, StringComparison.OrdinalIgnoreCase) && (stateFilter.SelectedIndex == 0 || x.Enabled == (stateFilter.SelectedIndex == 1))).OrderByDescending(x => x.Priority).ThenByDescending(x => x.ModifiedAt).ToArray();
37 37 foreach (var workflow in filtered)
38 38 {
39 var heading = new Grid { ColumnSpacing = 14, Padding = new(0, 6, 0, 6) }; heading.ColumnDefinitions.Add(new() { Width = new(32) }); heading.ColumnDefinitions.Add(new()); heading.ColumnDefinitions.Add(new() { Width = GridLength.Auto });
39 var heading = new Grid { ColumnSpacing = 14, Padding = new(0, 6, 0, 6) }; heading.ColumnDefinitions.Add(new() { Width = new(32) }); heading.ColumnDefinitions.Add(new()); heading.ColumnDefinitions.Add(new() { Width = GridLength.Auto }); heading.ColumnDefinitions.Add(new() { Width = GridLength.Auto });
40 40 heading.Children.Add(new FontIcon { Glyph = "\uEF90", FontSize = 22 });
41 41 var name = new StackPanel { Spacing = 4 }; name.Children.Add(new TextBlock { Text = workflow.Name, FontSize = 16, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, TextTrimming = TextTrimming.CharacterEllipsis });
42 42 var statuses = Services.Scheduler.StatusFor(workflow); name.Children.Add(Caption((workflow.Enabled == true ? "已开启" : "已关闭") + " · " + (workflow.Kind == WorkflowKind.Graph ? "图形化" : "C#") + " · " + statuses.Count + " 个触发节点")); Grid.SetColumn(name, 1); heading.Children.Add(name);
43 43 var toggle = new ToggleSwitch { IsOn = workflow.Enabled == true, OnContent = "开启", OffContent = "关闭", VerticalAlignment = VerticalAlignment.Center };
44 44 AutomationProperties.SetName(toggle, "启停工作流 " + workflow.Name); Grid.SetColumn(toggle, 2); heading.Children.Add(toggle);
45 var editButton = ActionButton("编辑", () => OpenAsync(workflow.Id), "EditWorkflow-" + workflow.Id);
46 editButton.Content = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6, Children = { new FontIcon { Glyph = "\uE70F", FontSize = 14 }, new TextBlock { Text = "编辑" } } };
47 AutomationProperties.SetName(editButton, "编辑工作流 " + workflow.Name); Grid.SetColumn(editButton, 3); heading.Children.Add(editButton);
45 48 var details = new StackPanel { Spacing = 12, Padding = new(0, 0, 0, 6) };
46 49 if (workflow.Description.Length > 0) details.Children.Add(Label(workflow.Description));
47 50 var runtimeStatus = new StackPanel { Spacing = 4 }; details.Children.Add(runtimeStatus);
@@ -54,7 +57,7 @@ public sealed partial class WorkflowListPage : ManagementPage
54 57 if (statuses.Count == 0) details.Children.Add(Caption("手动运行 · 在编辑器中添加触发节点可自动执行"));
55 58 var priority = new NumberBox { Header = "优先级(较大值优先)", Value = workflow.Priority, Minimum = -10000, Maximum = 10000, SmallChange = 1, SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, Width = 240, HorizontalAlignment = HorizontalAlignment.Left };
56 59 priority.LostFocus += async (_, _) => await Guard(async () => { if (!double.IsFinite(priority.Value)) return; var latest = await Services.Workflows.GetAsync(workflow.Id); if (latest.Priority == (int)priority.Value) return; await Services.Workflows.SetActivationAsync(workflow.Id, latest.Enabled == true, (int)priority.Value); workflow.Priority = (int)priority.Value; await Services.Scheduler.RefreshAsync(); }); details.Children.Add(priority);
57 var actions = new Controls.AdaptiveWrapPanel(); actions.Children.Add(ActionButton("编辑工作流", () => OpenAsync(workflow.Id), "EditWorkflow-" + workflow.Id, true));
60 var actions = new Controls.AdaptiveWrapPanel();
58 61 actions.Children.Add(ActionButton("运行一次", async () => { await WorkflowEditorPage.SaveOpenAsync(workflow.Id); var result = await Services.Runs.RunAsync(await Services.Workflows.GetAsync(workflow.Id)); Status.Text = workflow.Name + " · " + result.Status; }));
59 62 actions.Children.Add(ActionButton("复制", async () => { var copy = WorkflowJson.Clone(await Services.Workflows.GetAsync(workflow.Id)); copy.Id = Guid.NewGuid().ToString("N"); copy.Name += " 副本"; copy.Enabled = false; await Services.Workflows.SaveAsync(copy); await ReloadAsync(); }));
60 63 actions.Children.Add(ActionButton("删除", async () =>
Added docs/screenshots/workflow-document-docking.json +52 -0
@@ -0,0 +1,52 @@
1 [
2 {
3 "stage": "dark",
4 "theme": "Dark",
5 "stripLeft": 269.3333435058594,
6 "headerLeft": 269.3333435058594,
7 "tabBottom": 85.33332824707031,
8 "headerTop": 85.33332824707031,
9 "paletteWidth": 260,
10 "background": "#FF282828"
11 },
12 {
13 "stage": "resized",
14 "theme": "Dark",
15 "stripLeft": 317.3333435058594,
16 "headerLeft": 317.3333435058594,
17 "tabBottom": 85.33332824707031,
18 "headerTop": 85.33332824707031,
19 "paletteWidth": 308,
20 "background": "#FF282828"
21 },
22 {
23 "stage": "second-tab",
24 "theme": "Dark",
25 "stripLeft": 317.3333435058594,
26 "headerLeft": 317.3333435058594,
27 "tabBottom": 85.33332824707031,
28 "headerTop": 85.33332824707031,
29 "paletteWidth": 308,
30 "background": "#FF282828"
31 },
32 {
33 "stage": "first-tab-restored",
34 "theme": "Dark",
35 "stripLeft": 317.3333435058594,
36 "headerLeft": 317.3333435058594,
37 "tabBottom": 85.33332824707031,
38 "headerTop": 85.33332824707031,
39 "paletteWidth": 308,
40 "background": "#FF282828"
41 },
42 {
43 "stage": "light",
44 "theme": "Light",
45 "stripLeft": 317.3333435058594,
46 "headerLeft": 317.3333435058594,
47 "tabBottom": 85.33332824707031,
48 "headerTop": 85.33332824707031,
49 "paletteWidth": 308,
50 "background": "#FFF9F9F9"
51 }
52 ]
Added docs/screenshots/workflow-editor-connected-tabs-dark.png +0 -0
二进制文件已变更,无法进行逐行预览。
Added docs/screenshots/workflow-editor-connected-tabs-light.png +0 -0
二进制文件已变更,无法进行逐行预览。
Added docs/screenshots/workflow-list-edit-actions.png +0 -0
二进制文件已变更,无法进行逐行预览。
Modified docs/workbench.md +5 -1
@@ -2,7 +2,11 @@
2 2
3 3 ## 页面
4 4
5 新建工作流只填写名称,编辑器保留图形化与 C# 切换。顶部“所有工作流”可展开并搜索,每个条目也可展开,查看触发器是否启用、下次执行时间或监听错误。当前工作流的触发器与下次运行时间始终显示在编辑区上方。
5 新建工作流只填写名称,工作流列表与多标签编辑器独立。列表中的每一项均可展开,查看下次执行时间或监听错误;右侧的“编辑”按钮在收起时也直接打开对应标签。工作流启停由列表控制。
6
7 编辑器入口使用代码图标。左侧节点库独立停靠,工作流标签从右侧实际编辑窗口的左边缘开始排列;标签、图形化/C# 工具栏、属性与输出属于同一个带边框的工作区。选中标签与编辑窗口使用相同的主题背景并无间隙相接。调整节点库宽度时,标签和编辑内容一同移动,切换标签保留各自的编辑状态与节点库筛选。
8
9 实机截图:[列表编辑入口](screenshots/workflow-list-edit-actions.png)、[深色编辑器](screenshots/workflow-editor-connected-tabs-dark.png)、[浅色编辑器](screenshots/workflow-editor-connected-tabs-light.png)。[布局与主题验证数据](screenshots/workflow-document-docking.json)记录标签边界、编辑区边界和面板宽度。
6 10
7 11 插件中心合并本地管理与在线目录:已安装、插件 Space、我的发布;管理员额外看到审核中心。右侧详情显示来源、版本、能力、触发器、依赖和使用此版本的工作流数量。下载、上传、审核均使用真实 Space 接口。账号与插件共用固定的本地服务器 `http://localhost:3300/space/v1/`,无需在客户端配置地址。
8 12