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 @@
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2395,7 +2390,7 @@
HorizontalAlignment="Left">
+ Orientation="{Binding ElementName=StackPanelFloatingBar, Path=Orientation}">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs
index fcde88b2..62990d77 100644
--- a/Ink Canvas/MainWindow.xaml.cs
+++ b/Ink Canvas/MainWindow.xaml.cs
@@ -100,6 +100,14 @@ namespace Ink_Canvas
*/
InitializeComponent();
+ BoardBorderToolsPopup.CustomPopupPlacementCallback =
+ (popupSize, targetSize, offset) => new[]
+ {
+ new CustomPopupPlacement(
+ new Point((targetSize.Width - popupSize.Width) / 2, -popupSize.Height - 5),
+ PopupPrimaryAxis.Vertical)
+ };
+
BlackboardLeftSide.Visibility = Visibility.Collapsed;
BlackboardCenterSide.Visibility = Visibility.Collapsed;
BlackboardRightSide.Visibility = Visibility.Collapsed;
diff --git a/Ink Canvas/MainWindow_cs/MW_BoardIcons.cs b/Ink Canvas/MainWindow_cs/MW_BoardIcons.cs
index e22862e8..e350ac1c 100644
--- a/Ink Canvas/MainWindow_cs/MW_BoardIcons.cs
+++ b/Ink Canvas/MainWindow_cs/MW_BoardIcons.cs
@@ -35,7 +35,7 @@ namespace Ink_Canvas
{
AnimationsHelper.HideWithSlideAndFade(EraserSizePanel);
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(PenPalette);
AnimationsHelper.HideWithSlideAndFade(BoardPenPalette);
AnimationsHelper.HideWithSlideAndFade(BorderDrawShape);
diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs
index 20c184b5..4703f75d 100644
--- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs
+++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs
@@ -295,7 +295,7 @@ namespace Ink_Canvas
private void HideSubPanelsImmediately()
{
BorderTools.Visibility = Visibility.Collapsed;
- BoardBorderTools.Visibility = Visibility.Collapsed;
+ BoardBorderToolsPopup.IsOpen = false;
PenPalette.Visibility = Visibility.Collapsed;
BoardPenPalette.Visibility = Visibility.Collapsed;
BoardEraserSizePanel.Visibility = Visibility.Collapsed;
@@ -377,7 +377,7 @@ namespace Ink_Canvas
internal async void HideSubPanels(string mode = null, bool autoAlignCenter = false)
{
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(PenPalette);
AnimationsHelper.HideWithSlideAndFade(BoardPenPalette);
AnimationsHelper.HideWithSlideAndFade(BoardEraserSizePanel);
@@ -1100,7 +1100,7 @@ namespace Ink_Canvas
LeftUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
RightUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
if (Settings.RandSettings?.UseNewStyleUI == true)
@@ -1152,7 +1152,7 @@ namespace Ink_Canvas
private void OperatingGuideWindowIcon_MouseUp(object sender, MouseButtonEventArgs e)
{
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
new OperatingGuideWindow().Show();
@@ -1172,7 +1172,7 @@ namespace Ink_Canvas
RightUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
// 根据设置决定使用哪个点名窗口
@@ -1307,7 +1307,7 @@ namespace Ink_Canvas
RightUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
// 检查是否启用了外部点名功能
@@ -1386,7 +1386,7 @@ namespace Ink_Canvas
//if (lastBorderMouseDownObject != sender) return;
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
CollapseBorderDrawShape();
@@ -1687,10 +1687,10 @@ namespace Ink_Canvas
/// 鼠标按钮事件参数
internal void SymbolIconTools_MouseUp(object sender, MouseButtonEventArgs e)
{
- if (BorderTools.Visibility == Visibility.Visible || BoardBorderTools.Visibility == Visibility.Visible)
+ if (BorderTools.Visibility == Visibility.Visible || BoardBorderToolsPopup.IsOpen)
{
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
}
else
{
@@ -1702,7 +1702,7 @@ namespace Ink_Canvas
}
else
{
- AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardBorderTools);
+ AnimationsHelper.ShowPopupWithSlideAndFade(BoardBorderToolsPopup);
}
}
}
diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs
index 12354943..30350ef6 100644
--- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs
+++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs
@@ -99,7 +99,7 @@ namespace Ink_Canvas
if (lastBorderMouseDownObject != sender || inkCanvas.Visibility != Visibility.Visible) return;
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
GridNotifications.Visibility = Visibility.Collapsed;
@@ -917,7 +917,7 @@ namespace Ink_Canvas
{
if (lastBorderMouseDownObject != sender) return;
AnimationsHelper.HideWithSlideAndFade(BorderTools);
- AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
+ AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
var openFileDialog = new OpenFileDialog();
openFileDialog.InitialDirectory = Settings.Automation.AutoSavedStrokesLocation;
diff --git a/Ink Canvas/MainWindow_cs/MW_Toolbar.cs b/Ink Canvas/MainWindow_cs/MW_Toolbar.cs
index 13d81549..30f8204d 100644
--- a/Ink Canvas/MainWindow_cs/MW_Toolbar.cs
+++ b/Ink Canvas/MainWindow_cs/MW_Toolbar.cs
@@ -1,5 +1,7 @@
+using System;
using Ink_Canvas.Controls;
using Ink_Canvas.Controls.Toolbar;
+using Ink_Canvas.Helpers;
using System.Collections.Generic;
using System.Windows.Controls;
@@ -7,8 +9,6 @@ namespace Ink_Canvas
{
public partial class MainWindow
{
- // 这批属性替代了 XAML 中原有的 x:Name 自动生成字段;外部代码继续按原名访问。
- // 由对应 Toolbar Item 的 AfterBuild 回填,Populate 发生在 Window_Loaded 早期。
internal ToolbarImageButton SymbolIconDelete { get; private set; }
internal ToolbarImageButton Eraser_Icon { get; private set; }
internal ToolbarImageButton EraserByStrokes_Icon { get; private set; }
@@ -35,22 +35,46 @@ namespace Ink_Canvas
internal void AttachToolsBtn(ToolbarImageButton btn) => ToolsFloatingBarBtn = btn;
internal void AttachFoldIcon(ToolbarImageButton btn) => Fold_Icon = btn;
- ///
- /// 在 Window_Loaded 早期调用:按 Settings.Toolbar 配置把插件化按钮填充到对应容器。
- /// 必须在 LoadSettings 之前,因为 LoadSettings 会访问 Cursor_Icon/Pen_Icon/Eraser_Icon 等。
- ///
internal void InitializeToolbarPlugins()
{
- ToolbarHost = new ToolbarHost(this);
- var slots = new Dictionary
+ LogHelper.WriteLogToFile("MW_Toolbar: InitializeToolbarPlugins 开始", LogHelper.LogType.Info);
+ try
{
- { ToolbarSlot.FloatingBarMain, StackPanelFloatingBar },
- { ToolbarSlot.FloatingBarCanvasControls, StackPanelCanvasControls },
- { ToolbarSlot.FloatingBarEnd, StackPanelFloatingBarEnd },
- { ToolbarSlot.BlackboardLeft, BlackboardLeftSide },
- { ToolbarSlot.BlackboardRight, BlackboardRightSide }
- };
- ToolbarRegistry.Populate(ToolbarHost, slots, Settings?.Toolbar);
+ ToolbarHost = new ToolbarHost(this);
+ var slots = new Dictionary
+ {
+ { ToolbarSlot.FloatingBarMain, StackPanelFloatingBar },
+ { ToolbarSlot.FloatingBarCanvasControls, StackPanelCanvasControls },
+ { ToolbarSlot.FloatingBarEnd, StackPanelFloatingBarEnd },
+ { ToolbarSlot.BlackboardLeft, BlackboardLeftSide },
+ { ToolbarSlot.BlackboardRight, BlackboardRightSide }
+ };
+ ToolbarRegistry.Populate(ToolbarHost, slots, Settings?.Toolbar);
+ LogHelper.WriteLogToFile("MW_Toolbar: InitializeToolbarPlugins 完成", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"MW_Toolbar: InitializeToolbarPlugins 异常: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
+ }
+ }
+
+ internal void RebuildToolbar()
+ {
+ LogHelper.WriteLogToFile("MW_Toolbar: RebuildToolbar 开始", LogHelper.LogType.Info);
+ try
+ {
+ ToolbarRegistry.ClearInjected(StackPanelFloatingBar);
+ ToolbarRegistry.ClearInjected(StackPanelCanvasControls);
+ ToolbarRegistry.ClearInjected(StackPanelFloatingBarEnd);
+ ToolbarRegistry.ClearInjected(BlackboardLeftSide);
+ ToolbarRegistry.ClearInjected(BlackboardRightSide);
+ InitializeToolbarPlugins();
+ LogHelper.WriteLogToFile("MW_Toolbar: RebuildToolbar 完成", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"MW_Toolbar: RebuildToolbar 异常: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml b/Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml
new file mode 100644
index 00000000..821ab6c4
--- /dev/null
+++ b/Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml.cs
new file mode 100644
index 00000000..777bd65a
--- /dev/null
+++ b/Ink Canvas/Windows/SettingsViews/Pages/ToolbarPage.xaml.cs
@@ -0,0 +1,232 @@
+using GongSolutions.Wpf.DragDrop;
+using Ink_Canvas.Controls.Toolbar;
+using Ink_Canvas.Helpers;
+using Ink_Canvas.Windows.SettingsViews.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using Page = iNKORE.UI.WPF.Modern.Controls.Page;
+
+namespace Ink_Canvas.Windows.SettingsViews.Pages
+{
+ public partial class ToolbarPage : Page, IDropTarget
+ {
+ public class ToolbarItemViewModel : INotifyPropertyChanged
+ {
+ public string Id { get; }
+ public string DisplayName { get; }
+
+ private int _order;
+ public int Order { get => _order; set { _order = value; OnPropertyChanged(nameof(Order)); } }
+
+ private bool _isVisible = true;
+ public bool IsVisible { get => _isVisible; set { _isVisible = value; OnPropertyChanged(nameof(IsVisible)); } }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+
+ public ToolbarItemViewModel(string id, string displayName, int order, bool isVisible)
+ {
+ Id = id; DisplayName = displayName; _order = order; _isVisible = isVisible;
+ }
+ }
+
+ private static readonly string LogTag = "ToolbarPage";
+ private bool _isLoaded;
+
+ public ObservableCollection MainItems { get; } = new();
+ public ObservableCollection CanvasItems { get; } = new();
+ public ObservableCollection EndItems { get; } = new();
+
+ public ToolbarPage()
+ {
+ InitializeComponent();
+ DataContext = this;
+ Loaded += OnPageLoaded;
+ }
+
+ private void OnPageLoaded(object sender, RoutedEventArgs e)
+ {
+ try { LoadSettings(); }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"{LogTag}: LoadSettings 异常: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
+ }
+ _isLoaded = true;
+ }
+
+ private void LoadSettings()
+ {
+ LogHelper.WriteLogToFile($"{LogTag}: LoadSettings 开始", LogHelper.LogType.Info);
+ MainItems.Clear(); CanvasItems.Clear(); EndItems.Clear();
+
+ var layout = SettingsManager.Settings?.Toolbar ?? new ToolbarLayoutSettings();
+ IReadOnlyList discoveredItems;
+ try { discoveredItems = ToolbarRegistry.Discover(); }
+ catch (Exception ex) { LogHelper.WriteLogToFile($"{LogTag}: Discover 失败: {ex.Message}", LogHelper.LogType.Error); return; }
+
+ foreach (var item in discoveredItems)
+ {
+ try
+ {
+ if (!layout.Items.TryGetValue(item.Id, out var cfg))
+ {
+ cfg = new ToolbarItemConfig
+ {
+ Visible = item.DefaultVisible,
+ Order = item.DefaultOrder,
+ Slot = item.DefaultSlot,
+ Position = item.DefaultPosition,
+ AnchorName = item.DefaultAnchorName
+ };
+ }
+ string displayName;
+ try { displayName = item.DisplayName ?? item.Id; }
+ catch { displayName = item.Id; }
+
+ var vm = new ToolbarItemViewModel(item.Id, displayName, cfg.Order, cfg.Visible);
+ switch (cfg.Slot)
+ {
+ case ToolbarSlot.FloatingBarMain: MainItems.Add(vm); break;
+ case ToolbarSlot.FloatingBarCanvasControls: CanvasItems.Add(vm); break;
+ case ToolbarSlot.FloatingBarEnd: EndItems.Add(vm); break;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"{LogTag}: 处理条目失败 [{item?.Id}]: {ex.Message}", LogHelper.LogType.Warning);
+ }
+ }
+
+ ReorderCollections();
+ LogHelper.WriteLogToFile($"{LogTag}: LoadSettings 完成 Main={MainItems.Count} Canvas={CanvasItems.Count} End={EndItems.Count}", LogHelper.LogType.Info);
+ }
+
+ private void ReorderCollections()
+ {
+ SortCollection(MainItems);
+ SortCollection(CanvasItems);
+ SortCollection(EndItems);
+ }
+
+ private static void SortCollection(ObservableCollection collection)
+ {
+ if (collection == null) return;
+ var sorted = collection.OrderBy(x => x.Order).ToList();
+ for (int i = 0; i < sorted.Count; i++)
+ {
+ var oldIndex = collection.IndexOf(sorted[i]);
+ if (oldIndex != -1 && oldIndex != i)
+ collection.Move(oldIndex, i);
+ }
+ }
+
+ public new void DragOver(IDropInfo dropInfo)
+ {
+ if (dropInfo.Data is not ToolbarItemViewModel) return;
+ dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
+ dropInfo.Effects = DragDropEffects.Move;
+ }
+
+ public new void Drop(IDropInfo dropInfo)
+ {
+ if (dropInfo.Data is not ToolbarItemViewModel vm) return;
+ if (dropInfo.TargetCollection is not ObservableCollection target) return;
+
+ var oldIndex = target.IndexOf(vm);
+ var newIndex = oldIndex < dropInfo.UnfilteredInsertIndex ? dropInfo.UnfilteredInsertIndex - 1 : dropInfo.UnfilteredInsertIndex;
+ var finalIndex = Math.Min(newIndex >= target.Count ? target.Count - 1 : newIndex, target.Count);
+
+ if (!target.Contains(vm))
+ {
+ if (dropInfo.DragInfo.SourceCollection is ObservableCollection source)
+ source.Remove(vm);
+ target.Insert(dropInfo.UnfilteredInsertIndex, vm);
+ }
+ else if (oldIndex != -1 && oldIndex != finalIndex)
+ {
+ target.Move(oldIndex, finalIndex);
+ }
+
+ UpdateOrdersFromCollection(target);
+ SaveSettings();
+ }
+
+ private static void UpdateOrdersFromCollection(ObservableCollection collection)
+ {
+ for (int i = 0; i < collection.Count; i++)
+ collection[i].Order = (i + 1) * 10;
+ }
+
+ private void SaveSettings()
+ {
+ if (!_isLoaded) return;
+ try
+ {
+ var settings = SettingsManager.Settings;
+ if (settings == null) return;
+ if (settings.Toolbar == null) settings.Toolbar = new ToolbarLayoutSettings();
+ var layout = settings.Toolbar;
+
+ foreach (var vm in MainItems.Concat(CanvasItems).Concat(EndItems))
+ {
+ if (!layout.Items.TryGetValue(vm.Id, out var cfg))
+ {
+ var item = ToolbarRegistry.Discover().FirstOrDefault(i => i.Id == vm.Id);
+ cfg = new ToolbarItemConfig
+ {
+ Visible = item?.DefaultVisible ?? true,
+ Order = item?.DefaultOrder ?? 0,
+ Slot = item?.DefaultSlot ?? ToolbarSlot.FloatingBarMain,
+ Position = item?.DefaultPosition ?? ToolbarInsertPosition.Prepend,
+ AnchorName = item?.DefaultAnchorName
+ };
+ layout.Items[vm.Id] = cfg;
+ }
+ cfg.Visible = vm.IsVisible;
+ cfg.Order = vm.Order;
+ }
+
+ SettingsManager.SaveSettingsToFile();
+ LogHelper.WriteLogToFile($"{LogTag}: 设置已保存", LogHelper.LogType.Info);
+
+ Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ try
+ {
+ var mainWindow = Application.Current.Windows.OfType().FirstOrDefault();
+ mainWindow?.RebuildToolbar();
+ }
+ catch (Exception ex) { LogHelper.WriteLogToFile($"{LogTag}: RebuildToolbar 异常: {ex.Message}", LogHelper.LogType.Error); }
+ }));
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"{LogTag}: SaveSettings 异常: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
+ }
+ }
+
+ private void ButtonReset_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ SettingsManager.Settings?.Toolbar?.Items.Clear();
+ SettingsManager.SaveSettingsToFile();
+ Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ try { Application.Current.Windows.OfType().FirstOrDefault()?.RebuildToolbar(); }
+ catch (Exception ex) { LogHelper.WriteLogToFile($"{LogTag}: Reset Rebuild 异常: {ex.Message}", LogHelper.LogType.Error); }
+ }));
+ LoadSettings();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"{LogTag}: ButtonReset 异常: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
+ }
+ }
+ }
+}
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
index 9ba11761..15a2957e 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
+++ b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
@@ -202,6 +202,15 @@
+
+
+
+
+
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs
index 828cfce0..e7b11cc9 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs
+++ b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs
@@ -45,6 +45,7 @@ namespace Ink_Canvas.Windows.SettingsViews
{ "WindowPage", typeof(WindowPage) },
{ "AppearancePage", typeof(AppearancePage) },
{ "HotkeyPage", typeof(HotkeyPage) },
+ { "ToolbarPage", typeof(ToolbarPage) },
{ "UpdatePage", typeof(UpdatePage) },
{ "ExperimentalPage", typeof(ExperimentalPage) },
{ "AdvancedPage", typeof(AdvancedPage) },
@@ -312,8 +313,10 @@ namespace Ink_Canvas.Windows.SettingsViews
if (!_pages.TryGetValue(pageTag, out var cachedPage))
{
+ Ink_Canvas.Helpers.LogHelper.WriteLogToFile($"SettingsWindow: 创建页面实例 {pageTag} ({pageType.Name})", Ink_Canvas.Helpers.LogHelper.LogType.Info);
cachedPage = Activator.CreateInstance(pageType);
_pages.Add(pageTag, cachedPage);
+ Ink_Canvas.Helpers.LogHelper.WriteLogToFile($"SettingsWindow: 页面实例 {pageTag} 创建成功", Ink_Canvas.Helpers.LogHelper.LogType.Info);
}
if (cachedPage is PluginSettingsPage pluginSettingsPage && pluginInfo != null)
@@ -325,6 +328,7 @@ namespace Ink_Canvas.Windows.SettingsViews
}
catch (Exception ex)
{
+ Ink_Canvas.Helpers.LogHelper.WriteLogToFile($"SettingsWindow: 导航到 {pageTag} 异常: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", Ink_Canvas.Helpers.LogHelper.LogType.Error);
MessageBox.Show($"导航到页面时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally