From 723c0b9cdce586295da4a3e1e15b8d63174e8ef7 Mon Sep 17 00:00:00 2001 From: PrefacedCorg <1876568293@qq.com> Date: Sat, 2 May 2026 13:41:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B7=A5=E5=85=B7=E6=A0=8F=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE):=20=E6=B7=BB=E5=8A=A0=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B5=E9=9D=A2=E5=8F=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现工具栏配置页面,允许用户调整工具栏按钮的顺序和可见性 包含主工具栏、画布控制和尾部按钮三个区域的配置 支持恢复默认布局功能 --- Ink Canvas/Controls/Toolbar/IToolbarItem.cs | 12 +- .../Controls/Toolbar/Items/EraserToolItem.cs | 1 + .../Controls/Toolbar/Items/PenToolItem.cs | 1 + .../Toolbar/Items/ShapeDrawToolItem.cs | 1 + .../Items/ToolbarImageButtonItemBase.cs | 14 +- .../Controls/Toolbar/Items/ToolsToolItem.cs | 1 + .../Controls/Toolbar/ToolbarRegistry.cs | 59 +++- Ink Canvas/Helpers/AnimationsHelper.cs | 103 +++++- Ink Canvas/MainWindow.xaml | 301 +++++++++--------- Ink Canvas/MainWindow.xaml.cs | 8 + Ink Canvas/MainWindow_cs/MW_BoardIcons.cs | 2 +- .../MainWindow_cs/MW_FloatingBarIcons.cs | 20 +- .../MainWindow_cs/MW_Save&OpenStrokes.cs | 4 +- Ink Canvas/MainWindow_cs/MW_Toolbar.cs | 56 +++- .../SettingsViews/Pages/ToolbarPage.xaml | 92 ++++++ .../SettingsViews/Pages/ToolbarPage.xaml.cs | 232 ++++++++++++++ .../Windows/SettingsViews/SettingsWindow.xaml | 9 + .../SettingsViews/SettingsWindow.xaml.cs | 4 + 18 files changed, 714 insertions(+), 206 deletions(-) create mode 100644 Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml create mode 100644 Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml.cs diff --git a/Ink Canvas/Controls/Toolbar/IToolbarItem.cs b/Ink Canvas/Controls/Toolbar/IToolbarItem.cs index 0c7498c7..2f008826 100644 --- a/Ink Canvas/Controls/Toolbar/IToolbarItem.cs +++ b/Ink Canvas/Controls/Toolbar/IToolbarItem.cs @@ -2,28 +2,24 @@ using System.Windows; namespace Ink_Canvas.Controls.Toolbar { - /// - /// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。 - /// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。 - /// public interface IToolbarItem { - /// 稳定、唯一的 id,用于持久化用户配置。不要随便改。 string Id { get; } ToolbarSlot DefaultSlot { get; } - /// 同一 slot 内的默认顺序,小的在前。 int DefaultOrder { get; } bool DefaultVisible { get; } ToolbarInsertPosition DefaultPosition { get; } - /// 仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。 string DefaultAnchorName { get; } - /// 构造 UI 元素并接线所有行为。 + string DisplayName { get; } + + string MenuPanelName { get; } + FrameworkElement BuildView(IToolbarHost host); } } \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs index fb44594a..6cb37806 100644 --- a/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs +++ b/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs @@ -8,6 +8,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items public override string LocalizationKey => "FloatingBar_AreaEraser"; public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; public override int DefaultOrder => 100; + public override string MenuPanelName => "EraserSizePanel"; protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) => host.Window.EraserIcon_Click(sender, e); diff --git a/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs index 8f339eb0..1bd2ac36 100644 --- a/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs +++ b/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs @@ -8,6 +8,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items public override string LocalizationKey => "FloatingBar_Annotate"; public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain; public override int DefaultOrder => 110; + public override string MenuPanelName => "PenPalette"; protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) => host.Window.PenIcon_Click(sender, e); diff --git a/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs index 675cc43a..48133f54 100644 --- a/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs +++ b/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs @@ -8,6 +8,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items public override string LocalizationKey => "FloatingBar_Geometry"; public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; public override int DefaultOrder => 130; + public override string MenuPanelName => "BorderDrawShape"; protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) => host.Window.ImageDrawShape_MouseUp(sender, e); diff --git a/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs b/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs index 330322e5..e49df7a7 100644 --- a/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs +++ b/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs @@ -5,10 +5,6 @@ using System.Windows.Media; namespace Ink_Canvas.Controls.Toolbar.Items { - /// - /// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。 - /// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。 - /// internal abstract class ToolbarImageButtonItemBase : IToolbarItem { public abstract string Id { get; } @@ -19,22 +15,22 @@ namespace Ink_Canvas.Controls.Toolbar.Items public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend; public virtual string DefaultAnchorName => null; - /// DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。 - protected virtual string IconBrushResourceKey => null; + public string DisplayName => Strings.GetString(LocalizationKey) ?? LocalizationKey; + public virtual string MenuPanelName => null; - /// DynamicResource 名称,用于 LabelBrush(文字颜色)。默认为 null(使用控件自带前景色)。 + protected virtual string IconBrushResourceKey => null; protected virtual string LabelBrushResourceKey => null; protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e); - /// 构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。 protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { } public FrameworkElement BuildView(IToolbarHost host) { var btn = new ToolbarImageButton { - Label = Strings.GetString(LocalizationKey) ?? LocalizationKey + Label = Strings.GetString(LocalizationKey) ?? LocalizationKey, + Tag = "ToolbarRegistryInjected" }; if (!string.IsNullOrEmpty(IconBrushResourceKey)) { diff --git a/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs index cb80b8f8..506a286f 100644 --- a/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs +++ b/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs @@ -10,6 +10,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items public override int DefaultOrder => 110; public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor; public override string DefaultAnchorName => "FloatingBarEndSeparator"; + public override string MenuPanelName => "BorderTools"; protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) => host.Window.SymbolIconTools_MouseUp(sender, e); diff --git a/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs b/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs index 3701c296..46d94d17 100644 --- a/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs +++ b/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs @@ -8,12 +8,10 @@ using System.Windows.Controls; namespace Ink_Canvas.Controls.Toolbar { - /// - /// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。 - /// public static class ToolbarRegistry { private static List _items; + internal const string InjectedTag = "ToolbarRegistryInjected"; public static IReadOnlyList Discover() { @@ -34,13 +32,25 @@ namespace Ink_Canvas.Controls.Toolbar }) .Where(i => i != null) .ToList(); + LogHelper.WriteLogToFile($"ToolbarRegistry: Discover 完成, 发现 {_items.Count} 个条目", LogHelper.LogType.Info); return _items; } - /// 按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。 + public static void ClearInjected(Panel container) + { + if (container == null) return; + var toRemove = container.Children.OfType() + .Where(e => e.Tag as string == InjectedTag) + .ToList(); + foreach (var element in toRemove) + container.Children.Remove(element); + LogHelper.WriteLogToFile($"ToolbarRegistry: ClearInjected 清除 {toRemove.Count} 个元素 [{container.Name}]", LogHelper.LogType.Info); + } + public static void Populate(IToolbarHost host, IDictionary slots, ToolbarLayoutSettings layout) { - if (host == null || slots == null) return; + LogHelper.WriteLogToFile($"ToolbarRegistry: Populate 开始", LogHelper.LogType.Info); + if (host == null || slots == null) { LogHelper.WriteLogToFile("ToolbarRegistry: Populate host 或 slots 为空", LogHelper.LogType.Warning); return; } layout = layout ?? new ToolbarLayoutSettings(); var grouped = new Dictionary>(); @@ -65,18 +75,51 @@ namespace Ink_Canvas.Controls.Toolbar } list.Add((item, cfg)); } + LogHelper.WriteLogToFile($"ToolbarRegistry: 分组完成, {grouped.Count} 个 slot 有可见条目", LogHelper.LogType.Info); foreach (var kv in grouped) { if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue; + LogHelper.WriteLogToFile($"ToolbarRegistry: 注入到 {kv.Key}, 条目数={kv.Value.Count}", LogHelper.LogType.Info); InjectIntoContainer(host, container, kv.Value); } + + ApplyMenuVisibility(host, layout); + LogHelper.WriteLogToFile($"ToolbarRegistry: Populate 完成", LogHelper.LogType.Info); + } + + public static void ApplyMenuVisibility(IToolbarHost host, ToolbarLayoutSettings layout) + { + if (host == null || layout == null) return; + foreach (var item in Discover()) + { + if (string.IsNullOrEmpty(item.MenuPanelName)) continue; + bool visible = true; + if (layout.Items.TryGetValue(item.Id, out var cfg)) + visible = cfg.Visible; + try + { + var menuElement = host.Window.FindName(item.MenuPanelName) as FrameworkElement; + if (menuElement != null) + { + menuElement.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; + LogHelper.WriteLogToFile($"ToolbarRegistry: 菜单 [{item.MenuPanelName}] -> {(visible ? "Visible" : "Collapsed")}", LogHelper.LogType.Info); + } + else + { + LogHelper.WriteLogToFile($"ToolbarRegistry: 找不到菜单面板 [{item.MenuPanelName}]", LogHelper.LogType.Warning); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"ToolbarRegistry: 设置菜单可见性异常 [{item.MenuPanelName}]: {ex.Message}", LogHelper.LogType.Warning); + } + } } private static void InjectIntoContainer(IToolbarHost host, Panel container, List<(IToolbarItem item, ToolbarItemConfig cfg)> entries) { - // 按 Position 分桶,每桶内按 Order 升序。 var prepend = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Prepend).OrderBy(e => e.cfg.Order).ToList(); var append = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Append).OrderBy(e => e.cfg.Order).ToList(); var before = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.BeforeAnchor).ToList(); @@ -153,9 +196,9 @@ namespace Ink_Canvas.Controls.Toolbar } catch (Exception ex) { - LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning); + LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error); return null; } } } -} \ No newline at end of file +} diff --git a/Ink Canvas/Helpers/AnimationsHelper.cs b/Ink Canvas/Helpers/AnimationsHelper.cs index 00e8ba46..76fa7cf5 100644 --- a/Ink Canvas/Helpers/AnimationsHelper.cs +++ b/Ink Canvas/Helpers/AnimationsHelper.cs @@ -1,5 +1,6 @@ using System; using System.Windows; +using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Media.Animation; @@ -264,7 +265,6 @@ namespace Ink_Canvas.Helpers var sb = new Storyboard(); - // 渐变动画 var fadeOutAnimation = new DoubleAnimation { From = 1, @@ -283,5 +283,106 @@ namespace Ink_Canvas.Helpers sb.Begin((FrameworkElement)element); } + public static void ShowPopupWithSlideAndFade(Popup popup, double duration = 0.15) + { + try + { + if (popup == null) + throw new ArgumentNullException(nameof(popup)); + + if (popup.IsOpen) return; + + var child = popup.Child as FrameworkElement; + if (child == null) + { + popup.IsOpen = true; + return; + } + + child.Opacity = 0.5; + child.RenderTransform = new TranslateTransform(0, 10); + + popup.IsOpen = true; + + var sb = new Storyboard(); + + var fadeInAnimation = new DoubleAnimation + { + From = 0.5, + To = 1, + Duration = TimeSpan.FromSeconds(duration) + }; + fadeInAnimation.EasingFunction = new CubicEase(); + Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty)); + + var slideAnimation = new DoubleAnimation + { + From = 10, + To = 0, + Duration = TimeSpan.FromSeconds(duration) + }; + slideAnimation.EasingFunction = new CubicEase(); + Storyboard.SetTargetProperty(slideAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)")); + + sb.Children.Add(fadeInAnimation); + sb.Children.Add(slideAnimation); + + sb.Begin(child); + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + } + + public static void HidePopupWithSlideAndFade(Popup popup, double duration = 0.15) + { + try + { + if (popup == null) + throw new ArgumentNullException(nameof(popup)); + + if (!popup.IsOpen) return; + + var child = popup.Child as FrameworkElement; + if (child == null) + { + popup.IsOpen = false; + return; + } + + var sb = new Storyboard(); + + var fadeOutAnimation = new DoubleAnimation + { + From = 1, + To = 0, + Duration = TimeSpan.FromSeconds(duration) + }; + fadeOutAnimation.EasingFunction = new CubicEase(); + Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty)); + + var slideAnimation = new DoubleAnimation + { + From = 0, + To = 10, + Duration = TimeSpan.FromSeconds(duration) + }; + slideAnimation.EasingFunction = new CubicEase(); + Storyboard.SetTargetProperty(slideAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)")); + + sb.Children.Add(fadeOutAnimation); + sb.Children.Add(slideAnimation); + + sb.Completed += (s, e) => + { + popup.IsOpen = false; + child.Opacity = 1; + child.RenderTransform = new TranslateTransform(); + }; + + child.RenderTransform = new TranslateTransform(); + sb.Begin(child); + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + } + } } diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 0f2ce824..0a15ca5f 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -666,10 +666,10 @@ - + + Visibility="Collapsed" + Panel.ZIndex="1000" /> @@ -1489,18 +1489,20 @@ - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +