diff --git a/.all-contributorsrc b/.all-contributorsrc
index 41dcfa6e..3ac1bb16 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -86,7 +86,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/129855423?v=4",
"profile": "https://github.com/PrefacedCorg",
"contributions": [
- "code"
+ "code",
+ "design"
]
}
]
diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs
index e3ac9b51..3a953b57 100644
--- a/Ink Canvas/App.xaml.cs
+++ b/Ink Canvas/App.xaml.cs
@@ -45,7 +45,7 @@ namespace Ink_Canvas
// 新增:进程ID
private static int currentProcessId = Process.GetCurrentProcess().Id;
// 新增:应用启动时间
- private static DateTime appStartTime = DateTime.Now;
+ internal static DateTime appStartTime { get; private set; }
// 新增:最后一次错误信息
private static string lastErrorMessage = string.Empty;
// 新增:是否已初始化崩溃监听器
@@ -287,6 +287,7 @@ namespace Ink_Canvas
{
string reason = e.Reason == SessionEndReasons.Logoff ? "用户注销" : "系统关机";
WriteCrashLog($"系统会话即将结束: {reason}");
+ DeviceIdentifier.SaveUsageStatsOnShutdown();
}
// 新增:控制台取消事件处理
@@ -330,7 +331,8 @@ namespace Ink_Canvas
private void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
TimeSpan runDuration = DateTime.Now - appStartTime;
- WriteCrashLog($"应用程序退出,运行时长: {runDuration}");
+ string durationText = FormatTimeSpan(runDuration);
+ WriteCrashLog($"应用程序退出,运行时长: {durationText}");
// 如果有最后错误消息,记录到日志
if (!string.IsNullOrEmpty(lastErrorMessage))
@@ -339,6 +341,27 @@ namespace Ink_Canvas
}
}
+ // 新增:格式化时间跨度
+ private static string FormatTimeSpan(TimeSpan timeSpan)
+ {
+ if (timeSpan.TotalDays >= 1)
+ {
+ return $"{timeSpan.Days}天 {timeSpan.Hours}小时 {timeSpan.Minutes}分钟";
+ }
+ else if (timeSpan.TotalHours >= 1)
+ {
+ return $"{timeSpan.Hours}小时 {timeSpan.Minutes}分钟";
+ }
+ else if (timeSpan.TotalMinutes >= 1)
+ {
+ return $"{timeSpan.Minutes}分钟 {timeSpan.Seconds}秒";
+ }
+ else
+ {
+ return $"{timeSpan.Seconds}秒";
+ }
+ }
+
// 新增:记录崩溃日志
private static void WriteCrashLog(string message)
{
@@ -355,7 +378,7 @@ namespace Ink_Canvas
// 收集系统状态信息
string memoryUsage = (Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024)) + " MB";
string cpuTime = Process.GetCurrentProcess().TotalProcessorTime.ToString();
- string processUptime = (DateTime.Now - Process.GetCurrentProcess().StartTime).ToString();
+ string processUptime = FormatTimeSpan(DateTime.Now - Process.GetCurrentProcess().StartTime);
string statusInfo = $"[内存: {memoryUsage}, CPU时间: {cpuTime}, 运行时长: {processUptime}]";
@@ -435,6 +458,9 @@ namespace Ink_Canvas
void App_Startup(object sender, StartupEventArgs e)
{
+ // 初始化应用启动时间
+ appStartTime = DateTime.Now;
+
/*if (!StoreHelper.IsStoreApp) */
RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs
index 858d6840..00caa8a6 100644
--- a/Ink Canvas/AssemblyInfo.cs
+++ b/Ink Canvas/AssemblyInfo.cs
@@ -49,5 +49,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.7.5")]
-[assembly: AssemblyFileVersion("1.7.7.5")]
+[assembly: AssemblyVersion("1.7.7.7")]
+[assembly: AssemblyFileVersion("1.7.7.7")]
diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/AutoUpdateHelper.cs
index 138c0090..d0910eb9 100644
--- a/Ink Canvas/Helpers/AutoUpdateHelper.cs
+++ b/Ink Canvas/Helpers/AutoUpdateHelper.cs
@@ -45,9 +45,9 @@ namespace Ink_Canvas.Helpers
new UpdateLineGroup
{
GroupName = "GitHub主线",
- VersionUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
- DownloadUrlFormat = "https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
- LogUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
+ VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
+ DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
+ LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
@@ -80,9 +80,9 @@ namespace Ink_Canvas.Helpers
new UpdateLineGroup
{
GroupName = "GitHub主线",
- VersionUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
- DownloadUrlFormat = "https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
- LogUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
+ VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
+ DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
+ LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
@@ -1195,7 +1195,7 @@ namespace Ink_Canvas.Helpers
StringBuilder batchContent = new StringBuilder();
batchContent.AppendLine("@echo off");
-
+
batchContent.AppendLine("echo Set objShell = CreateObject(\"WScript.Shell\") > \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("echo objShell.Run \"cmd /c \"\"\" ^& WScript.Arguments(0) ^& \"\"\"\", 0, True >> \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("echo Wscript.Sleep 100 >> \"%temp%\\hideme.vbs\"");
@@ -1210,6 +1210,7 @@ namespace Ink_Canvas.Helpers
batchContent.AppendLine($"echo goto CHECK_PROCESS >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
+ batchContent.AppendLine($"echo timeout /t 1 /nobreak > nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Application closed, starting update process... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo timeout /t 2 /nobreak ^> nul >> \"{updateBatPath}\"");
@@ -1514,7 +1515,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 开始测试Windows 7 TLS连接...");
// 测试GitHub连接
- var testUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt";
+ var testUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt";
using (var handler = new HttpClientHandler())
{
diff --git a/Ink Canvas/Helpers/DeviceIdentifier.cs b/Ink Canvas/Helpers/DeviceIdentifier.cs
index a82fe3a8..79140029 100644
--- a/Ink Canvas/Helpers/DeviceIdentifier.cs
+++ b/Ink Canvas/Helpers/DeviceIdentifier.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
+using System.Threading;
namespace Ink_Canvas.Helpers
{
@@ -2745,5 +2746,303 @@ namespace Ink_Canvas.Helpers
return errorMsg;
}
}
+ ///
+ /// 关机时保存使用时间数据
+ ///
+ public static void SaveUsageStatsOnShutdown()
+ {
+ // 使用超时锁防止死锁
+ if (!Monitor.TryEnter(fileLock, TimeSpan.FromSeconds(30)))
+ {
+ LogHelper.WriteLogToFile("DeviceIdentifier | 关机保存超时,使用备用保存策略", LogHelper.LogType.Warning);
+ SaveUsageStatsOnShutdownFallback();
+ return;
+ }
+
+ try
+ {
+ LogHelper.WriteLogToFile("DeviceIdentifier | 开始关机时保存使用时间数据", LogHelper.LogType.Info);
+
+ // 1. 加载现有使用统计数据(多重恢复策略)
+ UsageStats stats = LoadUsageStatsWithFallback();
+ if (stats == null)
+ {
+ stats = new UsageStats { DeviceId = DeviceId };
+ LogHelper.WriteLogToFile("DeviceIdentifier | 创建新的使用统计数据", LogHelper.LogType.Info);
+ }
+
+ // 2. 计算本次会话时长(防止异常值)
+ TimeSpan sessionDuration = DateTime.Now - App.appStartTime;
+ long sessionSeconds = Math.Max(0, (long)sessionDuration.TotalSeconds);
+
+ // 防止异常大的会话时长(超过24小时)
+ if (sessionSeconds > 86400)
+ {
+ sessionSeconds = 86400;
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 会话时长异常,已限制为24小时: {sessionSeconds}秒", LogHelper.LogType.Warning);
+ }
+
+ // 3. 更新统计数据
+ stats.TotalUsageSeconds += sessionSeconds;
+ stats.LaunchCount++;
+ stats.AverageSessionSeconds = stats.TotalUsageSeconds / (double)Math.Max(1, stats.LaunchCount);
+ stats.LastLaunchTime = DateTime.Now;
+
+ // 更新数据哈希值
+ stats.UpdateDataHash();
+
+ // 4. 多重保存策略 - 确保数据不丢失
+ var saveResults = new List();
+
+ // 4.1 保存到所有文件位置
+ saveResults.Add(SaveUsageStatsToAllFileLocations(stats));
+
+ // 4.2 保存到所有注册表位置
+ saveResults.Add(SaveUsageStatsToAllRegistryLocations(stats));
+
+ // 4.3 保存到内存缓存(作为最后防线)
+ SaveUsageStatsToMemoryCache(stats);
+
+ // 4.4 强制刷新文件系统缓存
+ ForceFlushFileSystem();
+
+ // 4.5 验证保存结果
+ var verificationResult = VerifyDataSaveResults(stats, saveResults);
+
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 关机保存完成,验证结果: {verificationResult}", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 关机时保存使用时间数据失败: {ex.Message}", LogHelper.LogType.Error);
+
+ // 即使主保存失败,也要尝试备用保存
+ try
+ {
+ SaveUsageStatsOnShutdownFallback();
+ }
+ catch (Exception fallbackEx)
+ {
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 备用保存也失败: {fallbackEx.Message}", LogHelper.LogType.Error);
+ }
+ }
+ finally
+ {
+ Monitor.Exit(fileLock);
+ }
+ }
+
+ ///
+ /// 关机保存的备用策略
+ ///
+ private static void SaveUsageStatsOnShutdownFallback()
+ {
+ try
+ {
+ LogHelper.WriteLogToFile("DeviceIdentifier | 执行关机保存备用策略", LogHelper.LogType.Warning);
+
+ // 使用最基本的保存方式
+ var stats = new UsageStats { DeviceId = DeviceId };
+ stats.TotalUsageSeconds = 1; // 最小记录
+ stats.LaunchCount = 1;
+ stats.LastLaunchTime = DateTime.Now;
+
+ // 只保存到最可靠的位置
+ SaveUsageStatsToFile(BackupUsageStatsPath, stats);
+ SaveUsageStatsToRegistry(stats);
+
+ LogHelper.WriteLogToFile("DeviceIdentifier | 备用策略执行完成", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 备用策略执行失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 加载使用统计数据(带多重恢复策略)
+ ///
+ private static UsageStats LoadUsageStatsWithFallback()
+ {
+ try
+ {
+ // 1. 尝试从主文件加载
+ var stats = LoadUsageStats();
+ if (stats != null) return stats;
+
+ // 2. 尝试从备份文件加载
+ stats = LoadUsageStatsFromFile(BackupUsageStatsPath);
+ if (stats != null) return stats;
+
+ // 3. 尝试从其他备份位置加载
+ var backupPaths = new[] { SecondaryUsageBackupPath, TertiaryUsageBackupPath, QuaternaryUsageBackupPath };
+ foreach (var path in backupPaths)
+ {
+ stats = LoadUsageStatsFromFile(path);
+ if (stats != null) return stats;
+ }
+
+ // 4. 尝试从注册表恢复
+ stats = LoadUsageStatsFromRegistry();
+ if (stats != null) return stats;
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 多重恢复加载失败: {ex.Message}", LogHelper.LogType.Error);
+ return null;
+ }
+ }
+
+ ///
+ /// 保存使用统计到所有文件位置
+ ///
+ private static string SaveUsageStatsToAllFileLocations(UsageStats stats)
+ {
+ var results = new List();
+ var filePaths = new[]
+ {
+ UsageStatsFilePath,
+ BackupUsageStatsPath,
+ SecondaryUsageBackupPath,
+ TertiaryUsageBackupPath,
+ QuaternaryUsageBackupPath
+ };
+
+ foreach (var filePath in filePaths)
+ {
+ try
+ {
+ SaveUsageStatsToFile(filePath, stats);
+ results.Add($"✓ {Path.GetFileName(filePath)}");
+ }
+ catch (Exception ex)
+ {
+ results.Add($"✗ {Path.GetFileName(filePath)}: {ex.Message}");
+ }
+ }
+
+ return string.Join("\n", results);
+ }
+
+ ///
+ /// 保存使用统计到所有注册表位置
+ ///
+ private static string SaveUsageStatsToAllRegistryLocations(UsageStats stats)
+ {
+ var results = new List();
+
+ try
+ {
+ // 主注册表位置
+ SaveUsageStatsToRegistry(stats);
+ results.Add("✓ 主注册表位置");
+ }
+ catch (Exception ex)
+ {
+ results.Add($"✗ 主注册表位置: {ex.Message}");
+ }
+
+ try
+ {
+ // 备用注册表位置
+ SaveUsageStatsToMultipleRegistryLocations(stats);
+ results.Add("✓ 备用注册表位置");
+ }
+ catch (Exception ex)
+ {
+ results.Add($"✗ 备用注册表位置: {ex.Message}");
+ }
+
+ return string.Join("\n", results);
+ }
+
+ ///
+ /// 保存使用统计到内存缓存
+ ///
+ private static void SaveUsageStatsToMemoryCache(UsageStats stats)
+ {
+ try
+ {
+ // 将数据保存到静态变量作为内存备份
+ _cachedUsageStats = stats;
+ LogHelper.WriteLogToFile("DeviceIdentifier | 数据已保存到内存缓存", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 保存到内存缓存失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 强制刷新文件系统缓存
+ ///
+ private static void ForceFlushFileSystem()
+ {
+ try
+ {
+ // 强制刷新所有相关目录
+ var directories = new[]
+ {
+ Path.GetDirectoryName(UsageStatsFilePath),
+ Path.GetDirectoryName(BackupUsageStatsPath),
+ Path.GetDirectoryName(SecondaryUsageBackupPath),
+ Path.GetDirectoryName(TertiaryUsageBackupPath),
+ Path.GetDirectoryName(QuaternaryUsageBackupPath)
+ };
+
+ foreach (var dir in directories.Where(d => !string.IsNullOrEmpty(d) && Directory.Exists(d)))
+ {
+ try
+ {
+ // 创建临时文件来强制刷新
+ var tempFile = Path.Combine(dir, ".flush.tmp");
+ File.WriteAllText(tempFile, DateTime.Now.ToString());
+ File.Delete(tempFile);
+ }
+ catch { /* 忽略刷新错误 */ }
+ }
+
+ LogHelper.WriteLogToFile("DeviceIdentifier | 文件系统缓存刷新完成", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"DeviceIdentifier | 文件系统缓存刷新失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 验证数据保存结果
+ ///
+ private static string VerifyDataSaveResults(UsageStats stats, List saveResults)
+ {
+ var verification = new StringBuilder();
+ verification.AppendLine("数据保存验证结果:");
+ verification.AppendLine(string.Join("\n", saveResults));
+
+ // 验证关键数据是否保存成功
+ try
+ {
+ var savedStats = LoadUsageStats();
+ if (savedStats != null && savedStats.DeviceId == stats.DeviceId)
+ {
+ verification.AppendLine("✓ 主数据文件验证成功");
+ }
+ else
+ {
+ verification.AppendLine("✗ 主数据文件验证失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ verification.AppendLine($"✗ 主数据文件验证异常: {ex.Message}");
+ }
+
+ return verification.ToString();
+ }
+
+ // 内存缓存变量
+ private static UsageStats _cachedUsageStats;
}
-}
\ No newline at end of file
+}
+
diff --git a/Ink Canvas/Helpers/GlobalHotkeyManager.cs b/Ink Canvas/Helpers/GlobalHotkeyManager.cs
new file mode 100644
index 00000000..66d7cba2
--- /dev/null
+++ b/Ink Canvas/Helpers/GlobalHotkeyManager.cs
@@ -0,0 +1,854 @@
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+using System.IO;
+using System.Reflection;
+using Newtonsoft.Json;
+using NHotkey.Wpf;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// 全局快捷键管理器 - 使用NHotkey库实现全局快捷键功能
+ ///
+ public class GlobalHotkeyManager : IDisposable
+ {
+ #region Private Fields
+ private readonly Dictionary _registeredHotkeys;
+ private readonly MainWindow _mainWindow;
+ private bool _isDisposed = false;
+ private bool _hotkeysShouldBeRegistered = false; // 启动时不注册热键,等待需要时再注册
+
+ // 配置文件路径
+ private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "HotkeyConfig.json");
+ #endregion
+
+ #region Constructor
+ public GlobalHotkeyManager(MainWindow mainWindow)
+ {
+ _mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow));
+ _registeredHotkeys = new Dictionary();
+ _hotkeysShouldBeRegistered = false; // 启动时不注册热键,等待需要时再注册
+ }
+ #endregion
+
+ #region Public Methods
+ ///
+ /// 注册全局快捷键
+ ///
+ /// 快捷键名称
+ /// 按键
+ /// 修饰键
+ /// 执行动作
+ /// 是否注册成功
+ public bool RegisterHotkey(string hotkeyName, Key key, ModifierKeys modifiers, Action action)
+ {
+ try
+ {
+ if (_isDisposed)
+ return false;
+
+ // 如果快捷键已存在,先注销
+ if (_registeredHotkeys.ContainsKey(hotkeyName))
+ {
+ UnregisterHotkey(hotkeyName);
+ }
+
+ // 创建快捷键信息
+ var hotkeyInfo = new HotkeyInfo
+ {
+ Name = hotkeyName,
+ Key = key,
+ Modifiers = modifiers,
+ Action = action
+ };
+
+ // 注册快捷键
+ HotkeyManager.Current.AddOrReplace(hotkeyName, key, modifiers, (sender, e) =>
+ {
+ try
+ {
+ // 确保在主线程中执行
+ _mainWindow.Dispatcher.Invoke(() =>
+ {
+ action?.Invoke();
+ });
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"执行快捷键 {hotkeyName} 时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ });
+
+ _registeredHotkeys[hotkeyName] = hotkeyInfo;
+ // 成功注册全局快捷键
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+ }
+
+ ///
+ /// 注销指定快捷键
+ ///
+ /// 快捷键名称
+ /// 是否注销成功
+ public bool UnregisterHotkey(string hotkeyName)
+ {
+ try
+ {
+ if (_isDisposed || !_registeredHotkeys.ContainsKey(hotkeyName))
+ return false;
+
+ HotkeyManager.Current.Remove(hotkeyName);
+ _registeredHotkeys.Remove(hotkeyName);
+ // 成功注销全局快捷键
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"注销全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+ }
+
+ ///
+ /// 注销所有快捷键
+ ///
+ public void UnregisterAllHotkeys()
+ {
+ try
+ {
+ if (_isDisposed)
+ return;
+
+ foreach (var hotkeyName in _registeredHotkeys.Keys)
+ {
+ try
+ {
+ HotkeyManager.Current.Remove(hotkeyName);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"注销快捷键 {hotkeyName} 时出错: {ex.Message}", LogHelper.LogType.Warning);
+ }
+ }
+
+ _registeredHotkeys.Clear();
+ // 已注销所有全局快捷键,集合已清空
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"注销所有快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 检查快捷键是否已注册
+ ///
+ /// 快捷键名称
+ /// 是否已注册
+ public bool IsHotkeyRegistered(string hotkeyName)
+ {
+ return _registeredHotkeys.ContainsKey(hotkeyName);
+ }
+
+ ///
+ /// 获取已注册的快捷键列表
+ ///
+ /// 快捷键信息列表
+ public List GetRegisteredHotkeys()
+ {
+ return new List(_registeredHotkeys.Values);
+ }
+
+ ///
+ /// 获取配置文件中的快捷键信息(不注册,仅用于显示)
+ ///
+ /// 配置文件中的快捷键列表
+ public List GetHotkeysFromConfigFile()
+ {
+ try
+ {
+ if (!File.Exists(HotkeyConfigFile))
+ {
+ LogHelper.WriteLogToFile("快捷键配置文件不存在", LogHelper.LogType.Info);
+ return new List();
+ }
+
+ // 读取配置文件内容
+ string jsonContent = File.ReadAllText(HotkeyConfigFile, System.Text.Encoding.UTF8);
+ if (string.IsNullOrEmpty(jsonContent))
+ {
+ LogHelper.WriteLogToFile("快捷键配置文件为空", LogHelper.LogType.Warning);
+ return new List();
+ }
+
+ // 反序列化配置
+ var config = JsonConvert.DeserializeObject(jsonContent);
+ if (config?.Hotkeys == null || config.Hotkeys.Count == 0)
+ {
+ LogHelper.WriteLogToFile("快捷键配置为空或格式错误", LogHelper.LogType.Warning);
+ return new List();
+ }
+
+ // 转换为HotkeyInfo列表(不注册,仅用于显示)
+ var hotkeyList = new List();
+ foreach (var hotkeyConfig in config.Hotkeys)
+ {
+ hotkeyList.Add(new HotkeyInfo
+ {
+ Name = hotkeyConfig.Name,
+ Key = hotkeyConfig.Key,
+ Modifiers = hotkeyConfig.Modifiers,
+ Action = null // 不设置动作,仅用于显示
+ });
+ }
+
+ LogHelper.WriteLogToFile($"从配置文件读取到 {hotkeyList.Count} 个快捷键信息", LogHelper.LogType.Info);
+ return hotkeyList;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"从配置文件读取快捷键信息时出错: {ex.Message}", LogHelper.LogType.Error);
+ return new List();
+ }
+ }
+
+ ///
+ /// 注册默认快捷键集合
+ ///
+ public void RegisterDefaultHotkeys()
+ {
+ try
+ {
+ // 开始注册默认快捷键集合
+
+ // 基本操作快捷键
+ RegisterHotkey("Undo", Key.Z, ModifierKeys.Control, () => _mainWindow.SymbolIconUndo_MouseUp(null, null));
+ RegisterHotkey("Redo", Key.Y, ModifierKeys.Control, () => _mainWindow.SymbolIconRedo_MouseUp(null, null));
+ RegisterHotkey("Clear", Key.E, ModifierKeys.Control, () => _mainWindow.SymbolIconDelete_MouseUp(null, null));
+ RegisterHotkey("Paste", Key.V, ModifierKeys.Control, () => _mainWindow.HandleGlobalPaste(null, null));
+
+ // 工具切换快捷键
+ RegisterHotkey("SelectTool", Key.S, ModifierKeys.Alt, () => _mainWindow.SymbolIconSelect_MouseUp(null, null));
+ RegisterHotkey("DrawTool", Key.D, ModifierKeys.Alt, () => _mainWindow.PenIcon_Click(null, null));
+ RegisterHotkey("EraserTool", Key.E, ModifierKeys.Alt, () => _mainWindow.EraserIcon_Click(null, null));
+ RegisterHotkey("BlackboardTool", Key.B, ModifierKeys.Alt, () => _mainWindow.ImageBlackboard_MouseUp(null, null));
+ RegisterHotkey("QuitDrawTool", Key.Q, ModifierKeys.Alt, () => _mainWindow.CursorIcon_Click(null, null));
+
+ // 画笔快捷键 - 使用反射访问penType字段
+ RegisterHotkey("Pen1", Key.D1, ModifierKeys.Alt, () => SwitchToPenType(0));
+ RegisterHotkey("Pen2", Key.D2, ModifierKeys.Alt, () => SwitchToPenType(1));
+ RegisterHotkey("Pen3", Key.D3, ModifierKeys.Alt, () => SwitchToPenType(2));
+ RegisterHotkey("Pen4", Key.D4, ModifierKeys.Alt, () => SwitchToPenType(3));
+ RegisterHotkey("Pen5", Key.D5, ModifierKeys.Alt, () => SwitchToPenType(4));
+
+ // 功能快捷键
+ RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, () => _mainWindow.BtnDrawLine_Click(null, null));
+ RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop());
+ RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null));
+
+ // 退出快捷键
+ RegisterHotkey("Exit", Key.Escape, ModifierKeys.None, () => _mainWindow.KeyExit(null, null));
+
+ // 已注册默认全局快捷键集合
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"注册默认快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 从配置文件加载快捷键
+ ///
+ public void LoadHotkeysFromSettings()
+ {
+ try
+ {
+ // 开始从配置文件加载快捷键设置
+
+ // 检查是否应该注册快捷键
+ if (!_hotkeysShouldBeRegistered)
+ {
+ // 当前状态不允许注册快捷键,跳过加载
+ return;
+ }
+
+ // 尝试从配置文件加载
+ if (LoadHotkeysFromConfigFile())
+ {
+ // 成功从配置文件加载快捷键设置
+ _hotkeysShouldBeRegistered = true;
+ LogHelper.WriteLogToFile("成功从配置文件加载快捷键设置", LogHelper.LogType.Info);
+ }
+ else
+ {
+ // 如果配置文件不存在,才使用默认快捷键
+ if (!File.Exists(HotkeyConfigFile))
+ {
+ LogHelper.WriteLogToFile("配置文件不存在,注册默认快捷键", LogHelper.LogType.Info);
+ RegisterDefaultHotkeys();
+ _hotkeysShouldBeRegistered = true;
+ }
+ else
+ {
+ LogHelper.WriteLogToFile("配置文件存在但加载失败,保持当前状态", LogHelper.LogType.Warning);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"从设置加载快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
+ // 出错时不自动使用默认快捷键,保持当前状态
+ }
+ }
+
+ ///
+ /// 保存快捷键配置到设置
+ ///
+ public void SaveHotkeysToSettings()
+ {
+ try
+ {
+ LogHelper.WriteLogToFile("开始保存快捷键配置到配置文件", LogHelper.LogType.Event);
+
+ if (SaveHotkeysToConfigFile())
+ {
+ LogHelper.WriteLogToFile("快捷键配置已成功保存到配置文件", LogHelper.LogType.Event);
+ }
+ else
+ {
+ LogHelper.WriteLogToFile("保存快捷键配置失败", LogHelper.LogType.Error);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"保存快捷键配置时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 启用快捷键注册功能
+ /// 调用此方法后,快捷键将被允许注册
+ ///
+ public void EnableHotkeyRegistration()
+ {
+ try
+ {
+ if (!_hotkeysShouldBeRegistered)
+ {
+ _hotkeysShouldBeRegistered = true;
+ LogHelper.WriteLogToFile("启用快捷键注册功能", LogHelper.LogType.Info);
+
+ // 立即加载快捷键设置
+ LoadHotkeysFromSettings();
+ }
+ else
+ {
+ LogHelper.WriteLogToFile("快捷键注册功能已经启用", LogHelper.LogType.Info);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"启用快捷键注册功能时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 更新快捷键配置
+ ///
+ /// 快捷键名称
+ /// 新按键
+ /// 新修饰键
+ /// 是否更新成功
+ public bool UpdateHotkey(string hotkeyName, Key key, ModifierKeys modifiers)
+ {
+ try
+ {
+ if (!_registeredHotkeys.ContainsKey(hotkeyName))
+ {
+ LogHelper.WriteLogToFile($"快捷键 {hotkeyName} 不存在,无法更新", LogHelper.LogType.Warning);
+ return false;
+ }
+
+ // 获取原有的动作
+ var originalAction = _registeredHotkeys[hotkeyName].Action;
+
+ // 注销原有快捷键
+ UnregisterHotkey(hotkeyName);
+
+ // 注册新的快捷键
+ var success = RegisterHotkey(hotkeyName, key, modifiers, originalAction);
+
+ if (success)
+ {
+ LogHelper.WriteLogToFile($"成功更新快捷键 {hotkeyName}: {modifiers}+{key}", LogHelper.LogType.Event);
+ // 自动保存配置
+ SaveHotkeysToSettings();
+ }
+
+ return success;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"更新快捷键 {hotkeyName} 时出错: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+ }
+ #endregion
+
+ #region Private Helper Methods
+ ///
+ /// 切换到指定笔类型
+ ///
+ /// 笔类型索引
+ private void SwitchToPenType(int penTypeIndex)
+ {
+ try
+ {
+ // 通过反射访问主窗口的penType字段
+ var penTypeField = _mainWindow.GetType().GetField("penType",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ if (penTypeField != null)
+ {
+ penTypeField.SetValue(_mainWindow, penTypeIndex);
+
+ // 调用CheckPenTypeUIState方法更新UI状态
+ var checkPenTypeMethod = _mainWindow.GetType().GetMethod("CheckPenTypeUIState",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ if (checkPenTypeMethod != null)
+ {
+ checkPenTypeMethod.Invoke(_mainWindow, null);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"切换到笔类型{penTypeIndex}时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 从配置文件加载快捷键设置
+ ///
+ /// 是否加载成功
+ private bool LoadHotkeysFromConfigFile()
+ {
+ try
+ {
+ if (!File.Exists(HotkeyConfigFile))
+ {
+ LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning);
+ return false;
+ }
+
+ // 读取配置文件内容
+ string jsonContent = File.ReadAllText(HotkeyConfigFile, System.Text.Encoding.UTF8);
+ if (string.IsNullOrEmpty(jsonContent))
+ {
+ LogHelper.WriteLogToFile("快捷键配置文件为空", LogHelper.LogType.Warning);
+ return false;
+ }
+
+ // 反序列化配置
+ var config = JsonConvert.DeserializeObject(jsonContent);
+ if (config?.Hotkeys == null || config.Hotkeys.Count == 0)
+ {
+ LogHelper.WriteLogToFile("快捷键配置为空或格式错误", LogHelper.LogType.Warning);
+ return false;
+ }
+
+ // 注册配置中的快捷键
+ int successCount = 0;
+ foreach (var hotkeyConfig in config.Hotkeys)
+ {
+ try
+ {
+ // 根据快捷键名称获取对应的动作
+ var action = GetActionByName(hotkeyConfig.Name);
+ if (action != null)
+ {
+ if (RegisterHotkey(hotkeyConfig.Name, hotkeyConfig.Key, hotkeyConfig.Modifiers, action))
+ {
+ successCount++;
+ }
+ }
+ else
+ {
+ LogHelper.WriteLogToFile($"未找到快捷键 {hotkeyConfig.Name} 对应的动作", LogHelper.LogType.Warning);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"注册快捷键 {hotkeyConfig.Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ LogHelper.WriteLogToFile($"成功加载 {successCount}/{config.Hotkeys.Count} 个快捷键配置", LogHelper.LogType.Event);
+ if (successCount > 0)
+ {
+ _hotkeysShouldBeRegistered = true;
+ }
+ return successCount > 0;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"从配置文件加载快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+ }
+
+ ///
+ /// 保存快捷键配置到配置文件
+ ///
+ /// 是否保存成功
+ private bool SaveHotkeysToConfigFile()
+ {
+ try
+ {
+ // 确保配置目录存在
+ string configDir = Path.GetDirectoryName(HotkeyConfigFile);
+ if (!Directory.Exists(configDir))
+ {
+ Directory.CreateDirectory(configDir);
+ }
+
+ // 创建配置对象
+ var config = new HotkeyConfig
+ {
+ Version = "1.0",
+ LastModified = DateTime.Now,
+ Hotkeys = new List()
+ };
+
+ // 添加所有已注册的快捷键
+ foreach (var hotkey in _registeredHotkeys.Values)
+ {
+ config.Hotkeys.Add(new HotkeyConfigItem
+ {
+ Name = hotkey.Name,
+ Key = hotkey.Key,
+ Modifiers = hotkey.Modifiers
+ });
+ }
+
+ // 序列化为JSON
+ var settings = new JsonSerializerSettings
+ {
+ Formatting = Formatting.Indented
+ };
+
+ string jsonContent = JsonConvert.SerializeObject(config, settings);
+
+ // 直接写入原文件,覆盖原有内容
+ File.WriteAllText(HotkeyConfigFile, jsonContent, System.Text.Encoding.UTF8);
+
+ LogHelper.WriteLogToFile($"快捷键配置已保存到: {HotkeyConfigFile}", LogHelper.LogType.Event);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"保存快捷键配置到配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+ }
+
+ ///
+ /// 根据快捷键名称获取对应的动作
+ ///
+ /// 快捷键名称
+ /// 对应的动作,如果不存在则返回null
+ private Action GetActionByName(string hotkeyName)
+ {
+ try
+ {
+ switch (hotkeyName)
+ {
+ case "Undo":
+ return () => _mainWindow.SymbolIconUndo_MouseUp(null, null);
+ case "Redo":
+ return () => _mainWindow.SymbolIconRedo_MouseUp(null, null);
+ case "Clear":
+ return () => _mainWindow.SymbolIconDelete_MouseUp(null, null);
+ case "Paste":
+ return () => _mainWindow.HandleGlobalPaste(null, null);
+ case "SelectTool":
+ return () => _mainWindow.SymbolIconSelect_MouseUp(null, null);
+ case "DrawTool":
+ return () => _mainWindow.PenIcon_Click(null, null);
+ case "EraserTool":
+ return () => _mainWindow.EraserIcon_Click(null, null);
+ case "BlackboardTool":
+ return () => _mainWindow.ImageBlackboard_MouseUp(null, null);
+ case "QuitDrawTool":
+ return () => _mainWindow.CursorIcon_Click(null, null);
+ case "Pen1":
+ return () => SwitchToPenType(0);
+ case "Pen2":
+ return () => SwitchToPenType(1);
+ case "Pen3":
+ return () => SwitchToPenType(2);
+ case "Pen4":
+ return () => SwitchToPenType(3);
+ case "Pen5":
+ return () => SwitchToPenType(4);
+ case "DrawLine":
+ return () => _mainWindow.BtnDrawLine_Click(null, null);
+ case "Screenshot":
+ return () => _mainWindow.SaveScreenShotToDesktop();
+ case "Hide":
+ return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null);
+ case "Exit":
+ return () => _mainWindow.KeyExit(null, null);
+ default:
+ LogHelper.WriteLogToFile($"未知的快捷键名称: {hotkeyName}", LogHelper.LogType.Warning);
+ return null;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"获取快捷键 {hotkeyName} 对应动作时出错: {ex.Message}", LogHelper.LogType.Error);
+ return null;
+ }
+ }
+
+ ///
+ /// 检查当前是否处于鼠标模式(选择模式)
+ ///
+ /// 如果处于鼠标模式则返回true(不应该注册快捷键),否则返回false(应该注册快捷键)
+ private bool IsInSelectMode()
+ {
+ try
+ {
+ // 通过反射访问主窗口的FloatingbarSelectionBG字段
+ var floatingbarSelectionBGField = _mainWindow.GetType().GetField("FloatingbarSelectionBG",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+
+ if (floatingbarSelectionBGField != null)
+ {
+ var floatingbarSelectionBG = floatingbarSelectionBGField.GetValue(_mainWindow);
+ if (floatingbarSelectionBG != null)
+ {
+ // 检查高光是否可见
+ var visibilityProperty = floatingbarSelectionBG.GetType().GetProperty("Visibility");
+ if (visibilityProperty != null)
+ {
+ var visibility = visibilityProperty.GetValue(floatingbarSelectionBG);
+ if (visibility != null && visibility.ToString() == "Hidden")
+ {
+ // 高光隐藏,说明没有选中任何工具,此时应该注销快捷键以释放系统快捷键
+ return true; // 返回true表示应该注销快捷键
+ }
+ }
+
+ // 通过反射访问Canvas.GetLeft方法来获取高光位置
+ var canvasType = Type.GetType("System.Windows.Controls.Canvas, PresentationFramework");
+ if (canvasType != null)
+ {
+ var getLeftMethod = canvasType.GetMethod("GetLeft", BindingFlags.Public | BindingFlags.Static);
+ if (getLeftMethod != null)
+ {
+ var leftPosition = getLeftMethod.Invoke(null, new object[] { floatingbarSelectionBG });
+ if (leftPosition != null)
+ {
+ var position = Convert.ToDouble(leftPosition);
+
+ // 根据高光位置判断当前选中的工具
+ // 位置计算基于SetFloatingBarHighlightPosition方法中的逻辑
+ bool isMouseMode = false;
+ string currentTool = "unknown";
+
+ // 简化判断:如果位置接近0,说明是鼠标模式
+ // 如果位置接近28,说明是批注模式
+ // 如果位置更大,说明是其他工具
+ if (position < 5) // 鼠标模式:marginOffset + (cursorWidth - actualHighlightWidth) / 2 ≈ 0
+ {
+ isMouseMode = true;
+ currentTool = "鼠标";
+ }
+ else if (position < 35) // 批注模式:marginOffset + cursorWidth + (penWidth - actualHighlightWidth) / 2 ≈ 28
+ {
+ isMouseMode = false;
+ currentTool = "批注";
+ }
+ else // 其他工具(橡皮擦、选择等)
+ {
+ isMouseMode = false;
+ currentTool = "其他工具";
+ }
+
+ return isMouseMode;
+ }
+ }
+ }
+ }
+ }
+
+ // 如果无法获取高光状态,则回退到inkCanvas.EditingMode判断
+
+ // 通过反射访问主窗口的inkCanvas字段
+ var inkCanvasField = _mainWindow.GetType().GetField("inkCanvas",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+
+ if (inkCanvasField != null)
+ {
+ var inkCanvas = inkCanvasField.GetValue(_mainWindow);
+ if (inkCanvas != null)
+ {
+ // 通过反射访问inkCanvas的EditingMode属性
+ var editingModeProperty = inkCanvas.GetType().GetProperty("EditingMode");
+ if (editingModeProperty != null)
+ {
+ var editingMode = editingModeProperty.GetValue(inkCanvas);
+ if (editingMode != null)
+ {
+ // 检查是否为批注模式
+ var isInkMode = editingMode.ToString().Contains("Ink");
+ var isSelectMode = editingMode.ToString().Contains("Select");
+
+ // 如果是批注模式或选择模式,则应该注册快捷键(返回false)
+ // 如果是橡皮擦模式或其他模式,则不应该注册快捷键(返回true)
+ var shouldNotRegisterHotkeys = !isInkMode && !isSelectMode;
+
+ return shouldNotRegisterHotkeys;
+ }
+ }
+ }
+ }
+
+ // 如果无法获取任何状态信息,则回退到原来的判断逻辑
+
+ // 通过反射访问主窗口的currentMode字段(作为最后的备用方案)
+ var currentModeField = _mainWindow.GetType().GetField("currentMode",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+
+ if (currentModeField != null)
+ {
+ var currentMode = currentModeField.GetValue(_mainWindow);
+ if (currentMode != null)
+ {
+ var modeValue = currentMode.ToString();
+ // 注意:这里的逻辑需要修正
+ // currentMode == 0 表示屏幕模式(PPT放映),此时应该允许快捷键
+ // currentMode == 1 表示黑板/白板模式,此时也应该允许快捷键
+ var isSelectMode = false; // 修正:所有模式都应该允许快捷键
+ return isSelectMode;
+ }
+ }
+
+ return false; // 默认允许快捷键
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"检查鼠标模式状态时出错: {ex.Message}", LogHelper.LogType.Warning);
+ return false; // 出错时默认允许快捷键
+ }
+ }
+
+ ///
+ /// 动态管理快捷键注册状态
+ /// 根据当前工具选择状态自动注册或注销快捷键
+ ///
+ public void UpdateHotkeyRegistrationState()
+ {
+ try
+ {
+ bool isMouseMode = IsInSelectMode();
+
+ if (isMouseMode)
+ {
+ // 在鼠标模式下,注销所有快捷键以释放系统快捷键
+ if (_hotkeysShouldBeRegistered)
+ {
+ UnregisterAllHotkeys();
+ _hotkeysShouldBeRegistered = false;
+ }
+ else
+ {
+ // 快捷键已经处于注销状态,无需重复注销
+ }
+ }
+ else
+ {
+ // 在批注/选择/其他工具模式下,重新注册所有快捷键
+ if (!_hotkeysShouldBeRegistered)
+ {
+ // 第一次切换到批注/选择/其他工具模式,启用快捷键注册
+ EnableHotkeyRegistration();
+ }
+ else if (_registeredHotkeys.Count == 0)
+ {
+ // 快捷键已启用但数量为0,重新注册
+ LoadHotkeysFromSettings();
+ }
+ else
+ {
+ // 当前已有快捷键注册,无需重新注册
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"更新快捷键注册状态时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+ #endregion
+
+ #region IDisposable Implementation
+ public void Dispose()
+ {
+ if (!_isDisposed)
+ {
+ UnregisterAllHotkeys();
+ _isDisposed = true;
+ }
+ }
+ #endregion
+
+ #region Nested Classes
+ ///
+ /// 快捷键信息类
+ ///
+ public class HotkeyInfo
+ {
+ public string Name { get; set; }
+ public Key Key { get; set; }
+ public ModifierKeys Modifiers { get; set; }
+ public Action Action { get; set; }
+
+ public override string ToString()
+ {
+ var modifiersText = Modifiers == ModifierKeys.None ? "" : $"{Modifiers}+";
+ return $"{modifiersText}{Key}";
+ }
+ }
+
+ ///
+ /// 快捷键配置类
+ ///
+ private class HotkeyConfig
+ {
+ public string Version { get; set; }
+ public DateTime LastModified { get; set; }
+ public List Hotkeys { get; set; }
+ }
+
+ ///
+ /// 快捷键配置项类
+ ///
+ private class HotkeyConfigItem
+ {
+ public string Name { get; set; }
+ public Key Key { get; set; }
+ public ModifierKeys Modifiers { get; set; }
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/InkFadeManager.cs b/Ink Canvas/Helpers/InkFadeManager.cs
new file mode 100644
index 00000000..1a1e6b49
--- /dev/null
+++ b/Ink Canvas/Helpers/InkFadeManager.cs
@@ -0,0 +1,833 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media.Animation;
+using System.Windows.Threading;
+using System.Windows.Ink;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// 墨迹渐隐管理器 - 管理墨迹的渐隐动画和状态
+ ///
+ public class InkFadeManager
+ {
+ #region Properties
+ ///
+ /// 是否启用墨迹渐隐功能
+ ///
+ public bool IsEnabled { get; set; } = false;
+
+ ///
+ /// 墨迹渐隐时间(毫秒)
+ ///
+ public int FadeTime { get; set; } = 3000;
+
+ ///
+ /// 渐隐动画持续时间(毫秒)
+ ///
+ public int AnimationDuration { get; set; } = 1000;
+ #endregion
+
+ #region Private Fields
+ private readonly MainWindow _mainWindow;
+ private readonly Dispatcher _dispatcher;
+ private readonly Dictionary _fadeTimers;
+ private readonly Dictionary _strokeVisuals;
+ private readonly Dictionary _strokeStartPoints;
+ private readonly Dictionary _strokeEndPoints;
+ #endregion
+
+ #region Constructor
+ public InkFadeManager(MainWindow mainWindow)
+ {
+ _mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow));
+ _dispatcher = _mainWindow.Dispatcher;
+ _fadeTimers = new Dictionary();
+ _strokeVisuals = new Dictionary();
+ _strokeStartPoints = new Dictionary();
+ _strokeEndPoints = new Dictionary();
+ }
+ #endregion
+
+ #region Public Methods
+ ///
+ /// 添加需要渐隐的墨迹
+ ///
+ /// 墨迹对象
+ /// 落笔点
+ /// 抬笔点
+ public void AddFadingStroke(Stroke stroke, Point startPoint, Point endPoint)
+ {
+ if (!IsEnabled || stroke == null)
+ {
+ return;
+ }
+
+ try
+ {
+
+ // 记录墨迹的起点和终点
+ _strokeStartPoints[stroke] = startPoint;
+ _strokeEndPoints[stroke] = endPoint;
+
+ // 创建墨迹的视觉元素(湿墨迹状态)
+ var strokeVisual = CreateStrokeVisual(stroke);
+ if (strokeVisual == null) return;
+
+ _strokeVisuals[stroke] = strokeVisual;
+
+ // 创建定时器,在指定时间后开始渐隐动画
+ var timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(FadeTime)
+ };
+
+ timer.Tick += (sender, e) =>
+ {
+ StartFadeAnimation(stroke);
+ timer.Stop();
+ _fadeTimers.Remove(stroke);
+ };
+
+ _fadeTimers[stroke] = timer;
+ timer.Start();
+
+ // 将视觉元素添加到画布上
+ _dispatcher.InvokeAsync(() =>
+ {
+ try
+ {
+ if (_mainWindow.inkCanvas != null)
+ {
+ // 将墨迹添加到 inkCanvas 的父容器中,而不是 inkCanvas.Children
+ // 这样可以避免坐标系统问题
+ var parent = _mainWindow.inkCanvas.Parent as System.Windows.Controls.Panel;
+ if (parent != null)
+ {
+ parent.Children.Add(strokeVisual);
+ }
+ else
+ {
+ // 如果无法获取父容器,则添加到 inkCanvas.Children
+ _mainWindow.inkCanvas.Children.Add(strokeVisual);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"添加墨迹视觉元素到画布失败: {ex}", LogHelper.LogType.Error);
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"添加渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 移除墨迹
+ ///
+ /// 要移除的墨迹
+ public void RemoveStroke(Stroke stroke)
+ {
+ if (stroke == null) return;
+
+ try
+ {
+ if (_fadeTimers.TryGetValue(stroke, out var timer))
+ {
+ timer.Stop();
+ _fadeTimers.Remove(stroke);
+ }
+
+ if (_strokeVisuals.TryGetValue(stroke, out var visual))
+ {
+ _dispatcher.InvokeAsync(() =>
+ {
+ try
+ {
+ // 从父容器中移除墨迹
+ var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel;
+ if (parent != null && parent.Children.Contains(visual))
+ {
+ parent.Children.Remove(visual);
+ }
+ else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual))
+ {
+ _mainWindow.inkCanvas.Children.Remove(visual);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"从画布移除墨迹视觉元素失败: {ex}", LogHelper.LogType.Error);
+ }
+ });
+
+ _strokeVisuals.Remove(stroke);
+ }
+
+ _strokeStartPoints.Remove(stroke);
+ _strokeEndPoints.Remove(stroke);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"移除渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 清除所有渐隐墨迹
+ ///
+ public void ClearAllFadingStrokes()
+ {
+ try
+ {
+ foreach (var timer in _fadeTimers.Values)
+ {
+ timer.Stop();
+ }
+
+ _fadeTimers.Clear();
+
+ _dispatcher.InvokeAsync(() =>
+ {
+ try
+ {
+ if (_mainWindow.inkCanvas != null)
+ {
+ var parent = _mainWindow.inkCanvas.Parent as System.Windows.Controls.Panel;
+ foreach (var visual in _strokeVisuals.Values)
+ {
+ if (parent != null && parent.Children.Contains(visual))
+ {
+ parent.Children.Remove(visual);
+ }
+ else if (_mainWindow.inkCanvas.Children.Contains(visual))
+ {
+ _mainWindow.inkCanvas.Children.Remove(visual);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"清除所有墨迹视觉元素失败: {ex}", LogHelper.LogType.Error);
+ }
+ });
+
+ _strokeVisuals.Clear();
+ _strokeStartPoints.Clear();
+ _strokeEndPoints.Clear();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"清除所有渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 更新渐隐时间设置
+ ///
+ /// 新的渐隐时间(毫秒)
+ public void UpdateFadeTime(int fadeTime)
+ {
+ FadeTime = fadeTime;
+
+ foreach (var kvp in _fadeTimers)
+ {
+ var stroke = kvp.Key;
+ var timer = kvp.Value;
+
+ timer.Stop();
+ timer.Interval = TimeSpan.FromMilliseconds(FadeTime);
+ timer.Start();
+ }
+ }
+
+
+
+ ///
+ /// 启用墨迹渐隐功能
+ ///
+ public void Enable()
+ {
+ IsEnabled = true;
+ LogHelper.WriteLogToFile("墨迹渐隐功能已启用", LogHelper.LogType.Info);
+ }
+
+ ///
+ /// 禁用墨迹渐隐功能
+ ///
+ public void Disable()
+ {
+ IsEnabled = false;
+ LogHelper.WriteLogToFile("墨迹渐隐功能已禁用", LogHelper.LogType.Info);
+ }
+ #endregion
+
+ #region Private Methods
+ ///
+ /// 创建墨迹的视觉元素
+ ///
+ /// 墨迹对象
+ /// 视觉元素
+ private UIElement CreateStrokeVisual(Stroke stroke)
+ {
+ try
+ {
+ // 创建路径几何,使用墨迹的实际位置
+ var geometry = stroke.GetGeometry();
+ if (geometry == null)
+ {
+ return null;
+ }
+
+ // 获取绘画属性
+ var drawingAttribs = stroke.DrawingAttributes;
+
+ // 创建路径元素,确保使用正确的绘画属性
+ var path = new Path
+ {
+ Data = geometry,
+ Stroke = new SolidColorBrush(drawingAttribs.Color),
+ StrokeThickness = drawingAttribs.Width, // 使用原始墨迹的粗细
+ StrokeStartLineCap = PenLineCap.Round,
+ StrokeEndLineCap = PenLineCap.Round,
+ StrokeLineJoin = PenLineJoin.Round,
+ Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null, // 高亮笔需要填充
+ Opacity = 0.95, // 初始透明度更高,显得更自然
+
+ // 优化渲染质量
+ UseLayoutRounding = false,
+ SnapsToDevicePixels = false
+ };
+
+ // 如果是高亮笔,调整透明度和混合模式
+ if (drawingAttribs.IsHighlighter)
+ {
+ path.Opacity = 0.4; // 高亮笔初始透明度更低,更符合荧光笔特性
+
+ // 为高亮笔添加特殊的混合效果
+ // 使用更柔和的笔触样式
+ path.StrokeStartLineCap = PenLineCap.Flat;
+ path.StrokeEndLineCap = PenLineCap.Flat;
+ path.StrokeLineJoin = PenLineJoin.Miter;
+
+ // 高亮笔通常需要更宽的笔触来覆盖下面的内容
+ if (drawingAttribs.Width < 20)
+ {
+ path.StrokeThickness = Math.Max(drawingAttribs.Width * 1.5, 20);
+ }
+ }
+
+ // 不设置任何变换,保持墨迹原有粗细
+ var bounds = geometry.Bounds;
+
+ // 设置墨迹的初始位置
+ System.Windows.Controls.Canvas.SetLeft(path, bounds.Left);
+ System.Windows.Controls.Canvas.SetTop(path, bounds.Top);
+
+ return path;
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 开始渐隐动画
+ ///
+ /// 要渐隐的墨迹
+ private void StartFadeAnimation(Stroke stroke)
+ {
+ if (!_strokeVisuals.TryGetValue(stroke, out var visual)) return;
+
+ try
+ {
+ _dispatcher.InvokeAsync(() =>
+ {
+ // 获取当前透明度和判断是否为高亮笔
+ var currentOpacity = visual.Opacity;
+ var isHighlighter = stroke.DrawingAttributes.IsHighlighter;
+
+ // 根据墨迹类型选择不同的动画效果
+ if (isHighlighter)
+ {
+ StartHighlighterFadeAnimation(visual, stroke, currentOpacity);
+ }
+ else
+ {
+ StartNormalStrokeFadeAnimation(visual, stroke, currentOpacity);
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"开始渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 开始普通墨迹的渐隐动画
+ ///
+ private void StartNormalStrokeFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity)
+ {
+ try
+ {
+ StartProgressiveFadeAnimation(visual, stroke, currentOpacity, AnimationDuration);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"开始普通墨迹渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 开始高亮笔的渐隐动画
+ ///
+ private void StartHighlighterFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity)
+ {
+ try
+ {
+ StartProgressiveFadeAnimation(visual, stroke, currentOpacity, (int)(AnimationDuration * 1.5));
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"开始高亮笔渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 渐进式渐隐动画 - 从起点到终点逐渐消失
+ ///
+ private void StartProgressiveFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
+ {
+ try
+ {
+ // 确保所有墨迹都能显示动画,包括短墨迹
+ if (stroke.StylusPoints.Count < 2)
+ {
+ // 只有1个点的墨迹也使用分段动画,确保视觉效果
+ CreateSegmentedStroke(visual, stroke, currentOpacity, duration);
+ return;
+ }
+
+ // 将墨迹分段并创建多个 Path
+ CreateSegmentedStroke(visual, stroke, currentOpacity, duration);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"渐进式渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ // 失败时回退到简单动画
+ StartSimpleFadeAnimation(visual, stroke, currentOpacity, duration);
+ }
+ }
+
+ ///
+ /// 创建分段墨迹并开始渐进消失
+ ///
+ private void CreateSegmentedStroke(UIElement originalVisual, Stroke stroke, double opacity, int duration)
+ {
+ try
+ {
+ var stylusPoints = stroke.StylusPoints;
+ var totalPoints = stylusPoints.Count;
+
+ // 分段算法 - 确保所有墨迹都有足够的动画效果
+ var strokeLength = CalculateStrokeLength(stylusPoints);
+ var segmentCount = CalculateOptimalSegmentCount(totalPoints, strokeLength);
+
+ // 强制最小分段数量,确保短墨迹也有动画效果
+ segmentCount = Math.Max(segmentCount, 4);
+
+ var pointsPerSegment = Math.Max(1, totalPoints / segmentCount);
+
+ // 隐藏原始视觉元素
+ originalVisual.Visibility = Visibility.Hidden;
+
+ var segments = new List();
+ var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel;
+ if (parent == null)
+ {
+ // 如果父容器不是Panel,直接使用InkCanvas
+ parent = null; // 稍后会检查并使用InkCanvas.Children
+ }
+
+ // 创建各个分段 - 确保短墨迹也能正确分段
+ for (int i = 0; i < segmentCount; i++)
+ {
+ var startIndex = i * pointsPerSegment;
+ var endIndex = (i == segmentCount - 1) ? totalPoints - 1 : (i + 1) * pointsPerSegment;
+
+ // 确保有足够的点来创建分段,对于短墨迹特殊处理
+ if (endIndex <= startIndex && totalPoints > 1)
+ {
+ // 短墨迹:每个点作为一个分段
+ startIndex = i;
+ endIndex = Math.Min(i + 1, totalPoints - 1);
+ }
+
+ // 为每个分段添加重叠,确保连接处平滑
+ var overlap = Math.Max(1, pointsPerSegment / 6); // 15%的重叠,平衡平滑与速度
+ var actualStartIndex = Math.Max(0, startIndex - overlap);
+ var actualEndIndex = Math.Min(totalPoints - 1, endIndex + overlap);
+
+ var segment = CreateStrokeSegment(stroke, actualStartIndex, actualEndIndex, opacity);
+ if (segment != null)
+ {
+ segments.Add(segment);
+ if (parent != null)
+ {
+ parent.Children.Add(segment);
+ }
+ else if (_mainWindow.inkCanvas != null)
+ {
+ _mainWindow.inkCanvas.Children.Add(segment);
+ }
+ }
+ }
+
+ // 开始分段渐隐动画
+ StartSegmentedFadeAnimation(segments, stroke, originalVisual, duration);
+ }
+ catch (Exception ex)
+ {
+ StartSimpleFadeAnimation(originalVisual, stroke, opacity, duration);
+ }
+ }
+
+ ///
+ /// 创建墨迹分段
+ ///
+ private UIElement CreateStrokeSegment(Stroke originalStroke, int startIndex, int endIndex, double opacity)
+ {
+ try
+ {
+ // 创建分段的 StylusPoint 集合
+ var segmentPoints = new StylusPointCollection();
+ for (int i = startIndex; i <= endIndex && i < originalStroke.StylusPoints.Count; i++)
+ {
+ segmentPoints.Add(originalStroke.StylusPoints[i]);
+ }
+
+ if (segmentPoints.Count < 2) return null;
+
+ // 创建分段墨迹
+ var segmentStroke = new Stroke(segmentPoints)
+ {
+ DrawingAttributes = originalStroke.DrawingAttributes.Clone()
+ };
+
+ // 创建分段的视觉元素
+ var geometry = segmentStroke.GetGeometry();
+ if (geometry == null) return null;
+
+ var drawingAttribs = segmentStroke.DrawingAttributes;
+ var path = new Path
+ {
+ Data = geometry,
+ Stroke = new SolidColorBrush(drawingAttribs.Color),
+ StrokeThickness = drawingAttribs.Width,
+ StrokeStartLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round,
+ StrokeEndLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round,
+ StrokeLineJoin = drawingAttribs.IsHighlighter ? PenLineJoin.Miter : PenLineJoin.Round,
+ Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null,
+ Opacity = opacity,
+ UseLayoutRounding = false,
+ SnapsToDevicePixels = false
+ };
+
+ // 设置位置
+ var bounds = geometry.Bounds;
+ System.Windows.Controls.Canvas.SetLeft(path, bounds.Left);
+ System.Windows.Controls.Canvas.SetTop(path, bounds.Top);
+
+ return path;
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 开始分段渐隐动画
+ ///
+ private void StartSegmentedFadeAnimation(List segments, Stroke originalStroke, UIElement originalVisual, int totalDuration)
+ {
+ try
+ {
+ // 动画时序算法
+ var segmentDuration = CalculateOptimalSegmentDuration(totalDuration, segments.Count);
+ var animationCurve = CreateAppleStyleAnimationCurve(segments.Count, totalDuration);
+
+ // 跟踪动画完成状态
+ var completedSegments = new HashSet();
+ var totalSegments = segments.Count;
+
+ // 渐隐效果 - 使用自然的动画曲线
+ for (int i = 0; i < segments.Count; i++)
+ {
+ var segment = segments[i];
+
+ // 使用预计算的动画曲线获取延迟时间
+ var delay = animationCurve[i];
+
+ // 使用定时器延迟启动每个分段的动画
+ var timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(delay)
+ };
+
+ int segmentIndex = i; // 捕获当前索引
+ timer.Tick += (sender, e) =>
+ {
+ StartSingleSegmentFadeAnimation(segment, segmentDuration, () =>
+ {
+ // 动画完成回调
+ lock (completedSegments)
+ {
+ completedSegments.Add(segment);
+
+ // 检查是否所有分段都完成了
+ if (completedSegments.Count >= totalSegments)
+ {
+ CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
+ }
+ }
+ });
+ timer.Stop();
+ };
+
+ timer.Start();
+ }
+
+ // 设置一个安全超时定时器,防止无限等待
+ var safetyTimeout = totalDuration + (segments.Count * segmentDuration) + 1200; // 额外1.2秒缓冲,确保动画完整
+ var safetyTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(safetyTimeout)
+ };
+
+ safetyTimer.Tick += (sender, e) =>
+ {
+ CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
+ safetyTimer.Stop();
+ };
+
+ safetyTimer.Start();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"分段渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
+ }
+ }
+
+ ///
+ /// 单个分段的渐隐动画
+ ///
+ private void StartSingleSegmentFadeAnimation(UIElement segment, int duration, Action onCompleted = null)
+ {
+ try
+ {
+ // 只使用透明度动画,保持墨迹原有粗细
+ var fadeAnimation = new DoubleAnimation
+ {
+ From = segment.Opacity,
+ To = 0.0,
+ Duration = TimeSpan.FromMilliseconds(duration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut } // 更平滑的缓动
+ };
+
+ // 添加动画完成事件
+ if (onCompleted != null)
+ {
+ fadeAnimation.Completed += (sender, e) =>
+ {
+ onCompleted?.Invoke();
+ };
+ }
+
+ // 只应用透明度动画,不改变墨迹大小
+ segment.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"单个分段渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ // 即使失败也要调用完成回调
+ onCompleted?.Invoke();
+ }
+ }
+
+ ///
+ /// 清理分段动画
+ ///
+ private void CleanupSegmentedAnimation(List segments, Stroke originalStroke, UIElement originalVisual)
+ {
+ try
+ {
+ // 移除所有分段
+ var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel;
+
+ foreach (var segment in segments)
+ {
+ if (parent != null && parent.Children.Contains(segment))
+ {
+ parent.Children.Remove(segment);
+ }
+ else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(segment))
+ {
+ _mainWindow.inkCanvas.Children.Remove(segment);
+ }
+ }
+
+ // 清理原始墨迹
+ OnAnimationCompleted(originalVisual, originalStroke);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"清理分段动画失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 简单渐隐动画(备用方案)
+ ///
+ private void StartSimpleFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
+ {
+ try
+ {
+ var fadeAnimation = new DoubleAnimation
+ {
+ From = currentOpacity,
+ To = 0.0,
+ Duration = TimeSpan.FromMilliseconds(duration),
+ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn }
+ };
+
+ fadeAnimation.Completed += (sender, e) => OnAnimationCompleted(visual, stroke);
+ visual.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"简单渐隐动画失败: {ex}", LogHelper.LogType.Error);
+ OnAnimationCompleted(visual, stroke);
+ }
+ }
+
+ ///
+ /// 计算墨迹的实际长度
+ ///
+ private double CalculateStrokeLength(StylusPointCollection points)
+ {
+ if (points.Count < 2) return 0;
+
+ double totalLength = 0;
+ for (int i = 1; i < points.Count; i++)
+ {
+ var p1 = points[i - 1].ToPoint();
+ var p2 = points[i].ToPoint();
+ totalLength += Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2));
+ }
+ return totalLength;
+ }
+
+ ///
+ /// 根据墨迹特性计算最优分段数量 - 平衡速度与完整性
+ ///
+ private int CalculateOptimalSegmentCount(int pointCount, double strokeLength)
+ {
+ // 平衡速度与完整性,确保动画效果的同时提高速度
+ const double PIXELS_PER_SEGMENT = 12.0; // 每段适中长度,平衡效果与速度
+ const int MIN_SEGMENTS = 5; // 适当的最小分段数,确保动画效果
+ const int MAX_SEGMENTS = 100; // 适中的最大分段数,平衡性能与效果
+
+ // 根据长度计算基础分段数
+ var lengthBasedSegments = Math.Max(MIN_SEGMENTS, (int)(strokeLength / PIXELS_PER_SEGMENT));
+
+ // 根据点密度调整,平衡效果与速度
+ var density = pointCount > 0 ? strokeLength / pointCount : 1;
+ var densityFactor = Math.Max(0.4, Math.Min(2.5, density / 1.8));
+
+ var finalSegments = (int)(lengthBasedSegments * densityFactor);
+
+ // 对于短墨迹,确保至少有4个分段
+ if (pointCount <= 5)
+ {
+ finalSegments = Math.Max(finalSegments, 4);
+ }
+
+ // 限制在合理范围内
+ return Math.Min(MAX_SEGMENTS, Math.Max(MIN_SEGMENTS, finalSegments));
+ }
+
+ ///
+ /// 计算最优的单段动画持续时间 - 平衡速度与完整性
+ ///
+ private int CalculateOptimalSegmentDuration(int totalDuration, int segmentCount)
+ {
+ // 平衡速度与动画完整性
+ var baseDuration = totalDuration / Math.Max(segmentCount, 1);
+ var minDuration = 150; // 每段最少150ms,确保动画完整显示
+ var maxDuration = 500; // 每段最多500ms,平衡速度与完整性
+
+ return Math.Max(minDuration, Math.Min(maxDuration, baseDuration));
+ }
+
+ ///
+ /// 创建优化的动画时间曲线 - 平衡速度与完整性
+ ///
+ private int[] CreateAppleStyleAnimationCurve(int segmentCount, int totalDuration)
+ {
+ var curve = new int[segmentCount];
+
+ // 平衡速度与完整性,确保动画有足够时间播放
+ var availableTime = totalDuration * 0.6; // 使用60%的总时间,给动画留足够缓冲
+ var delayBetweenSegments = Math.Max(60, availableTime / Math.Max(segmentCount, 1));
+
+ for (int i = 0; i < segmentCount; i++)
+ {
+ // 线性延迟,确保每个分段都有足够时间
+ curve[i] = (int)(i * delayBetweenSegments);
+ }
+
+ return curve;
+ }
+
+ ///
+ /// 动画完成后的统一处理
+ ///
+ private void OnAnimationCompleted(UIElement visual, Stroke stroke)
+ {
+ try
+ {
+ // 从父容器中移除墨迹
+ var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel;
+ if (parent != null && parent.Children.Contains(visual))
+ {
+ parent.Children.Remove(visual);
+ }
+ else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual))
+ {
+ _mainWindow.inkCanvas.Children.Remove(visual);
+ }
+
+ RemoveStroke(stroke);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"渐隐动画完成后清理墨迹失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/PPTInkManager.cs b/Ink Canvas/Helpers/PPTInkManager.cs
index 0c548829..f8c62ccb 100644
--- a/Ink Canvas/Helpers/PPTInkManager.cs
+++ b/Ink Canvas/Helpers/PPTInkManager.cs
@@ -55,6 +55,13 @@ namespace Ink_Canvas.Helpers
{
try
{
+ // 完全清理之前的墨迹状态
+ ClearAllStrokes();
+
+ // 重置墨迹锁定状态
+ _inkLockUntil = DateTime.MinValue;
+ _lockedSlideIndex = -1;
+
// 生成演示文稿唯一标识符
_currentPresentationId = GeneratePresentationId(presentation);
@@ -152,17 +159,28 @@ namespace Ink_Canvas.Helpers
{
try
{
- // 如果有当前墨迹,先保存
+ // 如果有当前墨迹,先保存到正确的页面
if (currentStrokes != null && currentStrokes.Count > 0)
{
- SaveCurrentSlideStrokes(_lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex, currentStrokes);
+ // 确定要保存的页面索引
+ int saveToSlideIndex = _lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex;
+
+ // 确保页面索引有效
+ if (saveToSlideIndex > 0 && saveToSlideIndex < _memoryStreams.Length)
+ {
+ SaveCurrentSlideStrokes(saveToSlideIndex, currentStrokes);
+ LogHelper.WriteLogToFile($"已保存第{saveToSlideIndex}页墨迹,墨迹数量: {currentStrokes.Count}", LogHelper.LogType.Trace);
+ }
}
// 设置墨迹锁定
LockInkForSlide(slideIndex);
// 加载新页面的墨迹
- return LoadSlideStrokes(slideIndex);
+ var newStrokes = LoadSlideStrokes(slideIndex);
+ LogHelper.WriteLogToFile($"已切换到第{slideIndex}页,加载墨迹数量: {newStrokes.Count}", LogHelper.LogType.Trace);
+
+ return newStrokes;
}
catch (Exception ex)
{
diff --git a/Ink Canvas/Helpers/PPTManager.cs b/Ink Canvas/Helpers/PPTManager.cs
index 1a2d5a90..c51fa589 100644
--- a/Ink Canvas/Helpers/PPTManager.cs
+++ b/Ink Canvas/Helpers/PPTManager.cs
@@ -25,6 +25,7 @@ namespace Ink_Canvas.Helpers
public event Action PresentationOpen;
public event Action PresentationClose;
public event Action PPTConnectionChanged;
+ public event Action SlideShowStateChanged;
#endregion
#region Properties
@@ -92,6 +93,7 @@ namespace Ink_Canvas.Helpers
#region Private Fields
private Timer _connectionCheckTimer;
+ private Timer _slideShowStateCheckTimer;
private Timer _wpsProcessCheckTimer;
private Process _wpsProcess;
private bool _hasWpsProcessId;
@@ -99,6 +101,7 @@ namespace Ink_Canvas.Helpers
private int _wpsProcessCheckCount;
private WpsWindowInfo _lastForegroundWpsWindow;
private DateTime _lastWindowCheckTime = DateTime.MinValue;
+ private bool _lastSlideShowState = false;
private readonly object _lockObject = new object();
private bool _disposed = false;
#endregion
@@ -114,6 +117,10 @@ namespace Ink_Canvas.Helpers
_connectionCheckTimer = new Timer(500);
_connectionCheckTimer.Elapsed += OnConnectionCheckTimerElapsed;
_connectionCheckTimer.AutoReset = true;
+
+ _slideShowStateCheckTimer = new Timer(1000);
+ _slideShowStateCheckTimer.Elapsed += OnSlideShowStateCheckTimerElapsed;
+ _slideShowStateCheckTimer.AutoReset = true;
}
public void StartMonitoring()
@@ -121,6 +128,7 @@ namespace Ink_Canvas.Helpers
if (!_disposed)
{
_connectionCheckTimer?.Start();
+ _slideShowStateCheckTimer?.Start();
LogHelper.WriteLogToFile("PPT监控已启动", LogHelper.LogType.Trace);
}
}
@@ -128,6 +136,7 @@ namespace Ink_Canvas.Helpers
public void StopMonitoring()
{
_connectionCheckTimer?.Stop();
+ _slideShowStateCheckTimer?.Stop();
DisconnectFromPPT();
LogHelper.WriteLogToFile("PPT监控已停止", LogHelper.LogType.Trace);
}
@@ -146,6 +155,18 @@ namespace Ink_Canvas.Helpers
}
}
+ private void OnSlideShowStateCheckTimerElapsed(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ CheckSlideShowState();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"PPT放映状态检查失败: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
private void CheckAndConnectToPPT()
{
lock (_lockObject)
@@ -187,6 +208,30 @@ namespace Ink_Canvas.Helpers
}
}
+ private void CheckSlideShowState()
+ {
+ try
+ {
+ if (!IsConnected) return;
+
+ var currentSlideShowState = IsInSlideShow;
+ if (currentSlideShowState != _lastSlideShowState)
+ {
+ _lastSlideShowState = currentSlideShowState;
+ SlideShowStateChanged?.Invoke(currentSlideShowState);
+
+ if (!currentSlideShowState)
+ {
+ LogHelper.WriteLogToFile("检测到PPT放映已结束", LogHelper.LogType.Trace);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"检查PPT放映状态异常: {ex}", LogHelper.LogType.Error);
+ }
+ }
+
private Microsoft.Office.Interop.PowerPoint.Application TryConnectToPowerPoint()
{
try
@@ -1627,6 +1672,7 @@ namespace Ink_Canvas.Helpers
StopWpsProcessCheckTimer();
_connectionCheckTimer?.Dispose();
+ _slideShowStateCheckTimer?.Dispose();
_wpsProcessCheckTimer?.Dispose();
_disposed = true;
diff --git a/Ink Canvas/Helpers/PPTUIManager.cs b/Ink Canvas/Helpers/PPTUIManager.cs
index ca235958..5bf4b636 100644
--- a/Ink Canvas/Helpers/PPTUIManager.cs
+++ b/Ink Canvas/Helpers/PPTUIManager.cs
@@ -18,7 +18,10 @@ namespace Ink_Canvas.Helpers
public int PPTBButtonsOption { get; set; } = 121;
public int PPTLSButtonPosition { get; set; } = 0;
public int PPTRSButtonPosition { get; set; } = 0;
+ public int PPTLBButtonPosition { get; set; } = 0;
+ public int PPTRBButtonPosition { get; set; } = 0;
public bool EnablePPTButtonPageClickable { get; set; } = true;
+ public bool EnablePPTButtonLongPressPageTurn { get; set; } = true;
#endregion
#region Private Fields
@@ -120,6 +123,29 @@ namespace Ink_Canvas.Helpers
});
}
+ ///
+ /// 处理PPT放映状态变化
+ ///
+ public void OnSlideShowStateChanged(bool isInSlideShow)
+ {
+ _dispatcher.InvokeAsync(() =>
+ {
+ try
+ {
+ if (!isInSlideShow)
+ {
+ // 如果不在放映模式,隐藏所有导航面板
+ HideAllNavigationPanels();
+ LogHelper.WriteLogToFile("PPT放映状态变化:隐藏导航面板", LogHelper.LogType.Trace);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"处理PPT放映状态变化失败: {ex}", LogHelper.LogType.Error);
+ }
+ });
+ }
+
///
/// 更新导航面板显示状态
///
@@ -130,7 +156,10 @@ namespace Ink_Canvas.Helpers
try
{
// 检查是否应该显示PPT按钮
- bool shouldShowButtons = ShowPPTButton && _mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible;
+ // 不仅要检查按钮设置,还要确保确实在PPT放映模式下
+ bool shouldShowButtons = ShowPPTButton &&
+ _mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
+ _mainWindow.PPTManager?.IsInSlideShow == true;
if (!shouldShowButtons)
{
@@ -142,6 +171,10 @@ namespace Ink_Canvas.Helpers
_mainWindow.LeftSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTLSButtonPosition * 2);
_mainWindow.RightSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTRSButtonPosition * 2);
+ // 设置底部按钮水平位置
+ _mainWindow.LeftBottomPanelForPPTNavigation.Margin = new Thickness(6 + PPTLBButtonPosition, 0, 0, 6);
+ _mainWindow.RightBottomPanelForPPTNavigation.Margin = new Thickness(0, 0, 6 + PPTRBButtonPosition, 6);
+
// 根据显示选项设置面板可见性
var displayOption = PPTButtonsDisplayOption.ToString();
if (displayOption.Length >= 4)
diff --git a/Ink Canvas/HotkeyConfig.json b/Ink Canvas/HotkeyConfig.json
new file mode 100644
index 00000000..4ce2d1ee
--- /dev/null
+++ b/Ink Canvas/HotkeyConfig.json
@@ -0,0 +1,96 @@
+{
+ "Version": "1.0",
+ "LastModified": "2025-01-28T15:30:00",
+ "Hotkeys": [
+ {
+ "Name": "Undo",
+ "Key": "Z",
+ "Modifiers": "Control"
+ },
+ {
+ "Name": "Redo",
+ "Key": "Y",
+ "Modifiers": "Control"
+ },
+ {
+ "Name": "Clear",
+ "Key": "E",
+ "Modifiers": "Control"
+ },
+ {
+ "Name": "Paste",
+ "Key": "V",
+ "Modifiers": "Control"
+ },
+ {
+ "Name": "SelectTool",
+ "Key": "S",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "DrawTool",
+ "Key": "D",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "EraserTool",
+ "Key": "E",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "BlackboardTool",
+ "Key": "B",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "QuitDrawTool",
+ "Key": "Q",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Pen1",
+ "Key": "D1",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Pen2",
+ "Key": "D2",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Pen3",
+ "Key": "D3",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Pen4",
+ "Key": "D4",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Pen5",
+ "Key": "D5",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "DrawLine",
+ "Key": "L",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Screenshot",
+ "Key": "C",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Hide",
+ "Key": "V",
+ "Modifiers": "Alt"
+ },
+ {
+ "Name": "Exit",
+ "Key": "Escape",
+ "Modifiers": "None"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml
index ae433f8f..da24ff88 100644
--- a/Ink Canvas/MainWindow.xaml
+++ b/Ink Canvas/MainWindow.xaml
@@ -832,6 +832,27 @@
IsOn="True" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchAdvancedBezierSmoothing_Toggled" />
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1197,16 +1218,16 @@
-
+
-
+
@@ -1215,16 +1236,16 @@
-
+
-
+
@@ -1233,16 +1254,16 @@
-
+
-
+
@@ -1251,16 +1272,16 @@
-
+
-
+
@@ -1268,9 +1289,9 @@
-
+
@@ -1430,9 +1451,21 @@
+ VerticalAlignment="Bottom" HorizontalAlignment="Left" RenderTransformOrigin="0.5,0.5">
+
+
+
+
+
+
+ VerticalAlignment="Bottom" HorizontalAlignment="Right" RenderTransformOrigin="0.5,0.5">
+
+
+
+
+
+
@@ -1618,6 +1651,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3860,7 +4061,7 @@
-
@@ -3874,7 +4075,7 @@
Background="#fafafa"
Opacity="1" BorderBrush="#2563eb"
BorderThickness="1" CornerRadius="8">
-
+
-
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
+ Width="80" Margin="10,0,0,0"
+ Toggled="ToggleSwitchEnableNibMode_Toggled"
+ IsOn="True" />
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -6900,7 +7113,7 @@
-
+
-
-
-
+
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -7768,7 +7991,7 @@
+ CornerRadius="5" Margin="-170,-140,-147,37">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/HotkeyItem.xaml.cs b/Ink Canvas/Windows/HotkeyItem.xaml.cs
new file mode 100644
index 00000000..4f498877
--- /dev/null
+++ b/Ink Canvas/Windows/HotkeyItem.xaml.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace Ink_Canvas.Windows
+{
+ ///
+ /// 快捷键项控件
+ ///
+ public partial class HotkeyItem : UserControl
+ {
+ #region Events
+ ///
+ /// 快捷键变更事件
+ ///
+ public event EventHandler HotkeyChanged;
+ #endregion
+
+ #region Properties
+ public string Title
+ {
+ get => TitleTextBlock.Text;
+ set => TitleTextBlock.Text = value;
+ }
+
+ public string Description
+ {
+ get => DescriptionTextBlock.Text;
+ set => DescriptionTextBlock.Text = value;
+ }
+
+ public string DefaultKey { get; set; }
+ public string DefaultModifiers { get; set; }
+
+ ///
+ /// 快捷键名称(用于标识,如"Undo")
+ ///
+ public string HotkeyName { get; set; }
+
+ private Key _currentKey = Key.None;
+ private ModifierKeys _currentModifiers = ModifierKeys.None;
+ #endregion
+
+ #region Constructor
+ public HotkeyItem()
+ {
+ InitializeComponent();
+ UpdateHotkeyDisplay();
+ }
+ #endregion
+
+ #region Public Methods
+ ///
+ /// 设置当前快捷键
+ ///
+ /// 按键
+ /// 修饰键
+ public void SetCurrentHotkey(Key key, ModifierKeys modifiers)
+ {
+ _currentKey = key;
+ _currentModifiers = modifiers;
+ UpdateHotkeyDisplay();
+ }
+
+ ///
+ /// 获取当前快捷键
+ ///
+ /// 快捷键信息
+ public (Key key, ModifierKeys modifiers) GetCurrentHotkey()
+ {
+ return (_currentKey, _currentModifiers);
+ }
+ #endregion
+
+ #region Private Methods
+ private void UpdateHotkeyDisplay()
+ {
+ if (_currentKey == Key.None)
+ {
+ CurrentHotkeyTextBlock.Text = "未设置";
+ CurrentHotkeyTextBlock.Foreground = System.Windows.Media.Brushes.Gray;
+ }
+ else
+ {
+ var modifiersText = _currentModifiers == ModifierKeys.None ? "" : $"{_currentModifiers}+";
+ CurrentHotkeyTextBlock.Text = $"{modifiersText}{_currentKey}";
+ CurrentHotkeyTextBlock.Foreground = System.Windows.Media.Brushes.Black;
+ }
+ }
+
+ private void StartHotkeyCapture()
+ {
+ BtnSetHotkey.Content = "请按键...";
+ BtnSetHotkey.Background = System.Windows.Media.Brushes.Orange;
+
+ // 设置焦点以捕获键盘事件
+ Focus();
+
+ // 添加键盘事件处理器
+ KeyDown += HotkeyItem_KeyDown;
+ KeyUp += HotkeyItem_KeyUp;
+ }
+
+ private void StopHotkeyCapture()
+ {
+ BtnSetHotkey.Content = "设置";
+ BtnSetHotkey.Background = System.Windows.Media.Brushes.DodgerBlue;
+
+ // 移除键盘事件处理器
+ KeyDown -= HotkeyItem_KeyDown;
+ KeyUp -= HotkeyItem_KeyUp;
+ }
+
+ private void HotkeyItem_KeyDown(object sender, KeyEventArgs e)
+ {
+ e.Handled = true;
+
+ // 忽略某些特殊键
+ if (e.Key == Key.LeftShift || e.Key == Key.RightShift ||
+ e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl ||
+ e.Key == Key.LeftAlt || e.Key == Key.RightAlt ||
+ e.Key == Key.LWin || e.Key == Key.RWin)
+ {
+ return;
+ }
+
+ // 获取修饰键
+ var modifiers = ModifierKeys.None;
+ if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
+ modifiers |= ModifierKeys.Control;
+ if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
+ modifiers |= ModifierKeys.Shift;
+ if (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt))
+ modifiers |= ModifierKeys.Alt;
+ if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin))
+ modifiers |= ModifierKeys.Windows;
+
+ // 设置新的快捷键
+ var oldKey = _currentKey;
+ var oldModifiers = _currentModifiers;
+
+ _currentKey = e.Key;
+ _currentModifiers = modifiers;
+
+ UpdateHotkeyDisplay();
+ StopHotkeyCapture();
+
+ // 触发快捷键变更事件
+ HotkeyChanged?.Invoke(this, new HotkeyChangedEventArgs
+ {
+ HotkeyName = HotkeyName ?? Title, // 优先使用HotkeyName,如果没有则使用Title
+ Key = _currentKey,
+ Modifiers = _currentModifiers
+ });
+ }
+
+ private void HotkeyItem_KeyUp(object sender, KeyEventArgs e)
+ {
+ e.Handled = true;
+ }
+ #endregion
+
+ #region Event Handlers
+ private void BtnSetHotkey_Click(object sender, RoutedEventArgs e)
+ {
+ StartHotkeyCapture();
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Windows/HotkeySettingsWindow.xaml b/Ink Canvas/Windows/HotkeySettingsWindow.xaml
new file mode 100644
index 00000000..68bde7dc
--- /dev/null
+++ b/Ink Canvas/Windows/HotkeySettingsWindow.xaml
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/HotkeySettingsWindow.xaml.cs b/Ink Canvas/Windows/HotkeySettingsWindow.xaml.cs
new file mode 100644
index 00000000..31289032
--- /dev/null
+++ b/Ink Canvas/Windows/HotkeySettingsWindow.xaml.cs
@@ -0,0 +1,570 @@
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using Ink_Canvas.Helpers;
+
+namespace Ink_Canvas.Windows
+{
+ ///
+ /// 快捷键设置窗口
+ ///
+ public partial class HotkeySettingsWindow : Window
+ {
+ #region Private Fields
+ private readonly MainWindow _mainWindow;
+ private readonly GlobalHotkeyManager _hotkeyManager;
+ private readonly Dictionary _hotkeyItems;
+ #endregion
+
+ #region Constructor
+ public HotkeySettingsWindow(MainWindow mainWindow, GlobalHotkeyManager hotkeyManager)
+ {
+ InitializeComponent();
+ _mainWindow = mainWindow;
+ _hotkeyManager = hotkeyManager;
+ _hotkeyItems = new Dictionary();
+
+ // 隐藏主窗口的设置页面
+ HideMainWindowSettings();
+ InitializeHotkeyItems();
+
+ // 延迟加载快捷键,确保快捷键管理器已完全初始化
+ this.Loaded += (s, e) =>
+ {
+ try
+ {
+ // 不启用快捷键注册功能,只读取配置文件中的快捷键信息用于显示
+ // 这样用户可以看到配置文件中保存的快捷键,但不会自动注册
+
+ // 加载当前快捷键(包括配置文件中的)
+ LoadCurrentHotkeys();
+ SetupEventHandlers();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"快捷键设置窗口初始化时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ };
+
+ // 注册窗口关闭事件
+ this.Closed += HotkeySettingsWindow_Closed;
+ }
+ #endregion
+
+ #region Private Methods
+ private void InitializeHotkeyItems()
+ {
+ try
+ {
+ LogHelper.WriteLogToFile("开始初始化快捷键项", LogHelper.LogType.Info);
+
+ // 初始化快捷键项并设置HotkeyName
+ _hotkeyItems["Undo"] = UndoHotkey;
+ UndoHotkey.HotkeyName = "Undo";
+
+ _hotkeyItems["Redo"] = RedoHotkey;
+ RedoHotkey.HotkeyName = "Redo";
+
+ _hotkeyItems["Clear"] = ClearHotkey;
+ ClearHotkey.HotkeyName = "Clear";
+
+ _hotkeyItems["Paste"] = PasteHotkey;
+ PasteHotkey.HotkeyName = "Paste";
+
+ _hotkeyItems["SelectTool"] = SelectToolHotkey;
+ SelectToolHotkey.HotkeyName = "SelectTool";
+
+ _hotkeyItems["DrawTool"] = DrawToolHotkey;
+ DrawToolHotkey.HotkeyName = "DrawTool";
+
+ _hotkeyItems["EraserTool"] = EraserToolHotkey;
+ EraserToolHotkey.HotkeyName = "EraserTool";
+
+ _hotkeyItems["BlackboardTool"] = BlackboardToolHotkey;
+ BlackboardToolHotkey.HotkeyName = "BlackboardTool";
+
+ _hotkeyItems["QuitDrawTool"] = QuitDrawToolHotkey;
+ QuitDrawToolHotkey.HotkeyName = "QuitDrawTool";
+
+ _hotkeyItems["Pen1"] = Pen1Hotkey;
+ Pen1Hotkey.HotkeyName = "Pen1";
+
+ _hotkeyItems["Pen2"] = Pen2Hotkey;
+ Pen2Hotkey.HotkeyName = "Pen2";
+
+ _hotkeyItems["Pen3"] = Pen3Hotkey;
+ Pen3Hotkey.HotkeyName = "Pen3";
+
+ _hotkeyItems["Pen4"] = Pen4Hotkey;
+ Pen4Hotkey.HotkeyName = "Pen4";
+
+ _hotkeyItems["Pen5"] = Pen5Hotkey;
+ Pen5Hotkey.HotkeyName = "Pen5";
+
+ _hotkeyItems["DrawLine"] = DrawLineHotkey;
+ DrawLineHotkey.HotkeyName = "DrawLine";
+
+ _hotkeyItems["Screenshot"] = ScreenshotHotkey;
+ ScreenshotHotkey.HotkeyName = "Screenshot";
+
+ _hotkeyItems["Hide"] = HideHotkey;
+ HideHotkey.HotkeyName = "Hide";
+
+ _hotkeyItems["Exit"] = ExitHotkey;
+ ExitHotkey.HotkeyName = "Exit";
+
+ LogHelper.WriteLogToFile($"成功初始化 {_hotkeyItems.Count} 个快捷键项", LogHelper.LogType.Info);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"初始化快捷键项时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private void LoadCurrentHotkeys()
+ {
+ try
+ {
+ // 首先尝试从配置文件获取快捷键信息
+ var configHotkeys = _hotkeyManager.GetHotkeysFromConfigFile();
+ LogHelper.WriteLogToFile($"配置文件中的快捷键数量: {configHotkeys.Count}", LogHelper.LogType.Info);
+
+ // 显示配置文件中的快捷键
+ foreach (var hotkey in configHotkeys)
+ {
+ if (_hotkeyItems.TryGetValue(hotkey.Name, out var hotkeyItem))
+ {
+ hotkeyItem.SetCurrentHotkey(hotkey.Key, hotkey.Modifiers);
+ LogHelper.WriteLogToFile($"从配置文件设置快捷键项: {hotkey.Name} -> {hotkey.Modifiers}+{hotkey.Key}", LogHelper.LogType.Info);
+ }
+ }
+
+ // 为没有快捷键的项目设置默认显示值(仅用于UI显示,不实际注册)
+ foreach (var kvp in _hotkeyItems)
+ {
+ var hotkeyItem = kvp.Value;
+ if (hotkeyItem.GetCurrentHotkey().key == Key.None)
+ {
+ // 根据DefaultKey和DefaultModifiers设置默认显示值
+ SetDefaultHotkeyForItem(hotkeyItem);
+ LogHelper.WriteLogToFile($"设置默认显示值: {hotkeyItem.HotkeyName}", LogHelper.LogType.Info);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"加载当前快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 为快捷键项设置默认值
+ ///
+ private void SetDefaultHotkeyForItem(HotkeyItem hotkeyItem)
+ {
+ try
+ {
+ // 根据HotkeyName设置默认快捷键
+ switch (hotkeyItem.HotkeyName)
+ {
+ case "Undo":
+ hotkeyItem.SetCurrentHotkey(Key.Z, ModifierKeys.Control);
+ break;
+ case "Redo":
+ hotkeyItem.SetCurrentHotkey(Key.Y, ModifierKeys.Control);
+ break;
+ case "Clear":
+ hotkeyItem.SetCurrentHotkey(Key.E, ModifierKeys.Control);
+ break;
+ case "Paste":
+ hotkeyItem.SetCurrentHotkey(Key.V, ModifierKeys.Control);
+ break;
+ case "SelectTool":
+ hotkeyItem.SetCurrentHotkey(Key.S, ModifierKeys.Alt);
+ break;
+ case "DrawTool":
+ hotkeyItem.SetCurrentHotkey(Key.D, ModifierKeys.Alt);
+ break;
+ case "EraserTool":
+ hotkeyItem.SetCurrentHotkey(Key.E, ModifierKeys.Alt);
+ break;
+ case "BlackboardTool":
+ hotkeyItem.SetCurrentHotkey(Key.B, ModifierKeys.Alt);
+ break;
+ case "QuitDrawTool":
+ hotkeyItem.SetCurrentHotkey(Key.Q, ModifierKeys.Alt);
+ break;
+ case "Pen1":
+ hotkeyItem.SetCurrentHotkey(Key.D1, ModifierKeys.Alt);
+ break;
+ case "Pen2":
+ hotkeyItem.SetCurrentHotkey(Key.D2, ModifierKeys.Alt);
+ break;
+ case "Pen3":
+ hotkeyItem.SetCurrentHotkey(Key.D3, ModifierKeys.Alt);
+ break;
+ case "Pen4":
+ hotkeyItem.SetCurrentHotkey(Key.D4, ModifierKeys.Alt);
+ break;
+ case "Pen5":
+ hotkeyItem.SetCurrentHotkey(Key.D5, ModifierKeys.Alt);
+ break;
+ case "DrawLine":
+ hotkeyItem.SetCurrentHotkey(Key.L, ModifierKeys.Alt);
+ break;
+ case "Screenshot":
+ hotkeyItem.SetCurrentHotkey(Key.C, ModifierKeys.Alt);
+ break;
+ case "Hide":
+ hotkeyItem.SetCurrentHotkey(Key.V, ModifierKeys.Alt);
+ break;
+ case "Exit":
+ hotkeyItem.SetCurrentHotkey(Key.Escape, ModifierKeys.None);
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ // 设置默认快捷键时出错,忽略
+ }
+ }
+
+ private void SetupEventHandlers()
+ {
+ // 为每个快捷键项设置事件处理器
+ foreach (var hotkeyItem in _hotkeyItems.Values)
+ {
+ hotkeyItem.HotkeyChanged += OnHotkeyChanged;
+ }
+ }
+
+ private void OnHotkeyChanged(object sender, HotkeyChangedEventArgs e)
+ {
+ try
+ {
+ LogHelper.WriteLogToFile($"收到快捷键变更事件: {e.HotkeyName} -> {e.Modifiers}+{e.Key}", LogHelper.LogType.Info);
+
+ // 检查快捷键冲突
+ if (IsHotkeyConflict(e.Key, e.Modifiers, e.HotkeyName))
+ {
+ MessageBox.Show($"快捷键 {e.Modifiers}+{e.Key} 已被其他功能使用,请选择其他组合。",
+ "快捷键冲突", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // 更新快捷键管理器
+ UpdateHotkeyInManager(e.HotkeyName, e.Key, e.Modifiers);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"处理快捷键变更时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private bool IsHotkeyConflict(Key key, ModifierKeys modifiers, string excludeHotkeyName)
+ {
+ // 检查是否与已注册的快捷键冲突
+ var registeredHotkeys = _hotkeyManager.GetRegisteredHotkeys();
+ foreach (var hotkey in registeredHotkeys)
+ {
+ if (hotkey.Name != excludeHotkeyName &&
+ hotkey.Key == key &&
+ hotkey.Modifiers == modifiers)
+ {
+ return true;
+ }
+ }
+
+ // 检查是否与默认快捷键冲突(如果当前快捷键项还没有注册)
+ if (excludeHotkeyName != null && _hotkeyItems.TryGetValue(excludeHotkeyName, out var currentItem))
+ {
+ var currentHotkey = currentItem.GetCurrentHotkey();
+ if (currentHotkey.key == Key.None)
+ {
+ // 如果当前项还没有快捷键,检查是否与其他默认快捷键冲突
+ foreach (var kvp in _hotkeyItems)
+ {
+ if (kvp.Key != excludeHotkeyName)
+ {
+ var item = kvp.Value;
+ var itemHotkey = item.GetCurrentHotkey();
+ if (itemHotkey.key == key && itemHotkey.modifiers == modifiers)
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void UpdateHotkeyInManager(string hotkeyName, Key key, ModifierKeys modifiers)
+ {
+ try
+ {
+ LogHelper.WriteLogToFile($"开始更新快捷键: {hotkeyName} -> {modifiers}+{key}", LogHelper.LogType.Info);
+
+ // 先注销原有的快捷键(如果存在)
+ _hotkeyManager.UnregisterHotkey(hotkeyName);
+ LogHelper.WriteLogToFile($"已注销原有快捷键: {hotkeyName}", LogHelper.LogType.Info);
+
+ // 根据快捷键名称获取对应的动作
+ var action = GetActionForHotkey(hotkeyName);
+ if (action != null)
+ {
+ LogHelper.WriteLogToFile($"找到快捷键动作: {hotkeyName}", LogHelper.LogType.Info);
+
+ // 直接注册新的快捷键
+ if (_hotkeyManager.RegisterHotkey(hotkeyName, key, modifiers, action))
+ {
+ LogHelper.WriteLogToFile($"成功注册新快捷键: {hotkeyName} -> {modifiers}+{key}", LogHelper.LogType.Info);
+
+ // 立即保存到配置文件
+ _hotkeyManager.SaveHotkeysToSettings();
+ LogHelper.WriteLogToFile($"已保存快捷键配置", LogHelper.LogType.Info);
+
+ // 更新UI显示
+ LoadCurrentHotkeys();
+ LogHelper.WriteLogToFile($"已更新UI显示", LogHelper.LogType.Info);
+
+ LogHelper.WriteLogToFile($"快捷键 {hotkeyName} 已更新为 {modifiers}+{key} 并保存", LogHelper.LogType.Event);
+ }
+ else
+ {
+ LogHelper.WriteLogToFile($"更新快捷键 {hotkeyName} 失败", LogHelper.LogType.Error);
+ }
+ }
+ else
+ {
+ LogHelper.WriteLogToFile($"未找到快捷键 {hotkeyName} 对应的动作", LogHelper.LogType.Warning);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"更新快捷键管理器时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private Action GetActionForHotkey(string hotkeyName)
+ {
+ switch (hotkeyName)
+ {
+ case "Undo":
+ return () => _mainWindow.SymbolIconUndo_MouseUp(null, null);
+ case "Redo":
+ return () => _mainWindow.SymbolIconRedo_MouseUp(null, null);
+ case "Clear":
+ return () => _mainWindow.SymbolIconDelete_MouseUp(null, null);
+ case "Paste":
+ return () => _mainWindow.HandleGlobalPaste(null, null);
+ case "SelectTool":
+ return () => _mainWindow.SymbolIconSelect_MouseUp(null, null);
+ case "DrawTool":
+ return () => _mainWindow.PenIcon_Click(null, null);
+ case "EraserTool":
+ return () => _mainWindow.EraserIcon_Click(null, null);
+ case "BlackboardTool":
+ return () => _mainWindow.ImageBlackboard_MouseUp(null, null);
+ case "QuitDrawTool":
+ return () => _mainWindow.CursorIcon_Click(null, null);
+ case "Pen1":
+ return () => SwitchToPenType(0);
+ case "Pen2":
+ return () => SwitchToPenType(1);
+ case "Pen3":
+ return () => SwitchToPenType(2);
+ case "Pen4":
+ return () => SwitchToPenType(3);
+ case "Pen5":
+ return () => SwitchToPenType(4);
+ case "DrawLine":
+ return () => _mainWindow.BtnDrawLine_Click(null, null);
+ case "Screenshot":
+ return () => _mainWindow.SaveScreenShotToDesktop();
+ case "Hide":
+ return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null);
+ case "Exit":
+ return () => _mainWindow.KeyExit(null, null);
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// 切换到指定笔类型
+ ///
+ /// 笔类型索引
+ private void SwitchToPenType(int penTypeIndex)
+ {
+ try
+ {
+ // 通过反射访问主窗口的penType字段
+ var penTypeField = _mainWindow.GetType().GetField("penType",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ if (penTypeField != null)
+ {
+ penTypeField.SetValue(_mainWindow, penTypeIndex);
+
+ // 调用CheckPenTypeUIState方法更新UI状态
+ var checkPenTypeMethod = _mainWindow.GetType().GetMethod("CheckPenTypeUIState",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ if (checkPenTypeMethod != null)
+ {
+ checkPenTypeMethod.Invoke(_mainWindow, null);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"切换到笔类型{penTypeIndex}时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+ #endregion
+
+ #region MainWindow Settings Management
+ ///
+ /// 隐藏主窗口的设置页面
+ ///
+ private void HideMainWindowSettings()
+ {
+ try
+ {
+ // 通过反射访问主窗口的设置面板
+ var settingsBorder = _mainWindow.GetType().GetField("BorderSettings",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(_mainWindow) as System.Windows.Controls.Border;
+
+ if (settingsBorder != null)
+ {
+ settingsBorder.Visibility = System.Windows.Visibility.Collapsed;
+ }
+
+ // 隐藏设置蒙版
+ var settingsMask = _mainWindow.GetType().GetField("BorderSettingsMask",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(_mainWindow) as System.Windows.Controls.Border;
+
+ if (settingsMask != null)
+ {
+ settingsMask.Visibility = System.Windows.Visibility.Collapsed;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"隐藏主窗口设置页面时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 显示主窗口的设置页面
+ ///
+ private void ShowMainWindowSettings()
+ {
+ try
+ {
+ // 通过反射访问主窗口的设置面板
+ var settingsBorder = _mainWindow.GetType().GetField("BorderSettings",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(_mainWindow) as System.Windows.Controls.Border;
+
+ if (settingsBorder != null)
+ {
+ settingsBorder.Visibility = System.Windows.Visibility.Visible;
+ }
+
+ // 显示设置蒙版
+ var settingsMask = _mainWindow.GetType().GetField("BorderSettingsMask",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(_mainWindow) as System.Windows.Controls.Border;
+
+ if (settingsMask != null)
+ {
+ settingsMask.Visibility = System.Windows.Visibility.Visible;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"显示主窗口设置页面时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+ #endregion
+
+ #region Window Event Handlers
+ ///
+ /// 窗口关闭事件处理
+ ///
+ private void HotkeySettingsWindow_Closed(object sender, EventArgs e)
+ {
+ // 恢复主窗口设置页面的显示
+ ShowMainWindowSettings();
+ }
+ #endregion
+
+ #region Event Handlers
+ private void BtnClose_Click(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void BtnResetToDefault_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var result = MessageBox.Show("确定要重置所有快捷键为默认设置吗?",
+ "确认重置", MessageBoxButton.YesNo, MessageBoxImage.Question);
+ if (result == MessageBoxResult.Yes)
+ {
+ // 先注销所有现有快捷键
+ _hotkeyManager.UnregisterAllHotkeys();
+
+ // 重置为默认快捷键
+ _hotkeyManager.RegisterDefaultHotkeys();
+
+ // 立即保存到配置文件
+ _hotkeyManager.SaveHotkeysToSettings();
+
+ // 更新UI显示
+ LoadCurrentHotkeys();
+
+ MessageBox.Show("快捷键已重置为默认设置。", "重置完成", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"重置快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
+ MessageBox.Show($"重置快捷键时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void BtnSave_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // 保存快捷键配置
+ _hotkeyManager.SaveHotkeysToSettings();
+
+ MessageBox.Show("快捷键设置已保存。", "保存成功", MessageBoxButton.OK, MessageBoxImage.Information);
+ Close();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"保存快捷键设置时出错: {ex.Message}", LogHelper.LogType.Error);
+ MessageBox.Show($"保存快捷键设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ #endregion
+ }
+
+ #region Hotkey Changed Event Args
+ ///
+ /// 快捷键变更事件参数
+ ///
+ public class HotkeyChangedEventArgs : EventArgs
+ {
+ public string HotkeyName { get; set; }
+ public Key Key { get; set; }
+ public ModifierKeys Modifiers { get; set; }
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/Ink Canvas/Windows/RandWindow.xaml b/Ink Canvas/Windows/RandWindow.xaml
index 00ef6d31..f1bada9c 100644
--- a/Ink Canvas/Windows/RandWindow.xaml
+++ b/Ink Canvas/Windows/RandWindow.xaml
@@ -103,14 +103,22 @@
-
-
-
-
-
-
-
-
+
+
+ ClassIsland点名
+ SecRandom点名
+ NamePicker点名
+
+
+
+
+
+
+
+
+
+
diff --git a/Ink Canvas/Windows/RandWindow.xaml.cs b/Ink Canvas/Windows/RandWindow.xaml.cs
index 13cfd923..4b4c5f5e 100644
--- a/Ink Canvas/Windows/RandWindow.xaml.cs
+++ b/Ink Canvas/Windows/RandWindow.xaml.cs
@@ -295,13 +295,13 @@ namespace Ink_Canvas
// 将 isIslandCallerFirstClick 设为静态字段,实现全局记录
private static bool isIslandCallerFirstClick = true;
- private void BorderBtnIslandCaller_MouseUp(object sender, MouseButtonEventArgs e)
+ private void BorderBtnExternalCaller_MouseUp(object sender, MouseButtonEventArgs e)
{
if (isIslandCallerFirstClick)
{
MessageBox.Show(
- "首次使用ClassIsland点名功能,请确保已安装ClassIsland和Island caller插件。\n" +
- "如未安装,请前往官网下载并安装后再使用。如果安装请再次点击此按钮。",
+ "首次使用外部点名功能,请确保已安装相应的点名软件。\n" +
+ "如未安装,请前往官网下载并安装后再使用。如果已安装请再次点击此按钮。",
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
isIslandCallerFirstClick = false;
return;
@@ -309,9 +309,26 @@ namespace Ink_Canvas
try
{
+ string protocol = "";
+ switch (ComboBoxCallerType.SelectedIndex)
+ {
+ case 0: // ClassIsland点名
+ protocol = "classisland://plugins/IslandCaller/Simple/1";
+ break;
+ case 1: // SecRandom点名
+ protocol = "secrandom://pumping?action=start";
+ break;
+ case 2: // NamePicker点名
+ protocol = "namepicker://";
+ break;
+ default:
+ protocol = "classisland://plugins/IslandCaller/Simple/1";
+ break;
+ }
+
Process.Start(new ProcessStartInfo
{
- FileName = "classisland://plugins/IslandCaller/Run",
+ FileName = protocol,
UseShellExecute = true
});
}
diff --git a/README.md b/README.md
index 5887da59..3e09af86 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,7 @@
 CJK_mkp 🚧 📖 💻 |
+  Hydrogen 💻 |
 CreeperAWA 💻 |
 2,2,3-三甲基戊烷 📝 📖 🎨 |
 Alan-CRL 💻 🚇 📖 💵 |
@@ -92,7 +93,7 @@
 Awesome Iwb 📖 |
-  PrefacedCorg 💻 |
+  PrefacedCorg 💻 🎨 |