diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index e891e6b1..45e8cbc3 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -528,13 +528,13 @@ jobs: - name: Create enhanced changelog with file table id: enhanced_changelog run: | - version="${{ needs.prepare.outputs.version }}" + version='${{ needs.prepare.outputs.version }}' # 读取git-cliff生成的changelog内容 - originalChangelog="${{ needs.prepare.outputs.changelog }}" + originalChangelog='${{ needs.prepare.outputs.changelog }}' # 检查是否为预发布版本,如果是则添加警告提示 - if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then + if [ '${{ needs.prepare.outputs.is_prerelease }}' = "true" ]; then warningText=$'\n> [!CAUTION]\n' warningText+=$'> **注意:此版本为预览或测试版**\n' warningText+=$'> \n' @@ -732,6 +732,7 @@ jobs: InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip fail_on_unmatched_files: false repository: "InkCanvasForClass/community-beta" + token: ${{ steps.octo-sts-beta.outputs.token }} env: GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }} diff --git a/Ink Canvas.sln b/Ink Canvas.sln index 0daac4d2..9e46f5cd 100644 --- a/Ink Canvas.sln +++ b/Ink Canvas.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33530.505 diff --git a/Ink Canvas/App.xaml b/Ink Canvas/App.xaml index 21f1bfa2..5ce16684 100644 --- a/Ink Canvas/App.xaml +++ b/Ink Canvas/App.xaml @@ -1,8 +1,8 @@ - @@ -32,6 +32,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 787cbc0b..2238c408 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Ink_Canvas.Properties; using iNKORE.UI.WPF.Modern.Controls; @@ -1061,6 +1061,7 @@ namespace Ink_Canvas } _taskbar = (TaskbarIcon)FindResource("TaskbarTrayIcon"); + _taskbar.ForceCreate(); StartArgs = e.Args; diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs index a2a1be72..affa789e 100644 --- a/Ink Canvas/AssemblyInfo.cs +++ b/Ink Canvas/AssemblyInfo.cs @@ -43,5 +43,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.18.8")] -[assembly: AssemblyFileVersion("1.7.18.8")] +[assembly: AssemblyVersion("1.7.18.9")] +[assembly: AssemblyFileVersion("1.7.18.9")] diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/AutoUpdateHelper.cs index 92fdcca8..0b96e22d 100644 --- a/Ink Canvas/Helpers/AutoUpdateHelper.cs +++ b/Ink Canvas/Helpers/AutoUpdateHelper.cs @@ -1436,7 +1436,7 @@ namespace Ink_Canvas.Helpers /// /// /// 该方法会临时将 App.IsUpdateInstalling 置为 true、尝试关闭进程保护(并在结束时还原)、在必要时备份当前设置、解压更新 ZIP、启动解压后的新可执行文件(以更新模式传递旧进程 ID、解压路径和目标路径等参数),并在新进程启动后关闭当前进程。方法会记录日志并在遇到错误时安全退出相应步骤,但不会抛出异常给调用方以外的上下文。 - /// 要安装的版本号,用于定位更新包文件名(例如 InkCanvasForClass.CE.{version}.zip)。 + /// 要安装的版本号,用于定位更新包文件名(与 一致;选择 x64 包时为 InkCanvasForClass.CE.{version}-x64.zip)。 /// 指示是否以静默模式启动新版本(影响传递给新进程的参数和可能的用户提示)。 public static void InstallNewVersionApp(string version, bool isInSilence) { @@ -1487,7 +1487,7 @@ namespace Ink_Canvas.Helpers LogHelper.WriteLogToFile($"更新前自动备份设置时出错: {ex.Message}", LogHelper.LogType.Error); } - string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip"); + string zipFilePath = GetLocalUpdateZipFilePath(version); LogHelper.WriteLogToFile($"AutoUpdate | 检查ZIP文件: {zipFilePath}"); if (!File.Exists(zipFilePath)) diff --git a/Ink Canvas/Helpers/InkRecognitionManager.cs b/Ink Canvas/Helpers/InkRecognitionManager.cs index 19be366f..69c86b32 100644 --- a/Ink Canvas/Helpers/InkRecognitionManager.cs +++ b/Ink Canvas/Helpers/InkRecognitionManager.cs @@ -100,19 +100,68 @@ namespace Ink_Canvas.Helpers return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true); } + /// 为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。 + /// 逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。 public Task CorrectInkAsync( StrokeCollection strokes, - ShapeRecognitionEngineMode mode) + ShapeRecognitionEngineMode mode, + bool applyHandwritingBeautify = false, + string handwritingFontFamilyList = null) { - if (!_isInitialized || strokes == null || strokes.Count == 0) + if (!_isInitialized) + { + LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info); return Task.FromResult(strokes); + } + + if (strokes == null || strokes.Count == 0) + { + LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info); + return Task.FromResult(strokes); + } try { - if (ShapeRecognitionRouter.ResolveUseWinRt(mode) && _modernAnalyzer != null) - return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes); + var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode); + if (!applyHandwritingBeautify) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" + + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } - return Task.FromResult(strokes); + if (!useWinRt) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } + + if (!Environment.Is64BitProcess) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:非 64 位进程,WinRT 手写体替换不可用。笔画数=" + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } + + if (_modernAnalyzer == null) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:ModernInkAnalyzer 未就绪(WinRT 初始化失败?)。笔画数=" + + strokes.Count, + LogHelper.LogType.Warning); + return Task.FromResult(strokes); + } + + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count + + ",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()), + LogHelper.LogType.Info); + return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes, handwritingFontFamilyList); } catch (Exception ex) { @@ -121,6 +170,32 @@ namespace Ink_Canvas.Helpers } } + /// + /// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。 + /// + public Task RecognizeHandwritingAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) + { + if (!_isInitialized || strokes == null || strokes.Count == 0) + return Task.FromResult(HandwritingRecognitionResult.Empty); + + try + { + if (!Environment.Is64BitProcess + || !ShapeRecognitionRouter.ResolveUseWinRt(mode) + || !WinRtHandwritingRecognizer.IsApiAvailable) + return Task.FromResult(HandwritingRecognitionResult.Empty); + + return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error); + return Task.FromResult(HandwritingRecognitionResult.Empty); + } + } + public bool IsValidShapeType(string shapeName) { return !string.IsNullOrEmpty(shapeName) @@ -166,9 +241,13 @@ namespace Ink_Canvas.Helpers internal sealed class ModernInkAnalyzer : IDisposable { - public Task AnalyzeAndCorrectAsync(StrokeCollection strokes) + public Task AnalyzeAndCorrectAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) { - return Task.FromResult(strokes); + return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync( + strokes, + handwritingFontFamilyList); } public void Dispose() diff --git a/Ink Canvas/Helpers/InkRecognizeHelper.cs b/Ink Canvas/Helpers/InkRecognizeHelper.cs index 34f5cfe6..5855fd9f 100644 --- a/Ink Canvas/Helpers/InkRecognizeHelper.cs +++ b/Ink Canvas/Helpers/InkRecognizeHelper.cs @@ -101,7 +101,10 @@ namespace Ink_Canvas.Helpers { _ = InkRecognitionManager.Instance; if (ShapeRecognitionRouter.ResolveUseWinRt(mode)) + { WinRtInkShapeRecognizer.Warmup(); + WinRtHandwritingRecognizer.Warmup(); + } else RecognizeShapeIACore(new StrokeCollection()); } @@ -111,6 +114,33 @@ namespace Ink_Canvas.Helpers } } + /// WinRT 手写识别(64 位 + Windows 10+)。 + public static Task RecognizeHandwritingUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) => + InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode); + + /// WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。 + public static Task CorrectHandwritingStrokesUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) => + InkRecognitionManager.Instance.CorrectInkAsync( + strokes, + mode, + MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false, + MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily); + + /// 显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。 + public static Task CorrectHandwritingStrokesUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode, + bool applyHandwritingBeautify) => + InkRecognitionManager.Instance.CorrectInkAsync( + strokes, + mode, + applyHandwritingBeautify, + MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily); + internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy) { if (legacy?.InkDrawingNode == null) diff --git a/Ink Canvas/Helpers/MultiTouchInput.cs b/Ink Canvas/Helpers/MultiTouchInput.cs index 40391fc0..f1a745b1 100644 --- a/Ink Canvas/Helpers/MultiTouchInput.cs +++ b/Ink Canvas/Helpers/MultiTouchInput.cs @@ -111,6 +111,14 @@ namespace Ink_Canvas.Helpers /// /// 绘制点段到新的DrawingVisual /// + private static double PressureToVisualScale(float pressureFactor, bool ignorePressure) + { + if (ignorePressure) + return 1.0; + // 与 WPF 墨迹观感接近:0.5 为标称,压低变细、抬高变粗(预览此前固定 Pen 宽,等同忽略压感) + return Math.Max(0.22, Math.Min(2.1, 0.42 + 1.16 * pressureFactor)); + } + private void DrawSegmentToNewVisual(int startIndex, int endIndex) { if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return; @@ -118,6 +126,7 @@ namespace Ink_Canvas.Helpers var points = Stroke.StylusPoints; var drawingAttributes = Stroke.DrawingAttributes; + var ignorePressure = drawingAttributes.IgnorePressure; // 创建新的DrawingVisual用于绘制这个点段 var segmentVisual = new DrawingVisual(); @@ -128,11 +137,6 @@ namespace Ink_Canvas.Helpers using (var dc = segmentVisual.RenderOpen()) { - var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), drawingAttributes.Width); - pen.StartLineCap = PenLineCap.Round; - pen.EndLineCap = PenLineCap.Round; - pen.LineJoin = PenLineJoin.Round; - // 绘制指定范围内的点段 if (endIndex - startIndex >= 2) { @@ -141,6 +145,15 @@ namespace Ink_Canvas.Helpers { var startPoint = new Point(points[i].X, points[i].Y); var endPoint = new Point(points[i + 1].X, points[i + 1].Y); + var s0 = PressureToVisualScale(points[i].PressureFactor, ignorePressure); + var s1 = PressureToVisualScale(points[i + 1].PressureFactor, ignorePressure); + var thickness = Math.Max(0.35, (drawingAttributes.Width * s0 + drawingAttributes.Width * s1) / 2.0); + var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), thickness) + { + StartLineCap = PenLineCap.Round, + EndLineCap = PenLineCap.Round, + LineJoin = PenLineJoin.Round + }; dc.DrawLine(pen, startPoint, endPoint); } } @@ -149,8 +162,9 @@ namespace Ink_Canvas.Helpers // 只有一个点,绘制圆点 var brush = new SolidColorBrush(drawingAttributes.Color); var point = points[startIndex]; + var s = PressureToVisualScale(point.PressureFactor, ignorePressure); dc.DrawEllipse(brush, null, new Point(point.X, point.Y), - drawingAttributes.Width / 2, drawingAttributes.Height / 2); + drawingAttributes.Width * s / 2, drawingAttributes.Height * s / 2); } } diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherButton.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherButton.cs deleted file mode 100644 index 479387db..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherButton.cs +++ /dev/null @@ -1,274 +0,0 @@ -using iNKORE.UI.WPF.Controls; -using System; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// 启动台按钮控件 - /// - public class LauncherButton - { - /// - /// 父插件 - /// - private readonly SuperLauncherPlugin _plugin; - - /// - /// 实际按钮控件 - /// - private readonly SimpleStackPanel _panel; - - /// - /// 获取按钮UI元素 - /// - public UIElement Element => _panel; - - /// - /// 构造函数 - /// - /// 父插件 - public LauncherButton(SuperLauncherPlugin plugin) - { - try - { - _plugin = plugin; - LogHelper.WriteLogToFile("开始创建启动台按钮"); - - // 创建SimpleStackPanel - _panel = new SimpleStackPanel - { - Name = "Launcher_Icon", - Orientation = Orientation.Vertical, - HorizontalAlignment = HorizontalAlignment.Center, - Width = 28, - Margin = new Thickness(0, -2, 0, 0), - Background = Brushes.Transparent - }; - - LogHelper.WriteLogToFile("创建SimpleStackPanel完成"); - - // 添加图标 - var image = CreateIconImage(); - _panel.Children.Add(image); - - // 添加文本 - TextBlock textBlock = new TextBlock - { - Text = "启动台", - Foreground = Brushes.Black, - FontSize = 8, - Margin = new Thickness(0, 1, 0, 0), - TextAlignment = TextAlignment.Center - }; - _panel.Children.Add(textBlock); - - // 设置鼠标事件 - _panel.MouseDown += Panel_MouseDown; - _panel.MouseUp += Panel_MouseUp; - _panel.MouseLeave += Panel_MouseLeave; - - // 右键菜单支持 - _panel.ContextMenu = CreateContextMenu(); - - // 设置工具提示 - _panel.ToolTip = "启动台"; - - LogHelper.WriteLogToFile("启动台按钮创建完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 创建右键菜单 - /// - private ContextMenu CreateContextMenu() - { - try - { - // 创建菜单 - ContextMenu menu = new ContextMenu(); - - // 创建位置切换菜单项 - MenuItem positionMenuItem = new MenuItem(); - positionMenuItem.Header = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ? - "移至右侧" : "移至左侧"; - positionMenuItem.Click += (s, e) => - { - // 切换位置 - _plugin.Config.ButtonPosition = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ? - LauncherButtonPosition.Right : LauncherButtonPosition.Left; - - // 更新按钮位置 - _plugin.UpdateButtonPosition(); - - // 保存配置 - _plugin.SaveConfig(); - - LogHelper.WriteLogToFile($"通过右键菜单切换启动台按钮位置为: {_plugin.Config.ButtonPosition}"); - }; - menu.Items.Add(positionMenuItem); - - // 添加设置菜单项 - MenuItem settingsMenuItem = new MenuItem(); - settingsMenuItem.Header = "打开设置"; - settingsMenuItem.Click += (s, e) => - { - // 打开插件设置窗口 - var mainWindow = Application.Current.MainWindow; - if (mainWindow != null) - { - try - { - // 使用反射调用主窗口的ShowPluginSettings方法 - var method = mainWindow.GetType().GetMethod("ShowPluginSettings"); - if (method != null) - { - method.Invoke(mainWindow, null); - LogHelper.WriteLogToFile("已打开插件设置窗口"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"打开插件设置窗口失败: {ex.Message}", LogHelper.LogType.Error); - } - } - }; - menu.Items.Add(settingsMenuItem); - - return menu; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建右键菜单时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 获取实际的UI元素 - /// - [Obsolete("使用Element属性代替")] - public UIElement GetUIElement() - { - return _panel; - } - - /// - /// 创建图标图像 - /// - private Image CreateIconImage() - { - try - { - // 创建图像 - Image image = new Image - { - Height = 17, - Margin = new Thickness(0, 3, 0, 0) - }; - - // 设置位图缩放模式 - RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); - - // 创建绘图图像 - DrawingImage drawingImage = new DrawingImage(); - DrawingGroup drawingGroup = new DrawingGroup(); - drawingGroup.ClipGeometry = Geometry.Parse("M0,0 V24 H24 V0 H0 Z"); - - // 使用提供的应用网格图标 - GeometryDrawing geometryDrawing = new GeometryDrawing - { - Brush = new SolidColorBrush(Color.FromRgb(0x1B, 0x1B, 0x1B)), - Geometry = Geometry.Parse("F0 M24,24z M0,0z M4.41721,4.29873C4.35178,4.29873,4.29873,4.35178,4.29873,4.41721L4.29873,9.15646C4.29873,9.22189,4.35178,9.27494,4.41721,9.27494L9.15646,9.27494C9.22189,9.27494,9.27494,9.22189,9.27494,9.15646L9.27494,4.41721C9.27494,4.35178,9.22189,4.29873,9.15646,4.29873L4.41721,4.29873z M2.64,4.41721C2.64,3.43569,3.43569,2.64,4.41721,2.64L9.15646,2.64C10.138,2.64,10.9337,3.43569,10.9337,4.41721L10.9337,9.15646C10.9337,10.138,10.138,10.9337,9.15646,10.9337L4.41721,10.9337C3.43569,10.9337,2.64,10.138,2.64,9.15646L2.64,4.41721z M14.8435,4.29873C14.7781,4.29873,14.7251,4.35178,14.7251,4.41721L14.7251,9.15646C14.7251,9.22189,14.7781,9.27494,14.8435,9.27494L19.5828,9.27494C19.6482,9.27494,19.7013,9.22189,19.7013,9.15646L19.7013,4.41721C19.7013,4.35178,19.6482,4.29873,19.5828,4.29873L14.8435,4.29873z M13.0663,4.41721C13.0663,3.43569,13.862,2.64,14.8435,2.64L19.5828,2.64C20.5643,2.64,21.36,3.43569,21.36,4.41721L21.36,9.15646C21.36,10.138,20.5643,10.9337,19.5828,10.9337L14.8435,10.9337C13.862,10.9337,13.0663,10.138,13.0663,9.15646L13.0663,4.41721z M14.8435,14.7251C14.7781,14.7251,14.7251,14.7781,14.7251,14.8435L14.7251,19.5828C14.7251,19.6482,14.7781,19.7013,14.8435,19.7013L19.5828,19.7013C19.6482,19.7013,19.7013,19.6482,19.7013,19.5828L19.7013,14.8435C19.7013,14.7781,19.6482,14.7251,19.5828,14.7251L14.8435,14.7251z M13.0663,14.8435C13.0663,13.862,13.862,13.0663,14.8435,13.0663L19.5828,13.0663C20.5643,13.0663,21.36,13.862,21.36,14.8435L21.36,19.5828C21.36,20.5643,20.5643,21.36,19.5828,21.36L14.8435,21.36C13.862,21.36,13.0663,20.5643,13.0663,19.5828L13.0663,14.8435z M4.41721,14.7251C4.35178,14.7251,4.29873,14.7781,4.29873,14.8435L4.29873,19.5828C4.29873,19.6482,4.35178,19.7013,4.41721,19.7013L9.15646,19.7013C9.22189,19.7013,9.27494,19.6482,9.27494,19.5828L9.27494,14.8435C9.27494,14.7781,9.22189,14.7251,9.15646,14.7251L4.41721,14.7251z M2.64,14.8435C2.64,13.862,3.43569,13.0663,4.41721,13.0663L9.15646,13.0663C10.138,13.0663,10.9337,13.862,10.9337,14.8435L10.9337,19.5828C10.9337,20.5643,10.138,21.36,9.15646,21.36L4.41721,21.36C3.43569,21.36,2.64,20.5643,2.64,19.5828L2.64,14.8435z") - }; - - drawingGroup.Children.Add(geometryDrawing); - - // 设置图像源 - drawingImage.Drawing = drawingGroup; - image.Source = drawingImage; - - return image; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建图标图像时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - - // 返回一个空图像 - return new Image(); - } - } - - /// - /// 鼠标按下事件 - /// - private void Panel_MouseDown(object sender, MouseButtonEventArgs e) - { - try - { - // 提供反馈 - _panel.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0)); - LogHelper.WriteLogToFile("启动台按钮鼠标按下"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动台按钮鼠标按下事件出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 鼠标抬起事件 - /// - private void Panel_MouseUp(object sender, MouseButtonEventArgs e) - { - try - { - // 只有左键点击才显示启动台窗口 - if (e.ChangedButton != MouseButton.Left) - { - return; - } - - // 恢复背景 - _panel.Background = Brushes.Transparent; - LogHelper.WriteLogToFile("启动台按钮鼠标抬起,准备显示启动台窗口"); - - // 获取按钮在屏幕上的位置 - Point buttonPosition = _panel.PointToScreen(new Point(_panel.ActualWidth / 2, 0)); - - // 显示启动台窗口 - _plugin.ShowLauncherWindow(buttonPosition); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动台按钮鼠标抬起事件出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 鼠标离开事件 - /// - private void Panel_MouseLeave(object sender, MouseEventArgs e) - { - try - { - // 恢复背景 - _panel.Background = Brushes.Transparent; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动台按钮鼠标离开事件出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherModels.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherModels.cs deleted file mode 100644 index 9b6ac63b..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherModels.cs +++ /dev/null @@ -1,332 +0,0 @@ -using Microsoft.Win32; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Interop; -using System.Windows.Media; -using System.Windows.Media.Imaging; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// 启动台按钮位置 - /// - public enum LauncherButtonPosition - { - /// - /// 左侧 - /// - Left, - - /// - /// 右侧 - /// - Right - } - - /// - /// 启动台配置 - /// - public class LauncherConfig - { - /// - /// 启动台按钮位置 - /// - public LauncherButtonPosition ButtonPosition { get; set; } = LauncherButtonPosition.Right; - - /// - /// 启动台应用程序列表 - /// - public List Items { get; set; } = new List(); - } - - /// - /// 启动台应用项 - /// - public class LauncherItem - { - /// - /// 应用程序名称 - /// - public string Name { get; set; } - - /// - /// 应用程序路径 - /// - public string Path { get; set; } - - /// - /// 是否可见 - /// - public bool IsVisible { get; set; } = true; - - /// - /// 在启动台中的位置(0-39) - /// - public int Position { get; set; } = -1; - - /// - /// 是否已固定位置 - /// - public bool IsPositionFixed { get; set; } = false; - - /// - /// 图标缓存 - /// - [JsonIgnore] - private ImageSource _iconCache; - - /// - /// 获取应用程序图标 - /// - [JsonIgnore] - public ImageSource Icon - { - get - { - if (_iconCache != null) - { - return _iconCache; - } - - try - { - if (File.Exists(Path)) - { - // 从文件中获取图标 - Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(Path); - if (icon != null) - { - _iconCache = Imaging.CreateBitmapSourceFromHIcon( - icon.Handle, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions()); - - icon.Dispose(); - return _iconCache; - } - } - else - { - // 从注册表中获取文件类型关联图标 - string extension = System.IO.Path.GetExtension(Path); - if (!string.IsNullOrEmpty(extension)) - { - string fileType = Registry.ClassesRoot.OpenSubKey(extension)?.GetValue(string.Empty) as string; - if (!string.IsNullOrEmpty(fileType)) - { - string iconPath = Registry.ClassesRoot.OpenSubKey(fileType + "\\DefaultIcon")?.GetValue(string.Empty) as string; - if (!string.IsNullOrEmpty(iconPath)) - { - string[] parts = iconPath.Split(','); - string iconFile = parts[0].Trim('"'); - int iconIndex = parts.Length > 1 ? Convert.ToInt32(parts[1]) : 0; - - if (File.Exists(iconFile)) - { - Icon icon = IconExtractor.Extract(iconFile, iconIndex, true); - if (icon != null) - { - _iconCache = Imaging.CreateBitmapSourceFromHIcon( - icon.Handle, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions()); - - icon.Dispose(); - return _iconCache; - } - } - } - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取应用图标时出错: {ex.Message}", LogHelper.LogType.Error); - } - - // 返回默认图标 - return GetDefaultIcon(); - } - } - - /// - /// 获取默认图标 - /// - private ImageSource GetDefaultIcon() - { - try - { - // 对于资源管理器,使用特定图标 - if (Path.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase)) - { - try - { - // 直接从C:\Windows\explorer.exe获取图标 - string explorerPath = @"C:\Windows\explorer.exe"; - if (File.Exists(explorerPath)) - { - Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(explorerPath); - if (icon != null) - { - _iconCache = Imaging.CreateBitmapSourceFromHIcon( - icon.Handle, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions()); - - icon.Dispose(); - return _iconCache; - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取资源管理器图标时出错: {ex.Message}", LogHelper.LogType.Warning); - // 如果获取Windows图标失败,回退到默认图标 - } - - // 回退到备用图标 - string explorerIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_folder_24_regular.png"); - if (File.Exists(explorerIconPath)) - { - Uri uri = new Uri(explorerIconPath); - BitmapImage image = new BitmapImage(uri); - _iconCache = image; - return _iconCache; - } - } - - // 返回一个简单的默认图标 - string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-png", "icc.png"); - if (File.Exists(iconPath)) - { - Uri uri = new Uri(iconPath); - BitmapImage image = new BitmapImage(uri); - _iconCache = image; - return _iconCache; - } - - // 如果还是没有找到,尝试使用应用程序图标 - string appIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_apps_24_regular.png"); - if (File.Exists(appIconPath)) - { - Uri uri = new Uri(appIconPath); - BitmapImage image = new BitmapImage(uri); - _iconCache = image; - return _iconCache; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取默认图标时出错: {ex.Message}", LogHelper.LogType.Error); - } - - return null; - } - - /// - /// 启动应用程序 - /// - public void Launch() - { - try - { - if (string.IsNullOrEmpty(Path)) - { - LogHelper.WriteLogToFile("无法启动应用程序:路径为空", LogHelper.LogType.Error); - return; - } - - // 检查文件是否存在 - if (!File.Exists(Path) && !Path.Contains(":\\")) - { - // 可能是系统命令,如explorer.exe - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = Path, - UseShellExecute = true - }; - Process.Start(psi); - } - else - { - // 使用Process.Start启动应用程序 - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = Path, - UseShellExecute = true - }; - Process.Start(psi); - } - - LogHelper.WriteLogToFile($"已启动应用程序: {Path}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动应用程序时出错: {ex.Message}", LogHelper.LogType.Error); - MessageBox.Show($"启动应用程序时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - } - - /// - /// 图标提取工具类 - /// - public static class IconExtractor - { - /// - /// 从文件中提取图标 - /// - /// 文件路径 - /// 图标索引 - /// 是否提取大图标 - /// 提取的图标 - public static Icon Extract(string file, int index, bool largeIcon) - { - try - { - IntPtr large; - IntPtr small; - ExtractIconEx(file, index, out large, out small, 1); - - try - { - return Icon.FromHandle(largeIcon ? large : small); - } - catch - { - return null; - } - finally - { - if (large != IntPtr.Zero) - DestroyIcon(large); - - if (small != IntPtr.Zero) - DestroyIcon(small); - } - } - catch - { - return null; - } - } - - [DllImport("Shell32.dll", EntryPoint = "ExtractIconEx")] - private static extern int ExtractIconEx( - [MarshalAs(UnmanagedType.LPStr)] string lpszFile, - int nIconIndex, - out IntPtr phiconLarge, - out IntPtr phiconSmall, - int nIcons); - - [DllImport("User32.dll")] - private static extern int DestroyIcon(IntPtr hIcon); - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml deleted file mode 100644 index dde23f42..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs deleted file mode 100644 index 6ca80e90..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Threading; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// LauncherWindow.xaml 的交互逻辑 - /// - public partial class LauncherWindow : Window - { - /// - /// 父插件 - /// - private readonly SuperLauncherPlugin _plugin; - - /// - /// 是否处于固定模式 - /// - private bool _isFixMode; - - /// - /// 应用项按钮列表 - /// - private readonly Dictionary _appButtons = new Dictionary(); - - /// - /// 拖拽中的按钮 - /// - private Button _draggingButton; - - /// - /// 拖拽开始位置 - /// - private Point _dragStartPoint; - - /// - /// 构造函数 - /// - public LauncherWindow(SuperLauncherPlugin plugin) - { - InitializeComponent(); - - _plugin = plugin; - - // 加载应用项 - LoadLauncherItems(); - - // 添加鼠标按下事件(用于拖动窗口) - MouseDown += (s, e) => - { - if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed) - { - DragMove(); - } - }; - - // 根据应用数量调整窗口大小 - AdjustWindowSize(); - } - - /// - /// 加载启动台应用项 - /// - private void LoadLauncherItems() - { - // 清空现有应用项 - AppPanel.Children.Clear(); - _appButtons.Clear(); - - // 获取显示的应用项 - var visibleItems = _plugin.LauncherItems - .Where(item => item.IsVisible) - .OrderBy(item => item.Position) - .ToList(); - - foreach (var item in visibleItems) - { - // 创建应用按钮 - Button appButton = new Button - { - Style = (Style)FindResource("LauncherItemStyle"), - DataContext = item, - Tag = item.Position - }; - - // 添加点击事件 - appButton.Click += AppButton_Click; - - // 在固定模式下,添加拖拽事件 - appButton.PreviewMouseDown += AppButton_PreviewMouseDown; - appButton.PreviewMouseMove += AppButton_PreviewMouseMove; - appButton.PreviewMouseUp += AppButton_PreviewMouseUp; - - // 记录按钮和项目的对应关系 - _appButtons.Add(appButton, item); - - // 添加到面板 - AppPanel.Children.Add(appButton); - } - } - - /// - /// 根据应用数量调整窗口大小 - /// - private void AdjustWindowSize() - { - try - { - // 每行最多显示4个应用 - const int appsPerRow = 4; - - // 计算行数 - int visibleCount = _appButtons.Count; - int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow); - - // 设置窗口宽度(每个应用90像素宽 = 80 + 5*2) - Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400 - - // 设置窗口高度(每个应用90像素高 = 80 + 5*2) - Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20 - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 应用按钮点击事件 - /// - private void AppButton_Click(object sender, RoutedEventArgs e) - { - try - { - if (_isFixMode) return; // 在固定模式下,不响应点击事件 - - if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item)) - { - // 获取应用路径和名称,用于后续启动 - string appPath = item.Path; - string appName = item.Name; - - LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}"); - - // 首先标记窗口正在关闭 - IsClosing = true; - - // 创建一个应用启动任务 - var launchTask = new Task(() => - { - try - { - // 等待一段时间,确保窗口关闭流程已经开始 - Thread.Sleep(200); - - // 使用UI线程启动应用 - Application.Current.Dispatcher.Invoke(() => - { - try - { - // 检查应用路径是否存在 - if (File.Exists(appPath) || !appPath.Contains(":\\")) - { - // 创建进程启动信息 - var psi = new ProcessStartInfo - { - FileName = appPath, - UseShellExecute = true, - }; - - // 启动应用程序 - var process = Process.Start(psi); - LogHelper.WriteLogToFile($"应用程序 {appName} 已启动"); - } - else - { - LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error); - MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error); - MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - }); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error); - } - }); - - // 关闭窗口 - try - { - Dispatcher.BeginInvoke(new Action(() => - { - try { Close(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - // 启动应用程序任务 - launchTask.Start(); - }), DispatcherPriority.Background); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error); - // 如果无法通过UI关闭窗口,直接启动任务 - launchTask.Start(); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error); - try { IsClosing = true; Close(); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); } - } - } - - #region 固定模式拖拽事件 - - /// - /// 应用按钮鼠标按下事件 - /// - private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e) - { - if (!_isFixMode) return; - - if (e.ChangedButton == MouseButton.Left && sender is Button button) - { - _draggingButton = button; - _dragStartPoint = e.GetPosition(AppPanel); - button.CaptureMouse(); - button.Opacity = 0.7; - - // 阻止事件冒泡,以避免触发按钮点击 - e.Handled = true; - } - } - - /// - /// 应用按钮鼠标移动事件 - /// - private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e) - { - if (!_isFixMode || _draggingButton == null) return; - - if (e.LeftButton == MouseButtonState.Pressed) - { - Point currentPosition = e.GetPosition(AppPanel); - - // 移动按钮 - System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2); - System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2); - - // 将按钮移到最上层 - Panel.SetZIndex(_draggingButton, 100); - - // 阻止事件冒泡 - e.Handled = true; - } - } - - /// - /// 应用按钮鼠标释放事件 - /// - private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e) - { - if (!_isFixMode || _draggingButton == null) return; - - // 释放鼠标捕获 - _draggingButton.ReleaseMouseCapture(); - - // 计算新位置 - Point releasePoint = e.GetPosition(AppPanel); - int newPosition = CalculateGridPosition(releasePoint); - - // 获取当前项目 - LauncherItem currentItem = _appButtons[_draggingButton]; - - // 重新排序 - ReorderItems(currentItem, newPosition); - - // 重新加载应用项 - LoadLauncherItems(); - - // 保存配置 - _plugin.SaveConfig(); - - // 清除拖拽状态 - _draggingButton.Opacity = 1; - Panel.SetZIndex(_draggingButton, 0); - _draggingButton = null; - - // 阻止事件冒泡 - e.Handled = true; - } - - /// - /// 计算网格位置 - /// - private int CalculateGridPosition(Point point) - { - // 计算行和列 - int columnCount = 4; // 每行最多4个应用 - int columnWidth = 90; // 应用宽度(包括边距) - int rowHeight = 90; // 应用高度(包括边距) - - int column = (int)(point.X / columnWidth); - int row = (int)(point.Y / rowHeight); - - // 确保在有效范围内 - column = Math.Max(0, Math.Min(column, columnCount - 1)); - row = Math.Max(0, row); - - // 计算位置索引 - return row * columnCount + column; - } - - /// - /// 重新排序应用项 - /// - private void ReorderItems(LauncherItem item, int newPosition) - { - try - { - // 设置项目为固定位置 - item.IsPositionFixed = true; - - // 如果位置相同,无需调整 - if (item.Position == newPosition) - { - return; - } - - // 获取所有可见项目 - var visibleItems = _plugin.LauncherItems - .Where(i => i.IsVisible) - .OrderBy(i => i.Position) - .ToList(); - - // 移除当前项目 - visibleItems.Remove(item); - - // 查找插入位置 - int insertIndex = 0; - for (int i = 0; i < visibleItems.Count; i++) - { - if (visibleItems[i].Position >= newPosition) - { - insertIndex = i; - break; - } - insertIndex = i + 1; - } - - // 插入项目 - visibleItems.Insert(insertIndex, item); - - // 重新分配位置 - for (int i = 0; i < visibleItems.Count; i++) - { - visibleItems[i].Position = i; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 窗口事件处理 - - /// - /// 窗口失去焦点事件 - /// - private void Window_Deactivated(object sender, EventArgs e) - { - try - { - // 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口 - if (!_isFixMode && IsLoaded && !IsClosing) - { - // 标记为正在关闭 - IsClosing = true; - - // 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突 - Dispatcher.BeginInvoke(new Action(() => - { - try - { - // 再次检查窗口状态 - if (IsLoaded && !IsClosing) - { - Close(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - }), DispatcherPriority.Background); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 窗口是否正在关闭 - /// - private bool IsClosing { get; set; } - - /// - /// 重写OnClosing方法,标记窗口正在关闭 - /// - protected override void OnClosing(CancelEventArgs e) - { - IsClosing = true; - base.OnClosing(e); - } - - /// - /// 关闭按钮点击事件 - /// - private void BtnClose_Click(object sender, RoutedEventArgs e) - { - Close(); - } - - /// - /// 固定模式按钮点击事件 - /// - private void BtnFixMode_Click(object sender, RoutedEventArgs e) - { - // 切换固定模式 - _isFixMode = !_isFixMode; - - // 更新固定模式按钮图标颜色 - FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White; - - // 显示提示 - if (_isFixMode) - { - MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs deleted file mode 100644 index 555c7bf9..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs +++ /dev/null @@ -1,589 +0,0 @@ -using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn -{ - /// - /// 超级启动台插件 - /// - public class SuperLauncherPlugin : PluginBase - { - #region 插件基本信息 - - public override string Name => "超级启动台"; - - public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。"; - - public override Version Version => new Version(1, 0, 1); - - public override string Author => "ICC CE 团队"; - - public override bool IsBuiltIn => true; - - #endregion - - #region 插件属性和字段 - - /// - /// 启动台配置 - /// - public LauncherConfig Config { get; private set; } - - /// - /// 启动台应用程序列表 - /// - public ObservableCollection LauncherItems { get; private set; } - - /// - /// 启动台按钮 - /// - private LauncherButton _launcherButton; - - /// - /// 启动台窗口 - /// - private LauncherWindow _launcherWindow; - - /// - /// 配置文件路径 - /// - private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json"); - - /// - /// 标记是否已添加到浮动栏 - /// - private bool _isAddedToFloatingBar; - - #endregion - - #region 插件生命周期 - - public override void Initialize() - { - try - { - base.Initialize(); - - // 创建配置目录 - string configDir = Path.Combine(App.RootPath, "PluginConfigs"); - if (!Directory.Exists(configDir)) - { - Directory.CreateDirectory(configDir); - } - - // 加载配置 - LoadConfig(); - - LogHelper.WriteLogToFile("超级启动台插件已初始化"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - public override void Enable() - { - try - { - if (IsEnabled) return; // 防止重复启用 - - // 创建启动台按钮 - if (_launcherButton == null) - { - _launcherButton = new LauncherButton(this); - LogHelper.WriteLogToFile("超级启动台按钮已创建"); - } - - // 添加启动台按钮到浮动栏 - AddLauncherButtonToFloatingBar(); - - // 设置启用状态 - base.Enable(); - - // 保存插件配置 - SavePluginSettings(); - - LogHelper.WriteLogToFile("超级启动台插件已启用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - public override void Disable() - { - try - { - if (!IsEnabled) return; // 防止重复禁用 - - // 从浮动栏移除启动台按钮 - RemoveLauncherButtonFromFloatingBar(); - - // 如果启动台窗口打开,则关闭 - if (_launcherWindow != null && _launcherWindow.IsVisible) - { - _launcherWindow.Close(); - _launcherWindow = null; - } - - // 设置禁用状态 - base.Disable(); - - // 保存插件配置 - SavePluginSettings(); - - LogHelper.WriteLogToFile("超级启动台插件已禁用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - public override UserControl GetSettingsView() - { - return new LauncherSettingsControl(this); - } - - public override void Cleanup() - { - // 保存配置 - SaveConfig(); - - // 从浮动栏移除启动台按钮 - RemoveLauncherButtonFromFloatingBar(); - - // 如果启动台窗口打开,则关闭 - if (_launcherWindow != null && _launcherWindow.IsVisible) - { - _launcherWindow.Close(); - _launcherWindow = null; - } - - base.Cleanup(); - } - - /// - /// 保存插件设置 - /// - public override void SavePluginSettings() - { - try - { - // 确保配置已加载 - if (Config == null) - { - LoadConfig(); - } - - // 更新其他设置,但不更改插件启用状态 - - // 保存配置 - SaveConfig(); - - LogHelper.WriteLogToFile("超级启动台插件设置已保存"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 配置管理 - - /// - /// 加载配置 - /// - private void LoadConfig() - { - try - { - if (File.Exists(_configPath)) - { - string json = File.ReadAllText(_configPath); - Config = JsonConvert.DeserializeObject(json) ?? CreateDefaultConfig(); - LauncherItems = new ObservableCollection(Config.Items ?? new List()); - - // 注意:不再根据配置更改插件启用状态 - // 插件状态由PluginManager统一管理 - } - else - { - Config = CreateDefaultConfig(); - LauncherItems = new ObservableCollection(Config.Items); - SaveConfig(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error); - Config = CreateDefaultConfig(); - LauncherItems = new ObservableCollection(Config.Items); - } - } - - /// - /// 保存配置 - /// - public void SaveConfig() - { - try - { - // 同步LauncherItems到Config - Config.Items = new List(LauncherItems); - - // 序列化并保存配置 - string json = JsonConvert.SerializeObject(Config, Formatting.Indented); - File.WriteAllText(_configPath, json); - - LogHelper.WriteLogToFile("超级启动台配置已保存"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 创建默认配置 - /// - private LauncherConfig CreateDefaultConfig() - { - var config = new LauncherConfig - { - ButtonPosition = LauncherButtonPosition.Right, - // 不再使用IsEnabled,插件状态由PluginManager管理 - Items = new List - { - new LauncherItem - { - Name = "资源管理器", - Path = @"C:\Windows\explorer.exe", - IsVisible = true, - Position = 0 - } - } - }; - - return config; - } - - #endregion - - #region 启动台按钮管理 - - /// - /// 将启动台按钮添加到浮动栏 - /// - private void AddLauncherButtonToFloatingBar() - { - try - { - // 如果已经添加,先移除 - if (_isAddedToFloatingBar) - { - RemoveLauncherButtonFromFloatingBar(); - _isAddedToFloatingBar = false; - } - - // 获取主窗口实例 - var mainWindow = Application.Current.MainWindow; - if (mainWindow == null) - { - LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error); - return; - } - - // 创建启动台按钮 - _launcherButton = new LauncherButton(this); - var buttonElement = _launcherButton.Element; - - // 查找浮动栏 - var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel; - if (floatingBar == null) - { - // 如果直接查找失败,则尝试遍历可视树查找 - Panel floatingBarPanelFromTree = null; - FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree); - floatingBar = floatingBarPanelFromTree; - } - - if (floatingBar == null) - { - LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error); - return; - } - - // 添加启动台按钮到浮动栏 - if (Config.ButtonPosition == LauncherButtonPosition.Left) - { - floatingBar.Children.Insert(0, buttonElement); - LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧"); - } - else - { - floatingBar.Children.Add(buttonElement); - LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧"); - } - - _isAddedToFloatingBar = true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 递归查找StackPanelFloatingBar - /// - private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result) - { - if (parent == null || result != null) return; - - try - { - // 检查当前对象是否为我们要找的面板 - if (parent is Panel panel && panel.Name == "StackPanelFloatingBar") - { - result = panel; - return; - } - - // 获取子元素数量 - int childCount = VisualTreeHelper.GetChildrenCount(parent); - - // 遍历所有子元素 - for (int i = 0; i < childCount; i++) - { - DependencyObject child = VisualTreeHelper.GetChild(parent, i); - FindStackPanelFloatingBar(child, ref result); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 从浮动栏移除启动台按钮 - /// - private void RemoveLauncherButtonFromFloatingBar() - { - try - { - if (!_isAddedToFloatingBar || _launcherButton == null) - { - return; - } - - // 获取主窗口实例 - var mainWindow = Application.Current.MainWindow; - if (mainWindow == null) - { - LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error); - return; - } - - // 获取按钮元素 - var buttonElement = _launcherButton.Element; - - // 查找浮动栏 - var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel; - if (floatingBar == null) - { - // 如果直接查找失败,则尝试遍历可视树查找 - Panel floatingBarPanelFromTree = null; - FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree); - floatingBar = floatingBarPanelFromTree; - } - - if (floatingBar == null) - { - LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error); - return; - } - - // 从浮动栏移除启动台按钮 - if (floatingBar.Children.Contains(buttonElement)) - { - floatingBar.Children.Remove(buttonElement); - LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除"); - } - - _isAddedToFloatingBar = false; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 更新启动台按钮位置 - /// - public void UpdateButtonPosition() - { - try - { - // 如果按钮已添加到浮动栏,重新添加以更新位置 - if (_isAddedToFloatingBar) - { - RemoveLauncherButtonFromFloatingBar(); - AddLauncherButtonToFloatingBar(); - LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - #endregion - - #region 启动台功能 - - /// - /// 显示启动台窗口 - /// - /// 按钮在屏幕上的位置 - public void ShowLauncherWindow(Point buttonPosition) - { - try - { - // 如果窗口已存在,关闭它 - if (_launcherWindow != null && _launcherWindow.IsVisible) - { - _launcherWindow.Close(); - _launcherWindow = null; - return; - } - - // 创建新的启动台窗口 - _launcherWindow = new LauncherWindow(this); - - // 计算窗口位置,使其位于按钮上方 - PositionLauncherWindow(_launcherWindow, buttonPosition); - - // 显示窗口 - _launcherWindow.Show(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 设置启动台窗口位置 - /// - /// 启动台窗口 - /// 按钮在屏幕上的位置 - private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition) - { - // 确保窗口已加载 - if (window.ActualWidth == 0 || window.ActualHeight == 0) - { - window.WindowStartupLocation = WindowStartupLocation.CenterScreen; - - // 设置窗口加载完成后的位置 - window.Loaded += (s, e) => - { - // 窗口位于按钮上方居中 - double left = buttonPosition.X - (window.ActualWidth / 2); - double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距 - - // 确保窗口在屏幕内 - left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth)); - top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight)); - - window.Left = left; - window.Top = top; - }; - } - else - { - // 窗口位于按钮上方居中 - double left = buttonPosition.X - (window.ActualWidth / 2); - double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距 - - // 确保窗口在屏幕内 - left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth)); - top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight)); - - window.Left = left; - window.Top = top; - } - } - - /// - /// 添加应用到启动台 - /// - /// 启动台项 - public void AddLauncherItem(LauncherItem item) - { - // 如果项目数量已达上限,则不添加 - if (LauncherItems.Count >= 40) - { - MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); - return; - } - - // 寻找合适的位置 - if (item.Position < 0) - { - item.Position = FindNextAvailablePosition(); - } - - // 添加项目并保存配置 - LauncherItems.Add(item); - SaveConfig(); - } - - /// - /// 查找下一个可用位置 - /// - private int FindNextAvailablePosition() - { - // 获取已使用的位置列表 - var usedPositions = new HashSet(); - foreach (var item in LauncherItems) - { - usedPositions.Add(item.Position); - } - - // 查找第一个可用位置 - for (int i = 0; i < 40; i++) - { - if (!usedPositions.Contains(i)) - { - return i; - } - } - - // 如果所有位置都已使用,则返回0 - return 0; - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/EnhancedPluginBase.cs b/Ink Canvas/Helpers/Plugins/EnhancedPluginBase.cs deleted file mode 100644 index d22ca80e..00000000 --- a/Ink Canvas/Helpers/Plugins/EnhancedPluginBase.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 增强的插件基类,提供对插件服务的访问和基本实现 - /// - public abstract class EnhancedPluginBase : PluginBase, IEnhancedPlugin - { - /// - /// 插件服务实例 - /// - public IPluginService PluginService { get; private set; } - - /// - /// 构造函数 - /// - protected EnhancedPluginBase() - { - PluginService = PluginServiceManager.Instance; - } - - /// - /// 插件启动时调用,在Initialize之后 - /// - public virtual void OnStartup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已启动"); - } - - /// - /// 插件关闭时调用,在Cleanup之前 - /// - public virtual void OnShutdown() - { - LogHelper.WriteLogToFile($"插件 {Name} 正在关闭"); - } - - /// - /// 获取插件的菜单项 - /// - /// 菜单项集合 - public virtual MenuItem[] GetMenuItems() - { - return new MenuItem[0]; - } - - /// - /// 获取插件的工具栏按钮 - /// - /// 工具栏按钮集合 - public virtual Button[] GetToolbarButtons() - { - return new Button[0]; - } - - /// - /// 获取插件的状态栏信息 - /// - /// 状态栏信息 - public virtual string GetStatusBarInfo() - { - return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}"; - } - - /// - /// 插件配置变更时调用 - /// - public virtual void OnConfigurationChanged() - { - LogHelper.WriteLogToFile($"插件 {Name} 配置已变更"); - } - - /// - /// 重写初始化方法,调用OnStartup - /// - public override void Initialize() - { - base.Initialize(); - OnStartup(); - } - - /// - /// 重写清理方法,调用OnShutdown - /// - public override void Cleanup() - { - OnShutdown(); - base.Cleanup(); - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/EnhancedPluginBaseV2.cs b/Ink Canvas/Helpers/Plugins/EnhancedPluginBaseV2.cs deleted file mode 100644 index 218068b6..00000000 --- a/Ink Canvas/Helpers/Plugins/EnhancedPluginBaseV2.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 增强的插件基类 V2,提供对三个专门服务接口的访问 - /// 插件开发者可以根据需要选择性地使用这些服务 - /// - public abstract class EnhancedPluginBaseV2 : PluginBase, IEnhancedPlugin - { - /// - /// 获取服务实例 - /// - public IGetService GetService { get; private set; } - - /// - /// 窗口服务实例 - /// - public IWindowService WindowService { get; private set; } - - /// - /// 操作服务实例 - /// - public IActionService ActionService { get; private set; } - - /// - /// 插件服务实例(兼容性) - /// - public IPluginService PluginService { get; private set; } - - /// - /// 构造函数 - /// - protected EnhancedPluginBaseV2() - { - // 初始化所有服务实例 - PluginService = PluginServiceManager.Instance; - GetService = PluginServiceManager.Instance; - WindowService = PluginServiceManager.Instance; - ActionService = PluginServiceManager.Instance; - } - - /// - /// 插件启动时调用,在Initialize之后 - /// - public virtual void OnStartup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已启动"); - } - - /// - /// 插件关闭时调用,在Cleanup之前 - /// - public virtual void OnShutdown() - { - LogHelper.WriteLogToFile($"插件 {Name} 正在关闭"); - } - - /// - /// 获取插件的菜单项 - /// - /// 菜单项集合 - public virtual MenuItem[] GetMenuItems() - { - return new MenuItem[0]; - } - - /// - /// 获取插件的工具栏按钮 - /// - /// 工具栏按钮集合 - public virtual Button[] GetToolbarButtons() - { - return new Button[0]; - } - - /// - /// 获取插件的状态栏信息 - /// - /// 状态栏信息 - public virtual string GetStatusBarInfo() - { - return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}"; - } - - /// - /// 插件配置变更时调用 - /// - public virtual void OnConfigurationChanged() - { - LogHelper.WriteLogToFile($"插件 {Name} 配置已变更"); - } - - #region 便捷方法 - - /// - /// 显示通知消息 - /// - /// 消息内容 - /// 消息类型 - protected void ShowNotification(string message, NotificationType type = NotificationType.Info) - { - WindowService.ShowNotification(message, type); - } - - /// - /// 显示确认对话框 - /// - /// 消息内容 - /// 标题 - /// 用户选择结果 - protected bool ShowConfirmDialog(string message, string title = "确认") - { - return WindowService.ShowConfirmDialog(message, title); - } - - /// - /// 显示输入对话框 - /// - /// 提示消息 - /// 标题 - /// 默认值 - /// 用户输入内容 - protected string ShowInputDialog(string message, string title = "输入", string defaultValue = "") - { - return WindowService.ShowInputDialog(message, title, defaultValue); - } - - /// - /// 获取系统设置 - /// - /// 设置类型 - /// 设置键 - /// 默认值 - /// 设置值 - protected T GetSetting(string key, T defaultValue = default(T)) - { - return GetService.GetSetting(key, defaultValue); - } - - /// - /// 设置系统设置 - /// - /// 设置类型 - /// 设置键 - /// 设置值 - protected void SetSetting(string key, T value) - { - ActionService.SetSetting(key, value); - } - - /// - /// 保存设置 - /// - protected void SaveSettings() - { - ActionService.SaveSettings(); - } - - /// - /// 清除当前画布 - /// - protected void ClearCanvas() - { - ActionService.ClearCanvas(); - } - - /// - /// 撤销操作 - /// - protected void Undo() - { - ActionService.Undo(); - } - - /// - /// 重做操作 - /// - protected void Redo() - { - ActionService.Redo(); - } - - /// - /// 检查是否可以撤销 - /// - protected bool CanUndo => GetService.CanUndo; - - /// - /// 检查是否可以重做 - /// - protected bool CanRedo => GetService.CanRedo; - - /// - /// 获取当前绘制模式 - /// - protected int CurrentDrawingMode => GetService.CurrentDrawingMode; - - /// - /// 设置绘制模式 - /// - /// 绘制模式 - protected void SetDrawingMode(int mode) - { - ActionService.SetDrawingMode(mode); - } - - /// - /// 注册事件处理器 - /// - /// 事件名称 - /// 事件处理器 - protected void RegisterEventHandler(string eventName, System.EventHandler handler) - { - ActionService.RegisterEventHandler(eventName, handler); - } - - /// - /// 注销事件处理器 - /// - /// 事件名称 - /// 事件处理器 - protected void UnregisterEventHandler(string eventName, System.EventHandler handler) - { - ActionService.UnregisterEventHandler(eventName, handler); - } - - /// - /// 触发事件 - /// - /// 事件名称 - /// 事件发送者 - /// 事件参数 - protected void TriggerEvent(string eventName, object sender, System.EventArgs args) - { - ActionService.TriggerEvent(eventName, sender, args); - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IActionService.cs b/Ink Canvas/Helpers/Plugins/IActionService.cs deleted file mode 100644 index 99c61473..00000000 --- a/Ink Canvas/Helpers/Plugins/IActionService.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 操作服务接口,统一所有执行操作相关的方法 - /// - public interface IActionService - { - #region 画布操作 - - /// - /// 清除当前画布 - /// - void ClearCanvas(); - - /// - /// 清除所有画布 - /// - void ClearAllCanvases(); - - /// - /// 添加新页面 - /// - void AddNewPage(); - - /// - /// 删除当前页面 - /// - void DeleteCurrentPage(); - - /// - /// 切换到指定页面 - /// - /// 页面索引 - void SwitchToPage(int pageIndex); - - /// - /// 切换到下一页 - /// - void NextPage(); - - /// - /// 切换到上一页 - /// - void PreviousPage(); - - #endregion - - #region 绘制操作 - - /// - /// 设置绘制模式 - /// - /// 绘制模式 - void SetDrawingMode(int mode); - - /// - /// 设置笔触宽度 - /// - /// 宽度 - void SetInkWidth(double width); - - /// - /// 设置笔触颜色 - /// - /// 颜色 - void SetInkColor(Color color); - - /// - /// 设置高亮笔宽度 - /// - /// 宽度 - void SetHighlighterWidth(double width); - - /// - /// 设置橡皮擦大小 - /// - /// 大小 - void SetEraserSize(int size); - - /// - /// 设置橡皮擦类型 - /// - /// 类型 - void SetEraserType(int type); - - /// - /// 设置橡皮擦形状 - /// - /// 形状 - void SetEraserShape(int shape); - - /// - /// 设置笔触透明度 - /// - /// 透明度 - void SetInkAlpha(double alpha); - - /// - /// 设置笔触样式 - /// - /// 样式 - void SetInkStyle(int style); - - /// - /// 设置背景颜色 - /// - /// 颜色 - void SetBackgroundColor(string color); - - #endregion - - #region 文件操作 - - /// - /// 保存画布内容 - /// - /// 文件路径 - void SaveCanvas(string filePath); - - /// - /// 加载画布内容 - /// - /// 文件路径 - void LoadCanvas(string filePath); - - /// - /// 导出为图片 - /// - /// 文件路径 - /// 图片格式 - void ExportAsImage(string filePath, string format); - - /// - /// 导出为PDF - /// - /// 文件路径 - void ExportAsPDF(string filePath); - - #endregion - - #region 撤销重做操作 - - /// - /// 撤销操作 - /// - void Undo(); - - /// - /// 重做操作 - /// - void Redo(); - - #endregion - - #region 选择操作 - - /// - /// 全选 - /// - void SelectAll(); - - /// - /// 取消选择 - /// - void DeselectAll(); - - /// - /// 删除选中内容 - /// - void DeleteSelected(); - - /// - /// 复制选中内容 - /// - void CopySelected(); - - /// - /// 剪切选中内容 - /// - void CutSelected(); - - /// - /// 粘贴内容 - /// - void Paste(); - - #endregion - - #region 系统设置操作 - - /// - /// 设置系统设置 - /// - /// 设置类型 - /// 设置键 - /// 设置值 - void SetSetting(string key, T value); - - /// - /// 保存设置到文件 - /// - void SaveSettings(); - - /// - /// 从文件加载设置 - /// - void LoadSettings(); - - /// - /// 重置设置为默认值 - /// - void ResetSettings(); - - #endregion - - #region 插件管理操作 - - /// - /// 启用插件 - /// - /// 插件名称 - void EnablePlugin(string pluginName); - - /// - /// 禁用插件 - /// - /// 插件名称 - void DisablePlugin(string pluginName); - - /// - /// 卸载插件 - /// - /// 插件名称 - void UnloadPlugin(string pluginName); - - #endregion - - #region 事件系统操作 - - /// - /// 注册事件处理器 - /// - /// 事件名称 - /// 事件处理器 - void RegisterEventHandler(string eventName, EventHandler handler); - - /// - /// 注销事件处理器 - /// - /// 事件名称 - /// 事件处理器 - void UnregisterEventHandler(string eventName, EventHandler handler); - - /// - /// 触发事件 - /// - /// 事件名称 - /// 事件发送者 - /// 事件参数 - void TriggerEvent(string eventName, object sender, EventArgs args); - - #endregion - - #region 应用程序操作 - - /// - /// 重启应用程序 - /// - void RestartApplication(); - - /// - /// 退出应用程序 - /// - void ExitApplication(); - - /// - /// 检查更新 - /// - void CheckForUpdates(); - - /// - /// 打开帮助文档 - /// - void OpenHelpDocument(); - - /// - /// 打开关于页面 - /// - void OpenAboutPage(); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs b/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs deleted file mode 100644 index e018ac0d..00000000 --- a/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.IO; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件 - /// - public class ICCPPPluginAdapter : PluginBase - { - private readonly byte[] _pluginData; - private readonly string _pluginPath; - private readonly string _pluginName; - private readonly Version _pluginVersion; - private bool _isInitialized; - - /// - /// 创建 ICCPP 插件适配器 - /// - /// 插件文件路径 - /// 插件文件数据 - public ICCPPPluginAdapter(string pluginPath, byte[] pluginData) - { - _pluginPath = pluginPath; - _pluginData = pluginData; - PluginPath = pluginPath; - - // 从文件名获取插件名称 - _pluginName = Path.GetFileNameWithoutExtension(pluginPath); - _pluginVersion = new Version(1, 0, 0); // 默认版本 - - // 尝试从插件数据中读取更多信息 - TryReadPluginMetadata(); - } - - public ICCPPPluginAdapter() - { - _pluginPath = string.Empty; - _pluginData = new byte[0]; - PluginPath = string.Empty; - _pluginName = "ICCPPPlugin"; - _pluginVersion = new Version(1, 0, 0); - // 可选:初始化其他字段 - } - - /// - /// 尝试从插件数据中读取元数据 - /// - private void TryReadPluginMetadata() - { - try - { - // 这里可以根据 .iccpp 文件的实际格式解析元数据 - // 例如,如果文件有特定的头部结构,可以在这里解析 - - // 示例:如果前100字节包含元数据 - if (_pluginData.Length > 100) - { - // 解析元数据的代码... - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #region IPlugin 接口实现 - - /// - /// 插件名称 - /// - public override string Name => _pluginName; - - /// - /// 插件描述 - /// - public override string Description => $"{_pluginName} (ICCPP 格式插件)"; - - /// - /// 插件版本 - /// - public override Version Version => _pluginVersion; - - /// - /// 插件作者 - /// - public override string Author => "未知"; - - /// - /// 是否为内置插件 - /// - public override bool IsBuiltIn => false; - - /// - /// 初始化插件 - /// - public override void Initialize() - { - if (_isInitialized) return; - - try - { - // 这里可以添加 .iccpp 插件的初始化逻辑 - // 例如,根据文件格式加载特定资源 - - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化"); - _isInitialized = true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 启用插件 - /// - public override void Enable() - { - if (IsEnabled) return; - - try - { - // 这里可以添加 .iccpp 插件的启用逻辑 - // 例如,加载动态库、注册事件等 - - base.Enable(); // 设置启用状态并触发事件 - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 禁用插件 - /// - public override void Disable() - { - if (!IsEnabled) return; - - try - { - // 这里可以添加 .iccpp 插件的禁用逻辑 - // 例如,卸载动态库、注销事件等 - - base.Disable(); // 设置禁用状态并触发事件 - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 清理插件资源 - /// - public override void Cleanup() - { - try - { - // 这里可以添加 .iccpp 插件的清理逻辑 - // 例如,释放资源等 - - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IEnhancedPlugin.cs b/Ink Canvas/Helpers/Plugins/IEnhancedPlugin.cs deleted file mode 100644 index 312eb10c..00000000 --- a/Ink Canvas/Helpers/Plugins/IEnhancedPlugin.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 增强的插件接口,提供对插件服务的访问 - /// - public interface IEnhancedPlugin : IPlugin - { - /// - /// 获取插件服务实例 - /// - IPluginService PluginService { get; } - - /// - /// 插件启动时调用,在Initialize之后 - /// - void OnStartup(); - - /// - /// 插件关闭时调用,在Cleanup之前 - /// - void OnShutdown(); - - /// - /// 获取插件的菜单项 - /// - /// 菜单项集合 - MenuItem[] GetMenuItems(); - - /// - /// 获取插件的工具栏按钮 - /// - /// 工具栏按钮集合 - Button[] GetToolbarButtons(); - - /// - /// 获取插件的状态栏信息 - /// - /// 状态栏信息 - string GetStatusBarInfo(); - - /// - /// 插件配置变更时调用 - /// - void OnConfigurationChanged(); - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IGetService.cs b/Ink Canvas/Helpers/Plugins/IGetService.cs deleted file mode 100644 index ab48889a..00000000 --- a/Ink Canvas/Helpers/Plugins/IGetService.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.Collections.Generic; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 获取服务接口,统一所有获取类的方法 - /// - public interface IGetService - { - #region 窗口和UI获取 - - /// - /// 获取主窗口引用 - /// - Window MainWindow { get; } - - /// - /// 获取当前画布 - /// - InkCanvas CurrentCanvas { get; } - - /// - /// 获取所有画布页面 - /// - List AllCanvasPages { get; } - - /// - /// 获取当前页面索引 - /// - int CurrentPageIndex { get; } - - /// - /// 获取当前页面数量 - /// - int TotalPageCount { get; } - - /// - /// 获取浮动工具栏 - /// - FrameworkElement FloatingToolBar { get; } - - /// - /// 获取左侧面板 - /// - FrameworkElement LeftPanel { get; } - - /// - /// 获取右侧面板 - /// - FrameworkElement RightPanel { get; } - - /// - /// 获取顶部面板 - /// - FrameworkElement TopPanel { get; } - - /// - /// 获取底部面板 - /// - FrameworkElement BottomPanel { get; } - - #endregion - - #region 绘制工具状态获取 - - /// - /// 获取当前绘制模式 - /// - int CurrentDrawingMode { get; } - - /// - /// 获取当前笔触宽度 - /// - double CurrentInkWidth { get; } - - /// - /// 获取当前笔触颜色 - /// - Color CurrentInkColor { get; } - - /// - /// 获取当前高亮笔宽度 - /// - double CurrentHighlighterWidth { get; } - - /// - /// 获取当前橡皮擦大小 - /// - int CurrentEraserSize { get; } - - /// - /// 获取当前橡皮擦类型 - /// - int CurrentEraserType { get; } - - /// - /// 获取当前橡皮擦形状 - /// - int CurrentEraserShape { get; } - - /// - /// 获取当前笔触透明度 - /// - double CurrentInkAlpha { get; } - - /// - /// 获取当前笔触样式 - /// - int CurrentInkStyle { get; } - - /// - /// 获取当前背景颜色 - /// - string CurrentBackgroundColor { get; } - - #endregion - - #region 应用状态获取 - - /// - /// 获取当前主题模式 - /// - bool IsDarkTheme { get; } - - /// - /// 获取当前是否为白板模式 - /// - bool IsWhiteboardMode { get; } - - /// - /// 获取当前是否为PPT模式 - /// - bool IsPPTMode { get; } - - /// - /// 获取当前是否为全屏模式 - /// - bool IsFullScreenMode { get; } - - /// - /// 获取当前是否为画板模式 - /// - bool IsCanvasMode { get; } - - /// - /// 获取当前是否为选择模式 - /// - bool IsSelectionMode { get; } - - /// - /// 获取当前是否为擦除模式 - /// - bool IsEraserMode { get; } - - /// - /// 获取当前是否为形状绘制模式 - /// - bool IsShapeDrawingMode { get; } - - /// - /// 获取当前是否为高亮模式 - /// - bool IsHighlighterMode { get; } - - #endregion - - #region 撤销重做状态获取 - - /// - /// 获取是否可以撤销 - /// - bool CanUndo { get; } - - /// - /// 获取是否可以重做 - /// - bool CanRedo { get; } - - #endregion - - #region 系统设置获取 - - /// - /// 获取系统设置 - /// - /// 设置类型 - /// 设置键 - /// 默认值 - /// 设置值 - T GetSetting(string key, T defaultValue = default(T)); - - #endregion - - #region 插件信息获取 - - /// - /// 获取所有已加载的插件 - /// - /// 插件列表 - List GetAllPlugins(); - - /// - /// 获取指定插件 - /// - /// 插件名称 - /// 插件实例 - IPlugin GetPlugin(string pluginName); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IPlugin.cs b/Ink Canvas/Helpers/Plugins/IPlugin.cs deleted file mode 100644 index 0a43ab15..00000000 --- a/Ink Canvas/Helpers/Plugins/IPlugin.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 定义插件的基本接口 - /// - public interface IPlugin - { - /// - /// 插件名称 - /// - string Name { get; } - - /// - /// 插件描述 - /// - string Description { get; } - - /// - /// 插件版本 - /// - Version Version { get; } - - /// - /// 插件作者 - /// - string Author { get; } - - /// - /// 是否为内置插件 - /// - bool IsBuiltIn { get; } - - /// - /// 初始化插件 - /// 此方法在插件加载时被调用,用于执行一些初始化工作 - /// - void Initialize(); - - /// - /// 启用插件 - /// 此方法在插件被用户或系统启用时调用,激活插件功能 - /// - void Enable(); - - /// - /// 禁用插件 - /// 此方法在插件被用户或系统禁用时调用,停用插件功能 - /// - void Disable(); - - /// - /// 获取插件设置界面 - /// 此方法返回插件的设置界面控件,用于展示在设置窗口 - /// - /// 插件设置界面 - UserControl GetSettingsView(); - - /// - /// 插件卸载时的清理工作 - /// 此方法在插件被卸载前调用,用于释放资源和执行清理 - /// - void Cleanup(); - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IPluginService.cs b/Ink Canvas/Helpers/Plugins/IPluginService.cs deleted file mode 100644 index 9559a992..00000000 --- a/Ink Canvas/Helpers/Plugins/IPluginService.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件服务接口,提供对软件内部功能的访问 - /// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务 - /// - public interface IPluginService : IGetService, IWindowService, IActionService - { - // 这个接口现在继承自三个专门的服务接口 - // 所有方法都在子接口中定义,这里不需要重复定义 - } - - /// - /// 通知类型枚举 - /// - public enum NotificationType - { - /// - /// 信息 - /// - Info, - - /// - /// 成功 - /// - Success, - - /// - /// 警告 - /// - Warning, - - /// - /// 错误 - /// - Error - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IWindowService.cs b/Ink Canvas/Helpers/Plugins/IWindowService.cs deleted file mode 100644 index eb6fb863..00000000 --- a/Ink Canvas/Helpers/Plugins/IWindowService.cs +++ /dev/null @@ -1,152 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 窗口服务接口,统一所有窗口操作相关的方法 - /// - public interface IWindowService - { - #region 窗口显示和隐藏 - - /// - /// 显示设置窗口 - /// - void ShowSettingsWindow(); - - /// - /// 隐藏设置窗口 - /// - void HideSettingsWindow(); - - /// - /// 显示插件设置窗口 - /// - void ShowPluginSettingsWindow(); - - /// - /// 隐藏插件设置窗口 - /// - void HidePluginSettingsWindow(); - - /// - /// 显示帮助窗口 - /// - void ShowHelpWindow(); - - /// - /// 隐藏帮助窗口 - /// - void HideHelpWindow(); - - /// - /// 显示关于窗口 - /// - void ShowAboutWindow(); - - /// - /// 隐藏关于窗口 - /// - void HideAboutWindow(); - - #endregion - - #region 对话框和通知 - - /// - /// 显示通知消息 - /// - /// 消息内容 - /// 消息类型 - void ShowNotification(string message, NotificationType type = NotificationType.Info); - - /// - /// 显示确认对话框 - /// - /// 消息内容 - /// 标题 - /// 用户选择结果 - bool ShowConfirmDialog(string message, string title = "确认"); - - /// - /// 显示输入对话框 - /// - /// 提示消息 - /// 标题 - /// 默认值 - /// 用户输入内容 - string ShowInputDialog(string message, string title = "输入", string defaultValue = ""); - - #endregion - - #region 窗口状态控制 - - /// - /// 设置窗口全屏状态 - /// - /// 是否全屏 - void SetFullScreen(bool isFullScreen); - - /// - /// 设置窗口置顶状态 - /// - /// 是否置顶 - void SetTopMost(bool isTopMost); - - /// - /// 设置窗口可见性 - /// - /// 是否可见 - void SetWindowVisibility(bool isVisible); - - /// - /// 最小化窗口 - /// - void MinimizeWindow(); - - /// - /// 最大化窗口 - /// - void MaximizeWindow(); - - /// - /// 恢复窗口 - /// - void RestoreWindow(); - - /// - /// 关闭窗口 - /// - void CloseWindow(); - - #endregion - - #region 窗口位置和大小 - - /// - /// 设置窗口位置 - /// - /// X坐标 - /// Y坐标 - void SetWindowPosition(double x, double y); - - /// - /// 设置窗口大小 - /// - /// 宽度 - /// 高度 - void SetWindowSize(double width, double height); - - /// - /// 获取窗口位置 - /// - /// 窗口位置 - (double x, double y) GetWindowPosition(); - - /// - /// 获取窗口大小 - /// - /// 窗口大小 - (double width, double height) GetWindowSize(); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginBase.cs b/Ink Canvas/Helpers/Plugins/PluginBase.cs deleted file mode 100644 index 716b7e61..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginBase.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件基类,提供基本实现 - /// - public abstract class PluginBase : IPlugin - { - /// - /// 插件状态(私有字段) - /// - private bool _isEnabled; - - /// - /// 插件状态(公共属性) - /// - public bool IsEnabled - { - get => _isEnabled; - protected set - { - if (_isEnabled != value) - { - _isEnabled = value; - OnEnabledStateChanged(value); - } - } - } - - /// - /// 插件ID - /// - public string Id { get; protected set; } - - /// - /// 插件路径 - /// - public string PluginPath { get; set; } - - /// - /// 插件名称 - /// - public abstract string Name { get; } - - /// - /// 插件描述 - /// - public abstract string Description { get; } - - /// - /// 插件版本 - /// - public abstract Version Version { get; } - - /// - /// 插件作者 - /// - public abstract string Author { get; } - - /// - /// 是否为内置插件 - /// - public virtual bool IsBuiltIn => false; - - /// - /// 状态变更事件 - /// - public event EventHandler EnabledStateChanged; - - /// - /// 初始化插件 - /// - public virtual void Initialize() - { - Id = GetType().FullName; - - // 添加日志,记录插件名称 - try - { - string name = Name; - LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? "未命名"}"); - - if (string.IsNullOrEmpty(name)) - { - LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error); - } - - LogHelper.WriteLogToFile($"插件 {Name} 已初始化"); - } - - /// - /// 启用插件 - /// - public virtual void Enable() - { - if (!IsEnabled) - { - IsEnabled = true; - LogHelper.WriteLogToFile($"插件 {Name} 已启用"); - } - } - - /// - /// 禁用插件 - /// - public virtual void Disable() - { - if (IsEnabled) - { - IsEnabled = false; - LogHelper.WriteLogToFile($"插件 {Name} 已禁用"); - } - } - - /// - /// 获取插件设置界面 - /// - /// 插件设置界面 - public virtual UserControl GetSettingsView() - { - // 默认返回空设置页面 - return new UserControl(); - } - - /// - /// 插件卸载时的清理工作 - /// - public virtual void Cleanup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已卸载"); - } - - /// - /// 保存插件自身的设置 - /// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态 - /// 插件启用状态由PluginManager统一管理 - /// - public virtual void SavePluginSettings() - { - // 默认实现不做任何事情 - // 子类可以重写此方法,将自身设置保存到配置文件中 - LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event); - } - - /// - /// 触发状态变更事件 - /// - /// 是否启用 - protected virtual void OnEnabledStateChanged(bool isEnabled) - { - EnabledStateChanged?.Invoke(this, isEnabled); - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginConfigurationManager.cs b/Ink Canvas/Helpers/Plugins/PluginConfigurationManager.cs deleted file mode 100644 index 9688cd31..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginConfigurationManager.cs +++ /dev/null @@ -1,273 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件配置管理器,允许插件管理自己的配置 - /// - public class PluginConfigurationManager - { - private static readonly string PluginConfigDirectory = Path.Combine(App.RootPath, "PluginConfigs"); - private static readonly Dictionary> _pluginConfigs = new Dictionary>(); - private static readonly object _lockObject = new object(); - - static PluginConfigurationManager() - { - // 确保配置目录存在 - if (!Directory.Exists(PluginConfigDirectory)) - { - Directory.CreateDirectory(PluginConfigDirectory); - } - } - - /// - /// 获取插件配置值 - /// - /// 配置值类型 - /// 插件名称 - /// 配置键 - /// 默认值 - /// 配置值 - public static T GetConfiguration(string pluginName, string key, T defaultValue = default(T)) - { - lock (_lockObject) - { - try - { - if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig)) - { - if (pluginConfig.TryGetValue(key, out var value)) - { - if (value is T typedValue) - { - return typedValue; - } - - // 尝试类型转换 - try - { - return (T)Convert.ChangeType(value, typeof(T)); - } - catch - { - return defaultValue; - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - - return defaultValue; - } - } - - /// - /// 设置插件配置值 - /// - /// 配置值类型 - /// 插件名称 - /// 配置键 - /// 配置值 - public static void SetConfiguration(string pluginName, string key, T value) - { - lock (_lockObject) - { - try - { - if (!_pluginConfigs.ContainsKey(pluginName)) - { - _pluginConfigs[pluginName] = new Dictionary(); - } - - _pluginConfigs[pluginName][key] = value; - - // 异步保存配置 - Task.Run(() => SavePluginConfiguration(pluginName)); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - /// - /// 删除插件配置 - /// - /// 插件名称 - /// 配置键 - public static void RemoveConfiguration(string pluginName, string key) - { - lock (_lockObject) - { - try - { - if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig)) - { - if (pluginConfig.Remove(key)) - { - // 异步保存配置 - Task.Run(() => SavePluginConfiguration(pluginName)); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - /// - /// 获取插件的所有配置 - /// - /// 插件名称 - /// 配置字典 - public static Dictionary GetAllConfigurations(string pluginName) - { - lock (_lockObject) - { - if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig)) - { - return new Dictionary(pluginConfig); - } - return new Dictionary(); - } - } - - /// - /// 清除插件的所有配置 - /// - /// 插件名称 - public static void ClearAllConfigurations(string pluginName) - { - lock (_lockObject) - { - try - { - if (_pluginConfigs.Remove(pluginName)) - { - // 删除配置文件 - string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json"); - if (File.Exists(configFile)) - { - File.Delete(configFile); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清除插件 {pluginName} 所有配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - /// - /// 加载插件配置 - /// - /// 插件名称 - public static void LoadPluginConfiguration(string pluginName) - { - try - { - string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json"); - if (File.Exists(configFile)) - { - string json = File.ReadAllText(configFile); - var config = JsonConvert.DeserializeObject>(json); - - lock (_lockObject) - { - _pluginConfigs[pluginName] = config ?? new Dictionary(); - } - - LogHelper.WriteLogToFile($"已加载插件 {pluginName} 的配置"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 保存插件配置 - /// - /// 插件名称 - private static void SavePluginConfiguration(string pluginName) - { - try - { - Dictionary pluginConfig; - lock (_lockObject) - { - if (!_pluginConfigs.TryGetValue(pluginName, out pluginConfig)) - { - return; - } - } - - string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json"); - string json = JsonConvert.SerializeObject(pluginConfig, Formatting.Indented); - File.WriteAllText(configFile, json); - - LogHelper.WriteLogToFile($"已保存插件 {pluginName} 的配置"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载所有插件的配置 - /// - public static void LoadAllPluginConfigurations() - { - try - { - if (Directory.Exists(PluginConfigDirectory)) - { - string[] configFiles = Directory.GetFiles(PluginConfigDirectory, "*.json"); - foreach (string configFile in configFiles) - { - string pluginName = Path.GetFileNameWithoutExtension(configFile); - LoadPluginConfiguration(pluginName); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 保存所有插件的配置 - /// - public static void SaveAllPluginConfigurations() - { - try - { - lock (_lockObject) - { - foreach (string pluginName in _pluginConfigs.Keys) - { - SavePluginConfiguration(pluginName); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginManager.cs b/Ink Canvas/Helpers/Plugins/PluginManager.cs deleted file mode 100644 index 9e018183..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginManager.cs +++ /dev/null @@ -1,1459 +0,0 @@ -using Ink_Canvas.Windows; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using Timer = System.Timers.Timer; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件管理器,负责插件的加载、卸载和管理 - /// - public class PluginManager - { - private static readonly string PluginsDirectory = Path.Combine(App.RootPath, "Plugins"); - private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json"); - private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json.bak"); - - private static PluginManager _instance; - private static SemaphoreSlim _configLock = new SemaphoreSlim(1, 1); - - /// - /// 插件管理器单例 - /// - public static PluginManager Instance - { - get - { - if (_instance == null) - { - _instance = new PluginManager(); - } - return _instance; - } - } - - /// - /// 已加载的插件集合 - /// - public ObservableCollection Plugins { get; } = new ObservableCollection(); - - /// - /// 插件配置信息 - /// - public Dictionary PluginStates { get; private set; } = new Dictionary(); - - /// - /// 配置是否已更改但未保存 - /// - private bool _configDirty; - - /// - /// 配置自动保存计时器 - /// - private Timer _autoSaveTimer; - - /// - /// 加载的程序集缓存 - /// - private Dictionary _loadedAssemblies = new Dictionary(); - - /// - /// 插件文件哈希缓存,用于热重载检测 - /// - private Dictionary _pluginHashes = new Dictionary(); - - private PluginManager() - { - // 确保插件目录存在 - if (!Directory.Exists(PluginsDirectory)) - { - Directory.CreateDirectory(PluginsDirectory); - } - - - // 初始化自动保存计时器(3秒) - _autoSaveTimer = new Timer(3000); - _autoSaveTimer.Elapsed += (s, e) => - { - if (_configDirty) - { - SaveConfigAsync().ConfigureAwait(false); - } - }; - _autoSaveTimer.AutoReset = false; - - // 注册插件状态变更事件处理 - AppDomain.CurrentDomain.ProcessExit += (s, e) => - { - // 应用退出时强制保存配置 - if (_configDirty) - { - SaveConfig(); - } - }; - } - - /// - /// 初始化插件系统 - /// - public void Initialize() - { - try - { - LogHelper.WriteLogToFile("开始初始化插件系统"); - - // 加载配置 - LoadConfig(); - LogHelper.WriteLogToFile($"已从配置文件加载 {PluginStates.Count} 个插件状态记录"); - - // 加载内置插件 - LogHelper.WriteLogToFile("正在加载内置插件..."); - LoadBuiltInPlugins(); - - // 加载外部插件 - LogHelper.WriteLogToFile("正在加载外部插件..."); - LoadExternalPlugins(); - - // 启用已配置为启用的插件 - LogHelper.WriteLogToFile("正在应用配置的插件状态..."); - EnableConfiguredPlugins(); - - // 设置定期检查热重载 - StartHotReloadWatcher(); - - // 保存初始化后的配置(可能有新插件) - SaveConfig(); - - LogHelper.WriteLogToFile($"插件系统初始化完成,共加载 {Plugins.Count} 个插件"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化插件系统时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载内置插件 - /// - private void LoadBuiltInPlugins() - { - try - { - // 获取当前程序集 - Assembly currentAssembly = Assembly.GetExecutingAssembly(); - - // 查找实现了IPlugin接口的所有类型 - var pluginTypes = currentAssembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass); - - foreach (var pluginType in pluginTypes) - { - try - { - // 创建插件实例 - IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType); - - // 只处理内置插件 - if (plugin.IsBuiltIn) - { - plugin.Initialize(); - Plugins.Add(plugin); - LogHelper.WriteLogToFile($"已加载内置插件: {plugin.Name} v{plugin.Version}"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载内置插件 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载内置插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载外部插件 - /// - private void LoadExternalPlugins() - { - try - { - // 检查插件目录是否存在 - if (!Directory.Exists(PluginsDirectory)) - { - Directory.CreateDirectory(PluginsDirectory); - return; - } - - // 获取所有插件文件(支持 .iccpp 和 .dll 格式) - var pluginFiles = Directory.GetFiles(PluginsDirectory, "*.iccpp", SearchOption.TopDirectoryOnly) - .Concat(Directory.GetFiles(PluginsDirectory, "*.dll", SearchOption.TopDirectoryOnly)) - .ToArray(); - - LogHelper.WriteLogToFile($"发现 {pluginFiles.Length} 个外部插件文件"); - - foreach (var pluginFile in pluginFiles) - { - LoadExternalPlugin(pluginFile); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载外部插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载单个外部插件 - /// - /// 插件文件路径 - /// 加载的插件实例,加载失败则返回null - public IPlugin LoadExternalPlugin(string pluginPath) - { - try - { - // 计算文件哈希 - string fileHash = CalculateFileHash(pluginPath); - _pluginHashes[pluginPath] = fileHash; - - // 检查文件扩展名 - string extension = Path.GetExtension(pluginPath).ToLowerInvariant(); - if (extension == ".iccpp") - { - // 创建 ICCPP 插件适配器 - return CreateICCPPPluginAdapter(pluginPath); - } - - // 加载插件程序集 - Assembly pluginAssembly = LoadPluginAssembly(pluginPath); - if (pluginAssembly == null) return null; - - // 查找实现了IPlugin接口的类型 - var pluginTypes = pluginAssembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass); - - foreach (var pluginType in pluginTypes) - { - try - { - // 创建插件实例 - IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType); - - // 设置插件路径 - if (plugin is PluginBase pluginBase) - { - pluginBase.PluginPath = pluginPath; - } - - plugin.Initialize(); - Plugins.Add(plugin); - - LogHelper.WriteLogToFile($"已加载外部插件: {plugin.Name} v{plugin.Version} 来自 {Path.GetFileName(pluginPath)}"); - - return plugin; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"实例化插件类型 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - LogHelper.WriteLogToFile($"在程序集 {Path.GetFileName(pluginPath)} 中未找到有效的插件类型", LogHelper.LogType.Warning); - return null; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载外部插件 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 创建 ICCPP 插件适配器 - /// - /// 插件文件路径 - /// 适配的插件实例 - private IPlugin CreateICCPPPluginAdapter(string pluginPath) - { - try - { - // 读取插件文件内容 - byte[] pluginData = File.ReadAllBytes(pluginPath); - - // 创建适配器插件实例 - var pluginAdapter = new ICCPPPluginAdapter(pluginPath, pluginData); - - // 添加到插件列表 - Plugins.Add(pluginAdapter); - - LogHelper.WriteLogToFile($"已创建 ICCPP 插件适配器: {pluginAdapter.Name} 来自 {Path.GetFileName(pluginPath)}"); - - return pluginAdapter; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建 ICCPP 插件适配器时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 加载插件程序集 - /// - /// 插件文件路径 - /// 加载的程序集 - private Assembly LoadPluginAssembly(string pluginPath) - { - try - { - // 检查是否已加载该程序集 - if (_loadedAssemblies.TryGetValue(pluginPath, out var loadedAssembly)) - { - return loadedAssembly; - } - - // 直接加载程序集 - Assembly pluginAssembly = Assembly.LoadFrom(pluginPath); - _loadedAssemblies[pluginPath] = pluginAssembly; - - return pluginAssembly; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载插件程序集 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 启用已配置为启用的插件 - /// - private void EnableConfiguredPlugins() - { - int enabledCount = 0; - int disabledCount = 0; - int errorCount = 0; - - foreach (var plugin in Plugins) - { - try - { - string pluginTypeName = plugin.GetType().FullName; - - // 检查配置中的插件状态 - if (PluginStates.TryGetValue(pluginTypeName, out bool enabled)) - { - // 获取当前实际状态 - bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - - // 如果配置状态与当前状态不一致,则应用配置状态 - if (currentState != enabled) - { - // 注册插件状态变更事件 - if (plugin is PluginBase pb) - { - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - - if (enabled) - { - plugin.Enable(); - enabledCount++; - LogHelper.WriteLogToFile($"根据配置启用插件: {plugin.Name}"); - } - else - { - plugin.Disable(); - disabledCount++; - LogHelper.WriteLogToFile($"根据配置禁用插件: {plugin.Name}"); - } - } - else - { - // 状态一致,只注册事件 - if (plugin is PluginBase pb) - { - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - } - } - else - { - // 插件不在配置中,添加默认状态(禁用) - PluginStates[pluginTypeName] = false; - _configDirty = true; - - // 注册插件状态变更事件 - if (plugin is PluginBase pb) - { - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - - // 如果当前是启用状态,则禁用 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - plugin.Disable(); - disabledCount++; - LogHelper.WriteLogToFile($"插件不在配置中,默认禁用: {plugin.Name}"); - } - } - } - catch (Exception ex) - { - errorCount++; - LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - // 如果有配置变更,启动自动保存 - if (_configDirty) - { - TriggerAutoSave(); - } - - LogHelper.WriteLogToFile($"已应用插件配置: 启用 {enabledCount} 个,禁用 {disabledCount} 个,错误 {errorCount} 个"); - } - - /// - /// 插件状态变更事件处理 - /// - private void Plugin_EnabledStateChanged(object sender, bool isEnabled) - { - try - { - if (sender is IPlugin plugin) - { - string pluginTypeName = plugin.GetType().FullName; - - // 更新配置状态 - if (!PluginStates.ContainsKey(pluginTypeName) || PluginStates[pluginTypeName] != isEnabled) - { - PluginStates[pluginTypeName] = isEnabled; - _configDirty = true; - - LogHelper.WriteLogToFile($"插件状态变更: {plugin.Name} = {(isEnabled ? "启用" : "禁用")}"); - - // 立即同步保存配置(不再使用延迟自动保存) - SaveConfig(); - LogHelper.WriteLogToFile($"插件 {plugin.Name} 状态已立即保存到配置文件"); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"处理插件状态变更事件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 触发自动保存计时器 - /// - private void TriggerAutoSave() - { - // 重置并启动计时器 - _autoSaveTimer.Stop(); - _autoSaveTimer.Start(); - } - - /// - /// 启动热重载监视器 - /// - private void StartHotReloadWatcher() - { - // 创建定时检查任务 - Task.Run(async () => - { - while (true) - { - try - { - // 每5秒检查一次 - await Task.Delay(5000); - - // 获取所有外部插件 - var externalPlugins = Plugins.OfType() - .Where(p => !p.IsBuiltIn && !string.IsNullOrEmpty(p.PluginPath)) - .ToList(); - - foreach (var plugin in externalPlugins) - { - // 检查插件文件是否存在 - if (!File.Exists(plugin.PluginPath)) - { - continue; - } - - // 计算当前文件哈希 - string currentHash = CalculateFileHash(plugin.PluginPath); - - // 比较哈希值是否变化 - if (_pluginHashes.TryGetValue(plugin.PluginPath, out string oldHash) && - !string.IsNullOrEmpty(oldHash) && - oldHash != currentHash) - { - // 文件已变化,执行热重载 - Application.Current.Dispatcher.Invoke(() => - { - ReloadPlugin(plugin); - }); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"热重载检查出错: {ex.Message}", LogHelper.LogType.Error); - } - } - }); - } - - /// - /// 重新加载插件 - /// - /// 要重新加载的插件 - private void ReloadPlugin(PluginBase plugin) - { - try - { - string pluginPath = plugin.PluginPath; - if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath)) - { - LogHelper.WriteLogToFile($"无法重新加载插件 {plugin.Name}: 插件文件不存在", LogHelper.LogType.Error); - return; - } - - LogHelper.WriteLogToFile($"开始热重载插件: {plugin.Name} ({Path.GetFileName(pluginPath)})"); - - // 保存插件的当前状态 - bool wasEnabled = plugin.IsEnabled; - string pluginTypeName = plugin.GetType().FullName; - - // 卸载插件 - UnloadPlugin(plugin); - - // 从加载缓存中移除 - if (_loadedAssemblies.ContainsKey(pluginPath)) - { - _loadedAssemblies.Remove(pluginPath); - } - - // 计算新的文件哈希 - string newHash = CalculateFileHash(pluginPath); - _pluginHashes[pluginPath] = newHash; - - // 重新加载插件 - IPlugin newPlugin = LoadExternalPlugin(pluginPath); - - if (newPlugin != null) - { - // 恢复插件状态 - if (wasEnabled) - { - newPlugin.Enable(); - } - - // 更新配置(如果类型名称变化) - string newPluginTypeName = newPlugin.GetType().FullName; - if (pluginTypeName != newPluginTypeName && PluginStates.ContainsKey(pluginTypeName)) - { - bool state = PluginStates[pluginTypeName]; - PluginStates.Remove(pluginTypeName); - PluginStates[newPluginTypeName] = state; - _configDirty = true; - SaveConfig(); - } - - LogHelper.WriteLogToFile($"插件 {newPlugin.Name} v{newPlugin.Version} 热重载成功"); - - // 通知UI刷新 - NotifyUIRefresh(); - } - else - { - LogHelper.WriteLogToFile($"插件 {plugin.Name} 热重载失败", LogHelper.LogType.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重新加载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 卸载插件 - /// - /// 要卸载的插件 - /// 是否从配置中移除 - public void UnloadPlugin(IPlugin plugin, bool removeFromConfig = false) - { - try - { - // 保存插件名称,以便在卸载后使用 - string pluginName = plugin.Name; - - // 如果插件已启用,先禁用它 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - plugin.Disable(); - } - - // 执行插件清理 - plugin.Cleanup(); - - // 从插件集合中移除 - Plugins.Remove(plugin); - - // 从配置中移除(如果需要) - if (removeFromConfig && plugin.GetType() != null) - { - string pluginTypeName = plugin.GetType().FullName; - if (PluginStates.ContainsKey(pluginTypeName)) - { - PluginStates.Remove(pluginTypeName); - SaveConfig(); - } - } - - LogHelper.WriteLogToFile($"已卸载插件: {pluginName}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"卸载插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 删除插件 - /// - /// 要删除的插件 - /// 删除是否成功 - public bool DeletePlugin(IPlugin plugin) - { - try - { - // 只能删除外部插件 - if (plugin.IsBuiltIn) - { - return false; - } - - // 保存插件名称,以便在删除后使用 - string pluginName = plugin.Name; - - // 获取插件路径 - string pluginPath = null; - if (plugin is PluginBase pluginBase) - { - pluginPath = pluginBase.PluginPath; - } - - if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath)) - { - return false; - } - - // 卸载插件(并从配置中移除状态) - UnloadPlugin(plugin, true); - - // 删除插件文件 - File.Delete(pluginPath); - - // 清理缓存 - _loadedAssemblies.Remove(pluginPath); - _pluginHashes.Remove(pluginPath); - - // 保存配置 - SaveConfig(); - - LogHelper.WriteLogToFile($"已删除插件: {pluginName}"); - return true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除插件时出错: {ex.Message}", LogHelper.LogType.Error); - return false; - } - } - - /// - /// 切换插件启用状态 - /// - /// 目标插件 - /// 是否启用 - public void TogglePlugin(IPlugin plugin, bool enable) - { - try - { - // 检查当前状态是否已经是目标状态 - bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - if (currentState == enable) - { - // 已经是目标状态,无需操作 - LogHelper.WriteLogToFile($"插件 {plugin.Name} 已经是 {(enable ? "启用" : "禁用")} 状态,无需切换"); - return; - } - - // 记录插件信息,用于日志 - string pluginName = plugin.Name; - string pluginTypeName = plugin.GetType().FullName; - - LogHelper.WriteLogToFile($"开始切换插件 {pluginName} 状态为: {(enable ? "启用" : "禁用")}"); - - // 首先更新配置状态 - PluginStates[pluginTypeName] = enable; - _configDirty = true; - - // 更新插件状态 - try - { - // 注册事件(无需检查事件是否为null) - if (plugin is PluginBase pb) - { - // 先取消可能已有的订阅,避免重复订阅 - pb.EnabledStateChanged -= Plugin_EnabledStateChanged; - // 重新订阅 - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - - // 更新插件状态 - if (enable) - { - plugin.Enable(); - LogHelper.WriteLogToFile($"插件 {pluginName} 已启用"); - } - else - { - // 禁用前先记录是否为内置插件 - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {pluginName}"); - - // 禁用插件 - plugin.Disable(); - - // 禁用后立即检查状态,确保禁用成功 - bool actuallyDisabled = !(plugin is PluginBase pb2 && pb2.IsEnabled); - if (!actuallyDisabled) - { - LogHelper.WriteLogToFile($"警告: 插件 {pluginName} 禁用失败,再次尝试禁用", LogHelper.LogType.Warning); - plugin.Disable(); // 再次尝试禁用 - - // 再次检查 - actuallyDisabled = !(plugin is PluginBase pb3 && pb3.IsEnabled); - if (!actuallyDisabled) - { - LogHelper.WriteLogToFile($"错误: 插件 {pluginName} 禁用失败,强制设置禁用状态", LogHelper.LogType.Error); - // 强制设置状态 - if (plugin is PluginBase pb4) - { - // 使用反射强制设置禁用状态 - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(pb4, false); - LogHelper.WriteLogToFile($"已通过反射强制设置插件 {pluginName} 为禁用状态"); - } - } - } - } - - LogHelper.WriteLogToFile($"插件 {pluginName} 已禁用"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"更改插件 {pluginName} 状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - - // 立即保存配置 - SaveConfigAsync().ConfigureAwait(false); - - // 插件状态切换后,始终进行重载(无论是启用还是禁用) - if (plugin is PluginBase pluginInstance) - { - // 对于内置插件,执行专门的处理 - if (pluginInstance.IsBuiltIn) - { - LogHelper.WriteLogToFile($"处理内置插件 {pluginName} 状态变更"); - - // 对于内置插件,我们需要确保状态正确应用 - bool finalState = pluginInstance.IsEnabled; - bool expectedState = enable; - - if (finalState != expectedState) - { - LogHelper.WriteLogToFile($"内置插件状态不匹配: 当前={finalState}, 期望={expectedState},尝试纠正", LogHelper.LogType.Warning); - - // 再次尝试设置状态 - if (expectedState) - { - plugin.Enable(); - } - else - { - plugin.Disable(); - - // 最后一次检查,如果仍然不匹配,强制设置 - if (pluginInstance.IsEnabled != expectedState) - { - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(pluginInstance, expectedState); - LogHelper.WriteLogToFile($"已通过反射强制设置内置插件 {pluginName} 状态为 {(expectedState ? "启用" : "禁用")}"); - } - } - } - } - - // 通知UI刷新 - NotifyUIRefresh(); - } - else - { - // 外部插件,执行热重载 - try - { - if (!string.IsNullOrEmpty(pluginInstance.PluginPath) && File.Exists(pluginInstance.PluginPath)) - { - LogHelper.WriteLogToFile($"开始重载外部插件 {pluginName}"); - - // 使用调度器确保在UI线程执行热重载 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - ReloadPlugin(pluginInstance); - LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态"); - })); - } - else - { - // 当前不在UI线程,直接重载 - ReloadPlugin(pluginInstance); - LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态"); - } - } - else - { - LogHelper.WriteLogToFile($"外部插件 {pluginName} 文件不存在,无法重载", LogHelper.LogType.Warning); - NotifyUIRefresh(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重载插件 {pluginName} 时出错: {ex.Message}", LogHelper.LogType.Error); - // 出错时也要刷新UI - NotifyUIRefresh(); - } - } - } - else - { - // 通知UI刷新 - NotifyUIRefresh(); - } - - LogHelper.WriteLogToFile($"插件 {pluginName} 状态切换完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"切换插件状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 应用插件实时状态 - /// - /// 目标插件 - /// 是否启用 - private void ApplyPluginRealTimeState(IPlugin plugin, bool enable) - { - try - { - // 确保当前实例状态正确 - bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - if (currentState != enable) - { - if (enable) - { - plugin.Enable(); - LogHelper.WriteLogToFile($"实时应用: 已启用插件 {plugin.Name}"); - } - else - { - plugin.Disable(); - LogHelper.WriteLogToFile($"实时应用: 已禁用插件 {plugin.Name}"); - } - - // 同步状态到插件自身的配置 - if (plugin is PluginBase pluginSettings) - { - try - { - // 保存插件设置(与启用状态无关) - pluginSettings.SavePluginSettings(); - LogHelper.WriteLogToFile($"实时应用: 已保存插件 {plugin.Name} 设置"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"实时应用: 保存插件 {plugin.Name} 设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - // 对于外部插件,尝试执行热重载以确保状态立即生效 - if (plugin is PluginBase externalPlugin && !externalPlugin.IsBuiltIn) - { - string pluginPath = externalPlugin.PluginPath; - if (!string.IsNullOrEmpty(pluginPath) && File.Exists(pluginPath)) - { - // 记录插件类型名称,用于后续状态检查 - string pluginTypeName = plugin.GetType().FullName; - bool targetState = enable; - - // 使用调度器确保在UI线程执行热重载 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - try - { - // 热重载前再次确认配置状态正确 - if (PluginStates.TryGetValue(pluginTypeName, out bool storedStateUi) && storedStateUi != targetState) - { - LogHelper.WriteLogToFile($"热重载前发现状态不一致,修正配置: {plugin.Name}, 配置={storedStateUi}, 目标={targetState}", LogHelper.LogType.Warning); - PluginStates[pluginTypeName] = targetState; - SaveConfig(); - } - - // 执行热重载 - ReloadPlugin(externalPlugin); - LogHelper.WriteLogToFile($"插件 {plugin.Name} 已成功热重载以应用实时状态"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"热重载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - })); - } - else - { - // 当前不在UI线程,直接重载 - // 热重载前再次确认配置状态正确 - if (PluginStates.TryGetValue(pluginTypeName, out bool storedStateNonUi) && storedStateNonUi != targetState) - { - LogHelper.WriteLogToFile($"热重载前发现状态不一致,修正配置: {plugin.Name}, 配置={storedStateNonUi}, 目标={targetState}", LogHelper.LogType.Warning); - PluginStates[pluginTypeName] = targetState; - SaveConfig(); - } - - ReloadPlugin(externalPlugin); - } - } - } - - LogHelper.WriteLogToFile($"插件 {plugin.Name} 实时状态已应用: {(enable ? "启用" : "禁用")}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用插件实时状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 通知UI刷新 - /// - private void NotifyUIRefresh() - { - try - { - // 通知UI刷新 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - // 通知任何可能打开的插件设置窗口刷新 - foreach (Window window in Application.Current.Windows) - { - if (window is PluginSettingsWindow pluginWindow) - { - pluginWindow.RefreshPluginList(); - break; - } - } - })); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"通知UI刷新时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载插件配置 - /// - private void LoadConfig() - { - const int maxRetries = 3; // 最大重试次数 - const int retryDelayMs = 300; // 重试延迟时间(毫秒) - - LogHelper.WriteLogToFile($"开始从配置文件加载插件状态: {PluginConfigFile}"); - - // 确保至少有一个默认配置 - Dictionary defaultConfig = new Dictionary(); - - // 尝试获取配置锁 - _configLock.Wait(); - - try - { - for (int attempt = 1; attempt <= maxRetries; attempt++) - { - try - { - if (File.Exists(PluginConfigFile)) - { - string json; - // 使用共享读取模式,允许其他进程同时读取但不允许写入 - using (FileStream fs = new FileStream(PluginConfigFile, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (StreamReader reader = new StreamReader(fs)) - { - json = reader.ReadToEnd(); - } - - var loadedStates = JsonConvert.DeserializeObject>(json); - - if (loadedStates != null && loadedStates.Count > 0) - { - PluginStates = loadedStates; - _configDirty = false; // 重置脏标记 - LogHelper.WriteLogToFile($"成功从配置文件加载了 {PluginStates.Count} 个插件状态"); - } - else - { - LogHelper.WriteLogToFile("配置文件解析为空,尝试使用备份", LogHelper.LogType.Warning); - // 尝试加载备份 - if (File.Exists(PluginConfigBackupFile)) - { - try - { - string backupJson = File.ReadAllText(PluginConfigBackupFile); - var backupStates = JsonConvert.DeserializeObject>(backupJson); - - if (backupStates != null && backupStates.Count > 0) - { - PluginStates = backupStates; - _configDirty = true; // 从备份加载,需要重新保存主配置 - LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态"); - return; // 成功从备份加载,提前退出 - } - } - catch (Exception backupEx) - { - LogHelper.WriteLogToFile($"从备份恢复配置失败: {backupEx.Message}", LogHelper.LogType.Error); - } - } - - // 备份也失败,使用默认配置 - PluginStates = defaultConfig; - _configDirty = true; - } - } - else - { - LogHelper.WriteLogToFile($"配置文件不存在,尝试使用备份: {PluginConfigFile}", LogHelper.LogType.Warning); - - // 尝试加载备份 - if (File.Exists(PluginConfigBackupFile)) - { - try - { - string backupJson = File.ReadAllText(PluginConfigBackupFile); - var backupStates = JsonConvert.DeserializeObject>(backupJson); - - if (backupStates != null && backupStates.Count > 0) - { - PluginStates = backupStates; - _configDirty = true; // 从备份加载,需要重新保存主配置 - LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态"); - return; // 成功从备份加载,提前退出 - } - } - catch (Exception backupEx) - { - LogHelper.WriteLogToFile($"从备份恢复配置失败: {backupEx.Message}", LogHelper.LogType.Error); - } - } - - PluginStates = defaultConfig; - _configDirty = true; - LogHelper.WriteLogToFile("使用默认空配置", LogHelper.LogType.Warning); - } - - // 没有成功加载或使用备份,使用默认配置 - break; - } - catch (Exception ex) - { - if (attempt < maxRetries) - { - LogHelper.WriteLogToFile($"加载配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); - Thread.Sleep(retryDelayMs); - } - else - { - LogHelper.WriteLogToFile($"加载插件配置失败,已达最大重试次数 ({maxRetries}): {ex.Message}", LogHelper.LogType.Error); - - // 最终失败,使用默认配置 - PluginStates = defaultConfig; - _configDirty = true; - } - } - } - } - finally - { - // 释放配置锁 - _configLock.Release(); - } - } - - /// - /// 异步保存插件配置 - /// - public async Task SaveConfigAsync() - { - // 如果配置没有变化,无需保存 - if (!_configDirty) - { - return; - } - - // 尝试获取配置锁(异步) - if (!await _configLock.WaitAsync(0)) - { - // 已有保存操作在进行中,触发自动保存延迟 - TriggerAutoSave(); - return; - } - - try - { - // 创建配置任务 - await Task.Run(() => SaveConfig()); - } - finally - { - // 释放配置锁 - _configLock.Release(); - } - } - - /// - /// 保存插件配置 - /// - public void SaveConfig() - { - // 如果配置没有变化,无需保存 - if (!_configDirty) - { - return; - } - - const int maxRetries = 3; // 最大重试次数 - const int retryDelayMs = 500; // 重试延迟时间(毫秒) - - try - { - LogHelper.WriteLogToFile($"开始保存插件配置到: {PluginConfigFile}"); - - // 生成JSON数据 - string json = JsonConvert.SerializeObject(PluginStates, Formatting.Indented); - string tempFile = PluginConfigFile + ".temp"; // 临时文件路径 - - // 确保目录存在 - string configDir = Path.GetDirectoryName(PluginConfigFile); - if (!Directory.Exists(configDir)) - { - Directory.CreateDirectory(configDir); - LogHelper.WriteLogToFile($"创建配置目录: {configDir}"); - } - - // 先备份当前配置 - try - { - if (File.Exists(PluginConfigFile)) - { - File.Copy(PluginConfigFile, PluginConfigBackupFile, true); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"备份配置文件失败: {ex.Message}", LogHelper.LogType.Warning); - } - - for (int attempt = 1; attempt <= maxRetries; attempt++) - { - try - { - // 直接写入目标文件 - File.WriteAllText(PluginConfigFile, json); - - // 验证写入是否成功 - if (File.Exists(PluginConfigFile)) - { - // 重置脏标记 - _configDirty = false; - LogHelper.WriteLogToFile($"插件配置已成功保存到磁盘: {PluginConfigFile}, 共 {PluginStates.Count} 个插件状态"); - return; - } - } - catch (Exception ex) - { - if (attempt < maxRetries) - { - LogHelper.WriteLogToFile($"保存配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); - Thread.Sleep(retryDelayMs); - } - else - { - LogHelper.WriteLogToFile($"保存插件配置失败,已达最大重试次数 ({maxRetries}): {ex.Message}", LogHelper.LogType.Error); - - // 尝试使用临时文件方式 - try - { - // 删除可能存在的旧临时文件 - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - - // 写入临时文件 - File.WriteAllText(tempFile, json); - - // 如果目标文件存在,先删除 - if (File.Exists(PluginConfigFile)) - { - File.Delete(PluginConfigFile); - } - - // 重命名临时文件 - File.Move(tempFile, PluginConfigFile); - - // 重置脏标记 - _configDirty = false; - LogHelper.WriteLogToFile($"使用临时文件方式成功保存配置: {PluginConfigFile}"); - return; - } - catch (Exception fallbackEx) - { - LogHelper.WriteLogToFile($"临时文件保存方式也失败: {fallbackEx.Message}", LogHelper.LogType.Error); - } - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存插件配置时发生未处理异常: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 计算文件哈希 - /// - /// 文件路径 - /// 文件哈希值 - private string CalculateFileHash(string filePath) - { - try - { - using (var md5 = MD5.Create()) - using (var stream = File.OpenRead(filePath)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"计算文件哈希值时出错: {ex.Message}", LogHelper.LogType.Error); - return string.Empty; - } - } - - /// - /// 从配置文件重新加载所有插件状态并应用 - /// - public void ReloadPluginsFromConfig() - { - try - { - LogHelper.WriteLogToFile("开始从配置文件重新加载插件状态"); - - // 保存当前配置状态,以便在加载失败时回滚 - Dictionary previousStates = new Dictionary(PluginStates); - - // 重新加载配置文件 - LoadConfig(); - - // 如果配置文件加载失败,PluginStates可能为空,这时使用之前的状态 - if (PluginStates == null || PluginStates.Count == 0) - { - LogHelper.WriteLogToFile("加载的配置为空,恢复到之前的状态", LogHelper.LogType.Warning); - PluginStates = previousStates; - return; - } - - LogHelper.WriteLogToFile($"已加载 {PluginStates.Count} 个插件状态,开始应用..."); - - // 对比配置,查找变更的插件 - foreach (var plugin in Plugins.ToList()) // 创建副本进行遍历,避免集合修改异常 - { - string pluginTypeName = plugin.GetType().FullName; - - // 检查插件在配置中是否存在 - if (PluginStates.TryGetValue(pluginTypeName, out bool shouldBeEnabled)) - { - bool currentlyEnabled = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - - // 如果状态需要变更 - if (currentlyEnabled != shouldBeEnabled) - { - LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 的配置状态: {(shouldBeEnabled ? "启用" : "禁用")}"); - - if (shouldBeEnabled) - { - try - { - plugin.Enable(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - else - { - try - { - // 记录禁用信息,特别是内置插件 - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}"); - - // 禁用插件 - plugin.Disable(); - - // 对于内置插件,特别检查禁用状态 - if (isBuiltIn && plugin is PluginBase builtInPluginBase) - { - if (builtInPluginBase.IsEnabled) - { - LogHelper.WriteLogToFile($"内置插件 {plugin.Name} 禁用失败,尝试强制禁用", LogHelper.LogType.Warning); - // 强制设置禁用状态 - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(builtInPluginBase, false); - LogHelper.WriteLogToFile($"已通过反射强制禁用内置插件 {plugin.Name}"); - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - // 如果是外部插件,执行重载 - if (!plugin.IsBuiltIn && plugin is PluginBase externalPlugin && !string.IsNullOrEmpty(externalPlugin.PluginPath)) - { - try - { - ReloadPlugin(externalPlugin); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重载外部插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - } - else - { - // 插件不在配置中,将其添加为禁用状态 - PluginStates[pluginTypeName] = false; - LogHelper.WriteLogToFile($"插件 {plugin.Name} 不在配置中,默认设置为禁用状态"); - - // 如果当前是启用状态,则禁用它 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - try - { - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用未配置的{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}"); - - plugin.Disable(); - - // 对于内置插件,特别检查禁用状态 - if (isBuiltIn && pluginBase.IsEnabled) - { - LogHelper.WriteLogToFile($"未配置的内置插件 {plugin.Name} 禁用失败,尝试强制禁用", LogHelper.LogType.Warning); - // 强制设置禁用状态 - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(pluginBase, false); - LogHelper.WriteLogToFile($"已通过反射强制禁用未配置的内置插件 {plugin.Name}"); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用未配置插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - } - - // 保存更新后的配置 - SaveConfig(); - - // 通知UI更新 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.Invoke(() => - { - // 通知任何可能打开的插件设置窗口刷新 - foreach (Window window in Application.Current.Windows) - { - if (window is PluginSettingsWindow pluginWindow) - { - pluginWindow.RefreshPluginList(); - } - } - }); - } - - LogHelper.WriteLogToFile("插件状态已从配置文件重新加载完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从配置文件重新加载插件状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs b/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs deleted file mode 100644 index 9975998e..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs +++ /dev/null @@ -1,509 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件服务管理器,实现IPluginService接口,提供对软件内部功能的访问 - /// - public class PluginServiceManager : IPluginService - { - private static PluginServiceManager _instance; - private MainWindow _mainWindow; - private Dictionary _eventHandlers; - - /// - /// 单例实例 - /// - public static PluginServiceManager Instance - { - get - { - if (_instance == null) - { - _instance = new PluginServiceManager(); - } - return _instance; - } - } - - private PluginServiceManager() - { - _eventHandlers = new Dictionary(); - } - - /// - /// 设置主窗口引用 - /// - /// 主窗口实例 - public void SetMainWindow(MainWindow mainWindow) - { - _mainWindow = mainWindow; - } - - #region 窗口和UI访问 - - public Window MainWindow => _mainWindow; - - public InkCanvas CurrentCanvas => null; // 暂时返回null,避免访问权限问题 - - public List AllCanvasPages => new List(); // 暂时返回空列表 - - public int CurrentPageIndex => 0; // 暂时返回0 - - public int TotalPageCount => 0; // 暂时返回0 - - public FrameworkElement FloatingToolBar => _mainWindow?.ViewboxFloatingBar; - - public FrameworkElement LeftPanel => _mainWindow?.BlackboardLeftSide; - - public FrameworkElement RightPanel => _mainWindow?.BlackboardRightSide; - - public FrameworkElement TopPanel => _mainWindow?.BorderTools; - - public FrameworkElement BottomPanel => _mainWindow?.BorderSettings; - - #endregion - - #region 绘制工具状态 - - public int CurrentDrawingMode => 0; // 暂时返回0 - - public double CurrentInkWidth => 2.5; // 暂时返回默认值 - - public Color CurrentInkColor => Colors.Black; // 暂时返回默认值 - - public double CurrentHighlighterWidth => 20.0; // 暂时返回默认值 - - public int CurrentEraserSize => 2; // 暂时返回默认值 - - public int CurrentEraserType => 0; // 暂时返回默认值 - - public int CurrentEraserShape => 0; // 暂时返回默认值 - - public double CurrentInkAlpha => 255.0; // 暂时返回默认值 - - public int CurrentInkStyle => 0; // 暂时返回默认值 - - public string CurrentBackgroundColor => "#162924"; // 暂时返回默认值 - - #endregion - - #region 应用状态 - - public bool IsDarkTheme => false; // 暂时返回默认值 - - public bool IsWhiteboardMode => false; // 暂时返回默认值 - - public bool IsPPTMode => false; // 暂时返回默认值 - - public bool IsFullScreenMode => false; // 暂时返回默认值 - - public bool IsCanvasMode => true; // 暂时返回默认值 - - public bool IsSelectionMode => false; // 暂时返回默认值 - - public bool IsEraserMode => false; // 暂时返回默认值 - - public bool IsShapeDrawingMode => false; // 暂时返回默认值 - - public bool IsHighlighterMode => false; // 暂时返回默认值 - - #endregion - - #region IGetService 实现 - - public bool CanUndo => false; // 暂时返回默认值 - - public bool CanRedo => false; // 暂时返回默认值 - - public T GetSetting(string key, T defaultValue = default(T)) - { - // 暂时不实现,避免访问权限问题 - return defaultValue; - } - - public List GetAllPlugins() - { - return new List(PluginManager.Instance.Plugins); - } - - public IPlugin GetPlugin(string pluginName) - { - return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName); - } - - #endregion - - #region IWindowService 实现 - - public void ShowSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HideSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowPluginSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HidePluginSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowHelpWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HideHelpWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowAboutWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HideAboutWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowNotification(string message, NotificationType type = NotificationType.Info) - { - // 暂时不实现,避免访问权限问题 - } - - public bool ShowConfirmDialog(string message, string title = "确认") - { - // 暂时不实现,避免访问权限问题 - return false; - } - - public string ShowInputDialog(string message, string title = "输入", string defaultValue = "") - { - // 暂时不实现,避免访问权限问题 - return defaultValue; - } - - public void SetFullScreen(bool isFullScreen) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetTopMost(bool isTopMost) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetWindowVisibility(bool isVisible) - { - // 暂时不实现,避免访问权限问题 - } - - public void MinimizeWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void MaximizeWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void RestoreWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void CloseWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void SetWindowPosition(double x, double y) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetWindowSize(double width, double height) - { - // 暂时不实现,避免访问权限问题 - } - - public (double x, double y) GetWindowPosition() - { - // 暂时不实现,避免访问权限问题 - return (0, 0); - } - - public (double width, double height) GetWindowSize() - { - // 暂时不实现,避免访问权限问题 - return (800, 600); - } - - #endregion - - #region IActionService 实现 - - public void ClearCanvas() - { - // 暂时不实现,避免访问权限问题 - } - - public void ClearAllCanvases() - { - // 暂时不实现,避免访问权限问题 - } - - public void AddNewPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void DeleteCurrentPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void SwitchToPage(int pageIndex) - { - // 暂时不实现,避免访问权限问题 - } - - public void NextPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void PreviousPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void SetDrawingMode(int mode) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkWidth(double width) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkColor(Color color) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetHighlighterWidth(double width) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetEraserSize(int size) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetEraserType(int type) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetEraserShape(int shape) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkAlpha(double alpha) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkStyle(int style) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetBackgroundColor(string color) - { - // 暂时不实现,避免访问权限问题 - } - - public void SaveCanvas(string filePath) - { - // 暂时不实现,避免访问权限问题 - } - - public void LoadCanvas(string filePath) - { - // 暂时不实现,避免访问权限问题 - } - - public void ExportAsImage(string filePath, string format) - { - // 暂时不实现,避免访问权限问题 - } - - public void ExportAsPDF(string filePath) - { - // 暂时不实现,避免访问权限问题 - } - - public void Undo() - { - // 暂时不实现,避免访问权限问题 - } - - public void Redo() - { - // 暂时不实现,避免访问权限问题 - } - - public void SelectAll() - { - // 暂时不实现,避免访问权限问题 - } - - public void DeselectAll() - { - // 暂时不实现,避免访问权限问题 - } - - public void DeleteSelected() - { - // 暂时不实现,避免访问权限问题 - } - - public void CopySelected() - { - // 暂时不实现,避免访问权限问题 - } - - public void CutSelected() - { - // 暂时不实现,避免访问权限问题 - } - - public void Paste() - { - // 暂时不实现,避免访问权限问题 - } - - public void SetSetting(string key, T value) - { - // 暂时不实现,避免访问权限问题 - } - - public void SaveSettings() - { - // 暂时不实现,避免访问权限问题 - } - - public void LoadSettings() - { - // 暂时不实现,避免访问权限问题 - } - - public void ResetSettings() - { - // 暂时不实现,避免访问权限问题 - } - - public void EnablePlugin(string pluginName) - { - var plugin = GetPlugin(pluginName); - if (plugin != null) - { - PluginManager.Instance.TogglePlugin(plugin, true); - } - } - - public void DisablePlugin(string pluginName) - { - var plugin = GetPlugin(pluginName); - if (plugin != null) - { - PluginManager.Instance.TogglePlugin(plugin, false); - } - } - - public void UnloadPlugin(string pluginName) - { - var plugin = GetPlugin(pluginName); - if (plugin != null) - { - PluginManager.Instance.UnloadPlugin(plugin); - } - } - - public void RegisterEventHandler(string eventName, EventHandler handler) - { - if (!_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName] = handler; - } - else - { - _eventHandlers[eventName] += handler; - } - } - - public void UnregisterEventHandler(string eventName, EventHandler handler) - { - if (_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName] -= handler; - } - } - - public void TriggerEvent(string eventName, object sender, EventArgs args) - { - if (_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName]?.Invoke(sender, args); - } - } - - public void RestartApplication() - { - // 暂时不实现,避免访问权限问题 - } - - public void ExitApplication() - { - // 暂时不实现,避免访问权限问题 - } - - public void CheckForUpdates() - { - // 暂时不实现,避免访问权限问题 - } - - public void OpenHelpDocument() - { - // 暂时不实现,避免访问权限问题 - } - - public void OpenAboutPage() - { - // 暂时不实现,避免访问权限问题 - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginTemplate.cs b/Ink Canvas/Helpers/Plugins/PluginTemplate.cs deleted file mode 100644 index bd6979d8..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginTemplate.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件模板,用于开发者参考 - /// 注意:实际开发时,请将此类移到单独的程序集中 - /// - public class PluginTemplate : PluginBase - { - #region 插件基本信息 - - /// - /// 插件名称 - /// - public override string Name => "插件模板"; - - /// - /// 插件描述 - /// - public override string Description => "这是一个插件开发模板,用于开发者参考。"; - - /// - /// 插件版本 - /// - public override Version Version => new Version(1, 0, 0); - - /// - /// 插件作者 - /// - public override string Author => "Your Name"; - - /// - /// 是否为内置插件(外部插件请返回false) - /// - public override bool IsBuiltIn => false; - - #endregion - - #region 插件生命周期 - - /// - /// 插件初始化 - /// 在这里进行插件的初始化工作,如加载配置、注册事件等 - /// - public override void Initialize() - { - // 先调用基类方法,这样会设置插件ID和记录日志 - base.Initialize(); - - // TODO: 在这里进行插件初始化工作 - - // 示例:记录初始化信息 - LogHelper.WriteLogToFile($"插件 {Name} 开始初始化"); - - // 示例:加载配置 - LoadConfig(); - - // 示例:注册自定义事件 - // MainWindow.Instance.SomeEvent += OnSomeEvent; - - LogHelper.WriteLogToFile($"插件 {Name} 初始化完成"); - } - - /// - /// 启用插件 - /// 在这里激活插件功能 - /// - public override void Enable() - { - // 先调用基类方法,这样会设置插件状态和记录日志 - base.Enable(); - - // TODO: 在这里启用插件功能 - - LogHelper.WriteLogToFile($"插件 {Name} 已启用"); - } - - /// - /// 禁用插件 - /// 在这里停用插件功能 - /// - public override void Disable() - { - // 先调用基类方法,这样会设置插件状态和记录日志 - base.Disable(); - - // TODO: 在这里禁用插件功能 - - LogHelper.WriteLogToFile($"插件 {Name} 已禁用"); - } - - /// - /// 清理资源 - /// 在插件卸载时调用,清理资源 - /// - public override void Cleanup() - { - // TODO: 在这里清理插件资源 - - // 示例:取消注册事件 - // MainWindow.Instance.SomeEvent -= OnSomeEvent; - - // 示例:保存配置 - SaveConfig(); - - // 最后调用基类方法 - base.Cleanup(); - } - - #endregion - - #region 插件配置 - - /// - /// 加载插件配置 - /// - private void LoadConfig() - { - try - { - // TODO: 从文件或其他位置加载配置 - // 示例: - // string configPath = Path.Combine(App.RootPath, "PluginConfigs", "YourPluginName.json"); - // if (File.Exists(configPath)) - // { - // string json = File.ReadAllText(configPath); - // YourConfig = Newtonsoft.Json.JsonConvert.DeserializeObject(json); - // } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 保存插件配置 - /// - private void SaveConfig() - { - try - { - // TODO: 保存配置到文件或其他位置 - // 示例: - // string configDir = Path.Combine(App.RootPath, "PluginConfigs"); - // if (!Directory.Exists(configDir)) - // { - // Directory.CreateDirectory(configDir); - // } - // string configPath = Path.Combine(configDir, "YourPluginName.json"); - // string json = Newtonsoft.Json.JsonConvert.SerializeObject(YourConfig, Newtonsoft.Json.Formatting.Indented); - // File.WriteAllText(configPath, json); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 插件设置界面 - - /// - /// 获取插件设置界面 - /// - /// 插件设置界面 - public override UserControl GetSettingsView() - { - // 创建插件设置界面 - return new PluginTemplateSettingsControl(); - } - - #endregion - - #region 插件功能方法 - - // TODO: 在这里添加插件的具体功能方法 - - /// - /// 示例方法:执行一些功能 - /// - public void DoSomething() - { - if (!IsEnabled) return; - - try - { - // TODO: 实现你的功能 - MessageBox.Show("插件功能执行示例", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"执行插件功能时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - } - - /// - /// 插件设置控件 - /// - public class PluginTemplateSettingsControl : UserControl - { - public PluginTemplateSettingsControl() - { - // 创建设置界面布局 - var panel = new StackPanel - { - Margin = new Thickness(10) - }; - - // 添加标题 - panel.Children.Add(new TextBlock - { - Text = "插件模板设置", - FontSize = 16, - FontWeight = FontWeights.Bold, - Margin = new Thickness(0, 0, 0, 10) - }); - - // 添加说明文字 - panel.Children.Add(new TextBlock - { - Text = "这是一个示例设置界面,你可以在这里添加自己的设置控件。", - TextWrapping = TextWrapping.Wrap, - Margin = new Thickness(0, 0, 0, 15) - }); - - // 添加示例设置选项 - var checkBox = new CheckBox - { - Content = "启用某项功能", - Margin = new Thickness(0, 0, 0, 10) - }; - panel.Children.Add(checkBox); - - // 添加文本输入框 - panel.Children.Add(new TextBlock - { - Text = "设置项:", - Margin = new Thickness(0, 5, 0, 5) - }); - - panel.Children.Add(new TextBox - { - Margin = new Thickness(0, 0, 0, 10), - Width = 200, - HorizontalAlignment = HorizontalAlignment.Left - }); - - // 添加按钮 - var button = new Button - { - Content = "保存设置", - Padding = new Thickness(10, 5, 10, 5), - Margin = new Thickness(0, 10, 0, 0), - HorizontalAlignment = HorizontalAlignment.Left - }; - - button.Click += (sender, e) => - { - MessageBox.Show("设置已保存!", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information); - }; - - panel.Children.Add(button); - - // 设置控件内容 - Content = panel; - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/SoftwareLauncher.cs b/Ink Canvas/Helpers/SoftwareLauncher.cs index e3dc7188..395eadf3 100644 --- a/Ink Canvas/Helpers/SoftwareLauncher.cs +++ b/Ink Canvas/Helpers/SoftwareLauncher.cs @@ -1,72 +1,151 @@ -using Microsoft.Win32; +using Microsoft.Win32; using System; using System.Diagnostics; using System.IO; -using System.Runtime.InteropServices; +using System.Windows; namespace Ink_Canvas.Helpers { - internal class SoftwareLauncher + internal static class SoftwareLauncher { - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - + /// 与 ICA 一致:在「程序和功能」卸载列表中按 DisplayName 匹配后启动 sweclauncher.exe。 public static void LaunchEasiCamera(string softwareName) { string executablePath = FindEasiCameraExecutablePath(softwareName); - if (!string.IsNullOrEmpty(executablePath)) + if (string.IsNullOrEmpty(executablePath)) { - try - { - Process.Start(executablePath); - //Console.WriteLine(softwareName + " 启动成功!"); - } - catch (Exception ex) - { - Console.WriteLine("启动失败: " + ex.Message); - //MessageBox.Show("启动失败: " + ex.Message); - } + MessageBox.Show( + "未找到希沃视频展台安装信息(已扫描 64 位与 32 位卸载注册表)。请确认已通过官方安装包安装「希沃视频展台」。", + "Ink Canvas", + MessageBoxButton.OK, + MessageBoxImage.Information); + return; + } + + try + { + var directory = Path.GetDirectoryName(executablePath); + var psi = new ProcessStartInfo + { + FileName = executablePath, + UseShellExecute = true, + WorkingDirectory = string.IsNullOrEmpty(directory) ? Environment.SystemDirectory : directory + }; + Process.Start(psi); + } + catch (Exception ex) + { + MessageBox.Show( + "无法启动希沃视频展台:" + ex.Message, + "Ink Canvas", + MessageBoxButton.OK, + MessageBoxImage.Warning); } - //Console.WriteLine(softwareName + " 未找到可执行文件路径。"); } private static string FindEasiCameraExecutablePath(string softwareName) { - string executablePath = null; + if (string.IsNullOrWhiteSpace(softwareName)) + return null; - using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall")) + // 64 位进程默认只枚举 64 位注册表视图;32 位希沃常写在 WOW6432Node 下,需一并扫描。 + string[] uninstallRoots = { - foreach (string subkeyName in key.GetSubKeyNames()) - { - using (RegistryKey subkey = key.OpenSubKey(subkeyName)) - { - string displayName = subkey.GetValue("DisplayName") as string; - string installLocation = subkey.GetValue("InstallLocation") as string; - string uninstallString = subkey.GetValue("UninstallString") as string; + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", + }; - if (!string.IsNullOrEmpty(displayName) && displayName.Contains(softwareName)) - { - if (!string.IsNullOrEmpty(installLocation)) - { - executablePath = Path.Combine(installLocation, "sweclauncher.exe"); - } - else if (!string.IsNullOrEmpty(uninstallString)) - { - int lastSlashIndex = uninstallString.LastIndexOf("\\"); - if (lastSlashIndex >= 0) - { - string folderPath = uninstallString.Substring(0, lastSlashIndex); - executablePath = Path.Combine(folderPath, "sweclauncher", "sweclauncher.exe"); - } - } - break; - } - } + foreach (string root in uninstallRoots) + { + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(root)) + { + if (key == null) continue; + string found = FindInUninstallKey(key, softwareName); + if (!string.IsNullOrEmpty(found)) + return found; } } - return executablePath; + return null; + } + + private static string FindInUninstallKey(RegistryKey uninstallKey, string softwareName) + { + foreach (string subkeyName in uninstallKey.GetSubKeyNames()) + { + using (RegistryKey subkey = uninstallKey.OpenSubKey(subkeyName)) + { + if (subkey == null) continue; + + string displayName = subkey.GetValue("DisplayName") as string; + if (string.IsNullOrEmpty(displayName) || !displayName.Contains(softwareName)) + continue; + + string installLocation = subkey.GetValue("InstallLocation") as string; + string uninstallString = subkey.GetValue("UninstallString") as string; + + string resolved = TryResolveSweclauncher(installLocation, uninstallString); + if (!string.IsNullOrEmpty(resolved) && File.Exists(resolved)) + return resolved; + } + } + + return null; + } + + private static string TryResolveSweclauncher(string installLocation, string uninstallString) + { + if (!string.IsNullOrWhiteSpace(installLocation)) + { + string fromLoc = ResolveSweclauncherUnderInstallRoot(installLocation.Trim().TrimEnd('\\')); + if (!string.IsNullOrEmpty(fromLoc)) + return fromLoc; + } + + if (!string.IsNullOrWhiteSpace(uninstallString)) + { + // 常见:"...\uninstall.exe" 或带引号路径 + string trimmed = uninstallString.Trim(); + if (trimmed.Length >= 2 && trimmed[0] == '"') + { + int end = trimmed.IndexOf('"', 1); + if (end > 1) + trimmed = trimmed.Substring(1, end - 1); + } + + int lastSlash = trimmed.LastIndexOf('\\'); + if (lastSlash < 0) + return null; + + string folderPath = trimmed.Substring(0, lastSlash); + string candidate = Path.Combine(folderPath, "sweclauncher", "sweclauncher.exe"); + if (File.Exists(candidate)) + return candidate; + + candidate = Path.Combine(folderPath, "sweclauncher.exe"); + if (File.Exists(candidate)) + return candidate; + } + + return null; + } + + private static string ResolveSweclauncherUnderInstallRoot(string installRoot) + { + string[] candidates = + { + Path.Combine(installRoot, "sweclauncher.exe"), + Path.Combine(installRoot, "sweclauncher", "sweclauncher.exe"), + }; + + foreach (string p in candidates) + { + if (File.Exists(p)) + return p; + } + + return null; } } } diff --git a/Ink Canvas/Helpers/TimeMachine.cs b/Ink Canvas/Helpers/TimeMachine.cs index 58b9c546..492c96cc 100644 --- a/Ink Canvas/Helpers/TimeMachine.cs +++ b/Ink Canvas/Helpers/TimeMachine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Windows; using System.Windows.Ink; @@ -140,6 +140,12 @@ namespace Ink_Canvas.Helpers OnUndoStateChanged?.Invoke(_currentIndex > -1); OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0); } + + /// 当前历史是否允许撤销。 + public bool CanUndo => _currentIndex > -1; + + /// 当前历史是否允许重做。 + public bool CanRedo => _currentStrokeHistory.Count > 0 && _currentStrokeHistory.Count - _currentIndex - 1 > 0; } public class TimeMachineHistory diff --git a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs new file mode 100644 index 00000000..8fed3e0b --- /dev/null +++ b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs @@ -0,0 +1,843 @@ +using OSVersionExtension; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using WinAnalysis = global::Windows.UI.Input.Inking.Analysis; +using WinRtInk = global::Windows.UI.Input.Inking; + +namespace Ink_Canvas.Helpers +{ + /// + /// WinRT 手写体识别,以及将识别结果用手写风格字体轮廓转为墨迹笔画(「识别转手写体字形」)。 + /// + internal static class WinRtHandwritingRecognizer + { + private static WinRtInk.InkRecognizer _preferredHandwritingRecognizer; + private static bool _preferredHandwritingRecognizerResolved; + + private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info) + { + LogHelper.WriteLogToFile("[手写体] " + message, logType); + } + + public static bool IsApiAvailable => + OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10; + + public static void Warmup() + { + if (!IsApiAvailable || !Environment.Is64BitProcess) return; + try + { + var d = Application.Current?.Dispatcher; + if (d == null) return; + d.BeginInvoke(new Action(async () => + { + try + { + await RecognizeHandwritingAsync(new StrokeCollection()).ConfigureAwait(true); + } + catch + { + // ignore + } + })); + } + catch + { + // ignore + } + } + + /// + /// 将当前笔画集合识别为文字片段(含候选):先用墨迹分析得到分词与 , + /// 再对每一分词用 GetTextCandidates(与当前 SDK 中部分版本的 + /// 未暴露笔画映射的局限兼容)。 + /// + public static async Task RecognizeHandwritingAsync(StrokeCollection strokes) + { + if (!IsApiAvailable || strokes == null || strokes.Count == 0) + return HandwritingRecognitionResult.Empty; + + var traceRecognition = strokes.Count > 0; + + try + { + var recognizer = new WinRtInk.InkRecognizerContainer(); + TryApplyPreferredHandwritingRecognizer(recognizer, traceRecognition); + + var analyzer = new WinAnalysis.InkAnalyzer(); + var idToWpf = new Dictionary(); + + foreach (Stroke s in strokes) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s); + if (ink == null) continue; + analyzer.AddDataForStroke(ink); + analyzer.SetStrokeDataKind(ink.Id, WinAnalysis.InkAnalysisStrokeKind.Writing); + idToWpf[ink.Id] = s; + } + + if (idToWpf.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:无有效 WinRT 笔画(全部转换失败),输入笔画数=" + strokes.Count); + return HandwritingRecognitionResult.Empty; + } + + var analysisResult = await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true); + if (analysisResult == null || analysisResult.Status != WinAnalysis.InkAnalysisStatus.Updated) + { + if (traceRecognition) + LogHandwriting( + "识别:AnalyzeAsync 未得到 Updated,Status=" + + (analysisResult == null ? "null" : analysisResult.Status.ToString()) + + ",有效笔画数=" + idToWpf.Count + + ",尝试整批 RecognizeAsync 回退。"); + return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true); + } + + var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord); + if (wordNodes == null || wordNodes.Count == 0) + { + if (traceRecognition) + LogHandwriting( + "识别:未找到 InkWord 节点(墨迹分析常将非横平笔划判为绘图),有效笔画数=" + idToWpf.Count + + ",改用整批 RecognizeAsync 回退。"); + return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true); + } + + var segments = new List(); + + foreach (var node in wordNodes) + { + if (!(node is WinAnalysis.InkAnalysisInkWord word)) + continue; + + var ids = word.GetStrokeIds(); + if (ids == null || ids.Count == 0) + continue; + + var group = new List(); + foreach (var sid in ids) + { + if (idToWpf.TryGetValue(sid, out var st)) + group.Add(st); + } + + if (group.Count == 0) + continue; + + var wbr = word.BoundingRect; + var wpfRect = new Rect(wbr.X, wbr.Y, wbr.Width, wbr.Height); + var analysisText = word.RecognizedText ?? string.Empty; + + IReadOnlyList candList = Array.Empty(); + try + { + if (recognizer != null) + { + var mini = new WinRtInk.InkStrokeContainer(); + foreach (var st in group) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(st); + if (ink != null) + mini.AddStroke(ink); + } + + var miniStrokes = mini.GetStrokes(); + if (miniStrokes != null && miniStrokes.Count > 0) + { + var rr = await recognizer + .RecognizeAsync(mini, WinRtInk.InkRecognitionTarget.All) + .AsTask() + .ConfigureAwait(true); + if (rr != null && rr.Count > 0 && rr[0] != null) + { + var cands = rr[0].GetTextCandidates(); + if (cands != null && cands.Count > 0) + candList = cands.ToList(); + } + } + } + } + catch + { + candList = Array.Empty(); + } + + var primary = candList.Count > 0 ? candList[0] : analysisText; + var mergedCandidates = new List(); + if (candList.Count > 0) + { + foreach (var c in candList) + { + if (!string.IsNullOrEmpty(c) && !mergedCandidates.Contains(c)) + mergedCandidates.Add(c); + } + } + + if (!string.IsNullOrEmpty(analysisText) && !mergedCandidates.Contains(analysisText)) + mergedCandidates.Insert(0, analysisText); + + if (mergedCandidates.Count == 0 && !string.IsNullOrEmpty(primary)) + mergedCandidates.Add(primary); + + segments.Add(new HandwritingWordSegment( + primary, + mergedCandidates, + wpfRect, + group)); + } + + if (segments.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:分词列表为空(InkWord 无有效笔画映射)。"); + return HandwritingRecognitionResult.Empty; + } + + var hr = new HandwritingRecognitionResult(segments); + if (traceRecognition) + { + var preview = hr.CombinedText; + if (preview.Length > 120) + preview = preview.Substring(0, 117) + "..."; + LogHandwriting( + "识别成功:词数=" + segments.Count + + ",合并文本=\"" + preview + "\"" + + ",进程位数=" + (Environment.Is64BitProcess ? "x64" : "x86")); + for (var i = 0; i < segments.Count; i++) + { + var seg = segments[i]; + var t = seg.Text ?? ""; + if (t.Length > 40) + t = t.Substring(0, 37) + "..."; + LogHandwriting( + " 词[" + i + "] 文本=\"" + t + "\",笔画数=" + seg.Strokes.Count + + ",候选数=" + (seg.TextCandidates?.Count ?? 0) + + ",框=(" + Math.Round(seg.BoundingRectangle.X, 1) + "," + + Math.Round(seg.BoundingRectangle.Y, 1) + "," + + Math.Round(seg.BoundingRectangle.Width, 1) + "×" + + Math.Round(seg.BoundingRectangle.Height, 1) + ")"); + } + } + + return hr; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("WinRT 手写识别失败: " + ex.Message, LogHelper.LogType.Warning); + if (strokes != null && strokes.Count > 0) + LogHandwriting("识别异常:" + ex.Message, LogHelper.LogType.Warning); + return HandwritingRecognitionResult.Empty; + } + } + + private static void TryApplyPreferredHandwritingRecognizer( + WinRtInk.InkRecognizerContainer container, + bool logDetail) + { + if (container == null) + return; + try + { + if (!_preferredHandwritingRecognizerResolved) + { + _preferredHandwritingRecognizerResolved = true; + var all = container.GetRecognizers(); + _preferredHandwritingRecognizer = SelectBestInkRecognizer(all); + if (logDetail) + { + if (_preferredHandwritingRecognizer != null) + LogHandwriting("识别器:已选用 \"" + _preferredHandwritingRecognizer.Name + "\"。"); + else if (all != null && all.Count > 0) + LogHandwriting("识别器:未匹配到与 UI/区域语言对应的引擎,使用系统默认(共 " + all.Count + " 个)。"); + } + } + + if (_preferredHandwritingRecognizer != null) + container.SetDefaultRecognizer(_preferredHandwritingRecognizer); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 设置默认手写识别器失败: " + ex.Message, LogHelper.LogType.Warning); + } + } + + private static WinRtInk.InkRecognizer SelectBestInkRecognizer( + IReadOnlyList list) + { + if (list == null || list.Count == 0) + return null; + + var culture = PrimaryHandwritingCulture(); + var lang = (culture?.TwoLetterISOLanguageName ?? string.Empty).ToLowerInvariant(); + var name = culture?.Name ?? string.Empty; + + bool wantZhHans = lang == "zh" && + (name.IndexOf("hans", StringComparison.OrdinalIgnoreCase) >= 0 || + name.Equals("zh-cn", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-sg", StringComparison.OrdinalIgnoreCase) || + (name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) < 0 && + !name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) && + !name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) && + !name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase))); + + bool wantZhHant = lang == "zh" && + (name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) >= 0 || + name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase)); + + WinRtInk.InkRecognizer Pick(Func match) + { + foreach (var r in list) + { + var n = r?.Name; + if (string.IsNullOrEmpty(n)) + continue; + if (match(n)) + return r; + } + + return null; + } + + if (wantZhHans) + { + var r = Pick(n => + n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0 || + (n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0)) || + (n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("Simplified", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("PRC", StringComparison.OrdinalIgnoreCase) >= 0))); + if (r != null) + return r; + r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (wantZhHant) + { + var r = Pick(n => + n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0 || + (n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0)) || + (n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("Traditional", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Taiwan", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hong Kong", StringComparison.OrdinalIgnoreCase) >= 0))); + if (r != null) + return r; + r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (lang == "ja") + { + var r = Pick(n => + n.IndexOf("Japanese", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("日本語", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("日语", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (lang == "en") + { + var r = Pick(n => n.IndexOf("English", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + + if (lang == "zh") + { + var r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + + return null; + } + + private static CultureInfo PrimaryHandwritingCulture() + { + var ui = CultureInfo.CurrentUICulture; + var ct = CultureInfo.CurrentCulture; + if (string.Equals(ui.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase)) + return ui; + if (string.Equals(ct.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase)) + return ct; + return ui; + } + + private static async Task RecognizeHandwritingWholeInkAsync( + StrokeCollection strokes, + bool traceRecognition) + { + if (strokes == null || strokes.Count == 0) + return HandwritingRecognitionResult.Empty; + + var container = new WinRtInk.InkStrokeContainer(); + foreach (Stroke s in strokes) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s); + if (ink != null) + container.AddStroke(ink); + } + + var winStrokes = container.GetStrokes(); + if (winStrokes == null || winStrokes.Count == 0) + { + if (traceRecognition) + LogHandwriting("整批回退:无有效 WinRT 笔画。"); + return HandwritingRecognitionResult.Empty; + } + + var reco = new WinRtInk.InkRecognizerContainer(); + TryApplyPreferredHandwritingRecognizer(reco, false); + + IReadOnlyList rr; + try + { + rr = await reco + .RecognizeAsync(container, WinRtInk.InkRecognitionTarget.All) + .AsTask() + .ConfigureAwait(true); + } + catch (Exception ex) + { + if (traceRecognition) + LogHandwriting("整批回退:RecognizeAsync 异常:" + ex.Message); + return HandwritingRecognitionResult.Empty; + } + + if (rr == null || rr.Count == 0 || rr[0] == null) + { + if (traceRecognition) + LogHandwriting("整批回退:RecognizeAsync 无结果。"); + return HandwritingRecognitionResult.Empty; + } + + var cands = rr[0].GetTextCandidates(); + var primary = (cands != null && cands.Count > 0) ? cands[0] : string.Empty; + if (string.IsNullOrWhiteSpace(primary)) + { + if (traceRecognition) + LogHandwriting("整批回退:候选文本为空。"); + return HandwritingRecognitionResult.Empty; + } + + var merged = new List(); + if (cands != null) + { + foreach (var c in cands) + { + if (!string.IsNullOrEmpty(c) && !merged.Contains(c)) + merged.Add(c); + } + } + + var bounds = UnionStrokeBounds(strokes); + var group = new List(); + foreach (Stroke s in strokes) + group.Add(s); + + var seg = new HandwritingWordSegment(primary, merged, bounds, group); + return new HandwritingRecognitionResult(new List { seg }); + } + + private static Rect UnionStrokeBounds(StrokeCollection strokes) + { + if (strokes == null || strokes.Count == 0) + return Rect.Empty; + + var r = strokes[0].GetBounds(); + for (var i = 1; i < strokes.Count; i++) + r = Rect.Union(r, strokes[i].GetBounds()); + return r; + } + + private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script"; + + /// + /// 识别手写词后,将「有识别文本」的分词替换为指定手写风格字体的字形轮廓墨迹;未识别或空文本的词保留原笔画。 + /// + public static async Task ConvertRecognizedTextToHandwritingInkAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) + { + if (!IsApiAvailable || strokes == null || strokes.Count == 0) + { + if (strokes != null && strokes.Count > 0 && !IsApiAvailable) + LogHandwriting("字形替换:跳过,IsApiAvailable=false。"); + return strokes; + } + + var fontList = string.IsNullOrWhiteSpace(handwritingFontFamilyList) + ? DefaultHandwritingFontFamilyList + : handwritingFontFamilyList.Trim(); + LogHandwriting( + "字形替换开始:输入笔画数=" + strokes.Count + + ",字体链=\"" + fontList + "\"" + + ",PixelsPerDip=" + Math.Round(GetPixelsPerDipSafe(), 3)); + + try + { + var reco = await RecognizeHandwritingAsync(strokes).ConfigureAwait(true); + if (!reco.IsSuccess || reco.Words == null || reco.Words.Count == 0) + { + LogHandwriting( + "字形替换中止:识别未成功(IsSuccess=" + reco.IsSuccess + + ",词数=" + (reco.Words?.Count ?? 0) + "),原样返回笔画。"); + return strokes; + } + + var firstStrokeToSegment = new Dictionary(); + foreach (var w in reco.Words) + { + if (w?.Strokes == null || w.Strokes.Count == 0) + continue; + var ordered = w.Strokes.OrderBy(st => IndexOfStrokeInCollection(strokes, st)).ToList(); + var first = ordered[0]; + if (!firstStrokeToSegment.ContainsKey(first)) + firstStrokeToSegment[first] = w; + } + + if (firstStrokeToSegment.Count == 0) + { + LogHandwriting("字形替换中止:无法建立「首笔画→分词」映射,原样返回。"); + return strokes; + } + + var consumed = new HashSet(); + var result = new StrokeCollection(); + var pixelsPerDip = GetPixelsPerDipSafe(); + var replacedWordCount = 0; + var keptOriginalWordCount = 0; + var glyphStrokeTotal = 0; + + foreach (Stroke s in strokes) + { + if (consumed.Contains(s)) + continue; + + if (!firstStrokeToSegment.TryGetValue(s, out var seg)) + { + result.Add(s); + continue; + } + + if (string.IsNullOrWhiteSpace(seg.Text)) + { + LogHandwriting( + " 分词:文本为空,保留原笔画,笔画数=" + seg.Strokes.Count); + keptOriginalWordCount++; + foreach (var z in seg.Strokes) + { + if (!consumed.Contains(z)) + { + result.Add(z); + consumed.Add(z); + } + } + + continue; + } + + var templateDa = seg.Strokes[0]?.DrawingAttributes?.Clone() ?? new DrawingAttributes(); + OutlineAttributesForGlyphInk(templateDa); + + var glyphStrokes = CreateHandwritingGlyphStrokes( + seg.Text.Trim(), + seg.BoundingRectangle, + templateDa, + fontList, + pixelsPerDip); + + if (glyphStrokes == null || glyphStrokes.Count == 0) + { + LogHandwriting( + " 分词:字形轮廓生成失败,保留原笔画。文本=\"" + + (seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + "\""); + keptOriginalWordCount++; + foreach (var z in seg.Strokes) + { + if (!consumed.Contains(z)) + { + result.Add(z); + consumed.Add(z); + } + } + + continue; + } + + foreach (var nk in glyphStrokes) + result.Add(nk); + glyphStrokeTotal += glyphStrokes.Count; + replacedWordCount++; + LogHandwriting( + " 分词:已替换为手写体字形墨迹,文本=\"" + + (seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + + "\",生成笔画数=" + glyphStrokes.Count + ",移除原笔画数=" + seg.Strokes.Count); + + foreach (var z in seg.Strokes) + consumed.Add(z); + } + + LogHandwriting( + "字形替换结束:输出笔画数=" + result.Count + + "(输入=" + strokes.Count + "),替换词数=" + replacedWordCount + + ",保留原迹词数=" + keptOriginalWordCount + + ",字形子笔画合计=" + glyphStrokeTotal); + return result; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("WinRT 手写体字形替换失败: " + ex.Message, LogHelper.LogType.Warning); + LogHandwriting("字形替换异常:" + ex, LogHelper.LogType.Warning); + return strokes; + } + } + + private static int IndexOfStrokeInCollection(StrokeCollection collection, Stroke stroke) + { + if (collection == null || stroke == null) + return int.MaxValue; + for (var i = 0; i < collection.Count; i++) + { + if (ReferenceEquals(collection[i], stroke)) + return i; + } + + return int.MaxValue; + } + + private static void OutlineAttributesForGlyphInk(DrawingAttributes da) + { + if (da == null) return; + var w = Math.Max(0.8, Math.Min(da.Width, da.Height) * 0.2); + da.Width = w; + da.Height = w; + da.StylusTip = StylusTip.Ellipse; + da.IsHighlighter = false; + } + + private static double GetPixelsPerDipSafe() + { + try + { + if (Application.Current?.MainWindow is Visual v) + return VisualTreeHelper.GetDpi(v).PixelsPerDip; + } + catch + { + // ignore + } + + return 1.0; + } + + private static Typeface ResolveHandwritingTypeface(string fontFamilyList) + { + try + { + var ff = new FontFamily(fontFamilyList ?? DefaultHandwritingFontFamilyList); + return new Typeface(ff, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); + } + catch + { + return new Typeface( + SystemFonts.MessageFontFamily, + SystemFonts.MessageFontStyle, + SystemFonts.MessageFontWeight, + FontStretches.Normal); + } + } + + private static List CreateHandwritingGlyphStrokes( + string text, + Rect placeRect, + DrawingAttributes templateDa, + string fontFamilyList, + double pixelsPerDip) + { + var list = new List(); + if (string.IsNullOrEmpty(text) || placeRect.Width < 1 || placeRect.Height < 1) + return list; + + var typeface = ResolveHandwritingTypeface(fontFamilyList); + var culture = CultureInfo.CurrentCulture; + var em = Math.Max(6.0, placeRect.Height * 0.72); + FormattedText ft = null; + + for (var i = 0; i < 14; i++) + { + ft = new FormattedText( + text, + culture, + FlowDirection.LeftToRight, + typeface, + em, + Brushes.Black, + new NumberSubstitution(NumberCultureSource.Text, culture, NumberSubstitutionMethod.Context), + TextFormattingMode.Display, + pixelsPerDip); + + if (ft.Width <= placeRect.Width * 0.96 && ft.Height <= placeRect.Height * 0.96) + break; + + em *= 0.9; + if (em < 4.5) + break; + } + + if (ft == null || ft.Width < 0.5 || ft.Height < 0.5) + return list; + + var scale = Math.Min( + placeRect.Width * 0.94 / Math.Max(1e-6, ft.Width), + placeRect.Height * 0.94 / Math.Max(1e-6, ft.Height)); + var tx = placeRect.Left + (placeRect.Width - ft.Width * scale) / 2.0; + var ty = placeRect.Top + (placeRect.Height - ft.Height * scale) / 2.0; + + Geometry geom; + try + { + geom = ft.BuildGeometry(new Point(0, 0)); + } + catch + { + return list; + } + + if (geom == null || geom.IsEmpty()) + return list; + + var m = new Matrix(scale, 0, 0, scale, tx, ty); + geom.Transform = new MatrixTransform(m); + return StrokesFromOutlinedGeometry(geom, templateDa, 0.35); + } + + private static List StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance) + { + var list = new List(); + if (geometry == null || geometry.IsEmpty() || da == null) + return list; + + Geometry outlined; + try + { + outlined = geometry.GetOutlinedPathGeometry(tolerance, ToleranceType.Absolute); + } + catch + { + return list; + } + + if (outlined == null || outlined.IsEmpty()) + return list; + + Geometry flat; + try + { + flat = outlined.GetFlattenedPathGeometry(tolerance, ToleranceType.Absolute); + } + catch + { + return list; + } + + if (!(flat is PathGeometry pg)) + return list; + + foreach (var fig in pg.Figures) + { + var pts = new StylusPointCollection(); + pts.Add(new StylusPoint(fig.StartPoint.X, fig.StartPoint.Y, 0.5f)); + foreach (var seg in fig.Segments) + { + switch (seg) + { + case LineSegment ls: + pts.Add(new StylusPoint(ls.Point.X, ls.Point.Y, 0.5f)); + break; + case PolyLineSegment pls: + foreach (var p in pls.Points) + pts.Add(new StylusPoint(p.X, p.Y, 0.5f)); + break; + } + } + + if (pts.Count >= 2) + list.Add(new Stroke(pts) { DrawingAttributes = da.Clone() }); + } + + return list; + } + } + + /// 单个手写词片段的识别结果。 + public sealed class HandwritingWordSegment + { + public HandwritingWordSegment( + string text, + IReadOnlyList textCandidates, + Rect boundingRectangle, + IReadOnlyList strokes) + { + Text = text ?? string.Empty; + TextCandidates = textCandidates ?? Array.Empty(); + BoundingRectangle = boundingRectangle; + Strokes = strokes ?? Array.Empty(); + } + + public string Text { get; } + public IReadOnlyList TextCandidates { get; } + public Rect BoundingRectangle { get; } + public IReadOnlyList Strokes { get; } + } + + /// 一次手写识别批次的汇总结果。 + public sealed class HandwritingRecognitionResult + { + public static readonly HandwritingRecognitionResult Empty = new HandwritingRecognitionResult(); + + private HandwritingRecognitionResult() + { + Words = Array.Empty(); + IsSuccess = false; + CombinedText = string.Empty; + } + + public HandwritingRecognitionResult(IReadOnlyList words) + { + Words = words ?? Array.Empty(); + IsSuccess = Words.Count > 0; + CombinedText = string.Join("", Words.Select(w => w.Text ?? string.Empty)); + } + + public bool IsSuccess { get; } + public IReadOnlyList Words { get; } + public string CombinedText { get; } + } +} diff --git a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs index 2257ad0d..bfd0618f 100644 --- a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs @@ -99,7 +99,8 @@ namespace Ink_Canvas.Helpers } } - private static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) + /// 供 WinRT 手写等模块复用:将 WPF 转为 WinRT + internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) { if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0) return null; diff --git a/Ink Canvas/Helpers/WindowsNotificationHelper.cs b/Ink Canvas/Helpers/WindowsNotificationHelper.cs index 13f15c82..652732eb 100644 --- a/Ink Canvas/Helpers/WindowsNotificationHelper.cs +++ b/Ink Canvas/Helpers/WindowsNotificationHelper.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Microsoft.Toolkit.Uwp.Notifications; using System; using System.Windows; @@ -40,10 +40,9 @@ namespace Ink_Canvas.Helpers taskbar.Visibility = Visibility.Visible; - taskbar.ShowBalloonTip( + taskbar.ShowNotification( "InkCanvasForClass CE", - $"发现新版本!:{version}", - BalloonIcon.Info); + $"发现新版本!:{version}"); } catch { diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj index 14d4d38c..8ce91b1b 100644 --- a/Ink Canvas/InkCanvasForClass.csproj +++ b/Ink Canvas/InkCanvasForClass.csproj @@ -136,7 +136,7 @@ all - + diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index ac5756e4..bea07f88 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -134,6 +134,7 @@ + +