diff --git a/Ink Canvas/Docs/ExternalProtocol.md b/Ink Canvas/Docs/ExternalProtocol.md
new file mode 100644
index 00000000..d68569b7
--- /dev/null
+++ b/Ink Canvas/Docs/ExternalProtocol.md
@@ -0,0 +1,77 @@
+# Ink Canvas 外部协议 (URI Scheme) 说明文档
+
+Ink Canvas 支持通过自定义协议 `icc://` 进行外部调用。通过此功能,其他应用程序、网页脚本或系统快捷方式可以远程控制 Ink Canvas 的运行状态。
+
+## 启用方法
+
+在使用外部协议之前,必须先在软件设置中启用:
+1. 打开 **软件设置**。
+2. 进入 **高级选项** 面板。
+3. 找到 **外部协议调用** 区域。
+4. 开启 **“启用外部协议 (icc://)”** 开关。
+
+> **注意**:此操作会自动在系统注册表中为当前用户注册协议。如果手动关闭该功能,协议将被注销。
+
+---
+
+## 命令列表
+
+### 1. 基础控制命令
+
+| 命令 | 完整 URI | 作用 |
+| :--- | :--- | :--- |
+| **Fold** | `icc://fold` | 进入**收纳模式**。如果当前处于展开状态,将清空墨迹并折叠到侧边栏。 |
+| **Unfold** | `icc://unfold` | 退出**收纳模式**。如果当前已折叠,将展开浮动工具栏。 |
+| **Toggle** | `icc://toggle` | **切换**状态。已展开则收起,已收起则展开。 |
+| **Show** | `icc://show` | 与 `unfold` 作用相同,用于兼容旧版指令。 |
+
+### 2. 侧边栏工具命令
+
+以下命令对应收纳模式下侧边栏提供的快速工具。
+
+| 命令 | 完整 URI | 作用 |
+| :--- | :--- | :--- |
+| **单次抽** | `icc://randone` | 打开随机点名窗口并执行**单次抽选**。 |
+| **随机抽** | `icc://rand` | 打开随机点名窗口并执行**随机抽选**。 |
+| **计时器** | `icc://timer` | 打开**计时器/倒计时**工具。 |
+| **白板** | `icc://whiteboard` | 切换到**白板模式**(也可使用 `icc://board`)。 |
+
+### 3. 进阶功能命令 (隐藏功能)
+
+以下功能专门用于解决与第三方侧边栏或悬浮窗程序的兼容性问题,未在常规设置界面显示。
+
+| 命令 | 完整 URI | 作用 |
+| :--- | :--- | :--- |
+| **ThoroughHideOn** | `icc://thoroughHideOn` | **开启**“收起时彻底隐藏”功能。开启后,进入收纳模式时主窗口将完全不可见。 |
+| **ThoroughHideOff** | `icc://thoroughHideOff` | **关闭**“收起时彻底隐藏”功能。恢复默认的侧边栏边缘留痕模式。 |
+| **ThoroughHideToggle** | `icc://thoroughhidetoggle` | **切换**“收起时彻底隐藏”功能的开启/关闭状态。 |
+
+---
+
+## 使用示例
+
+### A. 在浏览器中调用
+可以直接在浏览器地址栏输入并回车,或在 HTML 中使用超链接:
+```html
+立即收纳 Ink Canvas
+```
+
+### B. 在 Windows “运行”对话框中使用
+按下 `Win + R`,输入 `icc://toggle` 并回车。
+
+### C. 在批处理或命令行中使用
+```cmd
+start icc://unfold
+```
+
+---
+
+## 开发者说明
+
+### 运行机制
+1. **唤醒启动**:如果 Ink Canvas 尚未运行,调用 URI 会直接启动程序并执行命令。
+2. **进程间通信 (IPC)**:如果程序已经在运行,外部调用会启动一个临时的指令传递进程,通过系统事件和临时文件将指令发送给已运行的实例,实现无缝控制。
+
+### 兼容性
+* 支持 Windows 7 及更高版本。
+* 注册表位置:`HKEY_CURRENT_USER\Software\Classes\icc` (无需管理员权限)。
diff --git a/Ink Canvas/Helpers/UriSchemeHelper.cs b/Ink Canvas/Helpers/UriSchemeHelper.cs
index 70a31b25..1e957402 100644
--- a/Ink Canvas/Helpers/UriSchemeHelper.cs
+++ b/Ink Canvas/Helpers/UriSchemeHelper.cs
@@ -75,8 +75,38 @@ namespace Ink_Canvas.Helpers
{
if (shellKey == null) return false;
string command = shellKey.GetValue("") as string;
- string exePath = Process.GetCurrentProcess().MainModule.FileName;
- return !string.IsNullOrEmpty(command) && command.Contains(exePath);
+ if (string.IsNullOrEmpty(command)) return false;
+
+ // 提取第一个标记作为可执行文件路径(处理带引号的情况)
+ string registeredExePath = "";
+ if (command.StartsWith("\""))
+ {
+ int nextQuote = command.IndexOf("\"", 1);
+ if (nextQuote > 1)
+ {
+ registeredExePath = command.Substring(1, nextQuote - 1);
+ }
+ }
+ else
+ {
+ int firstSpace = command.IndexOf(" ");
+ registeredExePath = firstSpace > 0 ? command.Substring(0, firstSpace) : command;
+ }
+
+ if (string.IsNullOrEmpty(registeredExePath)) return false;
+
+ string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
+
+ try
+ {
+ string normalizedRegisteredPath = System.IO.Path.GetFullPath(registeredExePath);
+ string normalizedCurrentPath = System.IO.Path.GetFullPath(currentExePath);
+ return string.Equals(normalizedRegisteredPath, normalizedCurrentPath, StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return string.Equals(registeredExePath, currentExePath, StringComparison.OrdinalIgnoreCase);
+ }
}
}
}
diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml
index 2ad47907..95032d06 100644
--- a/Ink Canvas/MainWindow.xaml
+++ b/Ink Canvas/MainWindow.xaml
@@ -2417,8 +2417,10 @@
-
diff --git a/Ink Canvas/MainWindow_cs/MW_AutoFold.cs b/Ink Canvas/MainWindow_cs/MW_AutoFold.cs
index 8c3298b7..4b2afe4a 100644
--- a/Ink Canvas/MainWindow_cs/MW_AutoFold.cs
+++ b/Ink Canvas/MainWindow_cs/MW_AutoFold.cs
@@ -105,6 +105,15 @@ namespace Ink_Canvas
HideSubPanels("cursor");
SidePannelMarginAnimation(-10);
});
+
+ // 新增:如果开启了彻底隐藏,则隐藏主窗口
+ if (Settings.Automation.ThoroughlyHideWhenFolded)
+ {
+ await Dispatcher.InvokeAsync(() =>
+ {
+ this.Visibility = Visibility.Hidden;
+ });
+ }
}
private async void LeftUnFoldButtonDisplayQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
@@ -230,6 +239,15 @@ namespace Ink_Canvas
public async Task UnFoldFloatingBar(object sender)
{
+ // 新增:如果之前彻底隐藏了,先恢复显示
+ if (this.Visibility != Visibility.Visible)
+ {
+ await Dispatcher.InvokeAsync(() =>
+ {
+ this.Visibility = Visibility.Visible;
+ });
+ }
+
await Dispatcher.InvokeAsync(() =>
{
LeftUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs
index a9bbc7f0..c696af39 100644
--- a/Ink Canvas/MainWindow_cs/MW_Settings.cs
+++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs
@@ -2856,24 +2856,55 @@ namespace Ink_Canvas
private void ToggleSwitchIsEnableUriScheme_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
- Settings.Advanced.IsEnableUriScheme = ToggleSwitchIsEnableUriScheme.IsOn;
- if (Settings.Advanced.IsEnableUriScheme)
+ bool newState = ToggleSwitchIsEnableUriScheme.IsOn;
+ bool success = false;
+
+ try
{
- if (!UriSchemeHelper.IsUriSchemeRegistered())
+ if (newState)
{
- UriSchemeHelper.RegisterUriScheme();
+ if (!UriSchemeHelper.IsUriSchemeRegistered())
+ {
+ success = UriSchemeHelper.RegisterUriScheme();
+ }
+ else
+ {
+ success = true;
+ }
}
+ else
+ {
+ if (UriSchemeHelper.IsUriSchemeRegistered())
+ {
+ success = UriSchemeHelper.UnregisterUriScheme();
+ }
+ else
+ {
+ success = true;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"切换URI Scheme状态失败: {ex.Message}", LogHelper.LogType.Error);
+ success = false;
+ }
+
+ if (success)
+ {
+ Settings.Advanced.IsEnableUriScheme = newState;
+ SaveSettingsToFile();
}
else
{
- if (UriSchemeHelper.IsUriSchemeRegistered())
- {
- UriSchemeHelper.UnregisterUriScheme();
- }
- }
+ // 回滚 UI 状态
+ isLoaded = false;
+ ToggleSwitchIsEnableUriScheme.IsOn = !newState;
+ isLoaded = true;
- SaveSettingsToFile();
+ ShowNotification("设置外部协议失败,请检查权限或日志");
+ }
}
private void TouchMultiplierSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
diff --git a/Ink Canvas/MainWindow_cs/MW_UriHandler.cs b/Ink Canvas/MainWindow_cs/MW_UriHandler.cs
index 0b124ed9..59013ddd 100644
--- a/Ink Canvas/MainWindow_cs/MW_UriHandler.cs
+++ b/Ink Canvas/MainWindow_cs/MW_UriHandler.cs
@@ -12,18 +12,32 @@ namespace Ink_Canvas
{
if (string.IsNullOrEmpty(uri)) return;
+ // 检查是否启用了外部协议
+ if (!Settings.Advanced.IsEnableUriScheme)
+ {
+ LogHelper.WriteLogToFile($"URI协议已禁用,忽略请求: {uri}", LogHelper.LogType.Warning);
+ return;
+ }
+
LogHelper.WriteLogToFile($"正在处理URI命令: {uri}", LogHelper.LogType.Event);
// 解析URI
// 格式: icc://command?param=value
// 如果URI以icc:开头但不是标准URI格式,尝试手动解析
string command = "";
-
+
if (Uri.TryCreate(uri, UriKind.Absolute, out Uri uriObj))
{
command = uriObj.Host.ToLower();
+ // 处理像 icc:fold 这样 Host 可能为空的情况
+ if (string.IsNullOrEmpty(command))
+ {
+ command = uriObj.AbsolutePath.Trim('/').ToLower();
+ }
}
- else if (uri.StartsWith("icc:", StringComparison.OrdinalIgnoreCase))
+
+ // 如果解析失败且是 icc: 协议,则手动处理
+ if (string.IsNullOrEmpty(command) && uri.StartsWith("icc:", StringComparison.OrdinalIgnoreCase))
{
// 简单的手动解析: icc:fold
string path = uri.Substring(4);
@@ -63,6 +77,52 @@ namespace Ink_Canvas
}
break;
+ case "thoroughhideon":
+ Settings.Automation.ThoroughlyHideWhenFolded = true;
+ SaveSettingsToFile();
+ ShowNotification("已开启:收起时彻底隐藏");
+ // 如果当前已经是在收纳模式,立即隐藏
+ if (isFloatingBarFolded)
+ {
+ this.Visibility = Visibility.Hidden;
+ }
+ break;
+
+ case "thoroughhideoff":
+ Settings.Automation.ThoroughlyHideWhenFolded = false;
+ SaveSettingsToFile();
+ ShowNotification("已关闭:收起时彻底隐藏");
+ // 确保窗口可见
+ this.Visibility = Visibility.Visible;
+ break;
+
+ case "thoroughhidetoggle":
+ Settings.Automation.ThoroughlyHideWhenFolded = !Settings.Automation.ThoroughlyHideWhenFolded;
+ SaveSettingsToFile();
+ ShowNotification(Settings.Automation.ThoroughlyHideWhenFolded ? "已开启:收起时彻底隐藏" : "已关闭:收起时彻底隐藏");
+ if (isFloatingBarFolded)
+ {
+ this.Visibility = Settings.Automation.ThoroughlyHideWhenFolded ? Visibility.Hidden : Visibility.Visible;
+ }
+ break;
+
+ case "randone":
+ SymbolIconRandOne_MouseUp(null, null);
+ break;
+
+ case "rand":
+ SymbolIconRand_MouseUp(null, null);
+ break;
+
+ case "timer":
+ ImageCountdownTimer_MouseUp(null, null);
+ break;
+
+ case "whiteboard":
+ case "board":
+ ImageBlackboard_MouseUp(null, null);
+ break;
+
default:
LogHelper.WriteLogToFile($"未知的URI命令: {command}", LogHelper.LogType.Warning);
break;
diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs
index bb3dac6d..2fbedc70 100644
--- a/Ink Canvas/Resources/Settings.cs
+++ b/Ink Canvas/Resources/Settings.cs
@@ -496,6 +496,9 @@ namespace Ink_Canvas
[JsonProperty("autoSaveStrokesIntervalMinutes")]
public int AutoSaveStrokesIntervalMinutes { get; set; } = 5;
+ [JsonProperty("thoroughlyHideWhenFolded")]
+ public bool ThoroughlyHideWhenFolded { get; set; } = false;
+
[JsonProperty("floatingWindowInterceptor")]
public FloatingWindowInterceptorSettings FloatingWindowInterceptor { get; set; } = new FloatingWindowInterceptorSettings();
}
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml b/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml
index 6b9576c3..aa07890a 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml
+++ b/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml
@@ -417,8 +417,8 @@
-
-
+
+
@@ -430,9 +430,9 @@
-
-
-
+
+
+
@@ -448,7 +448,10 @@
-
+
@@ -658,7 +661,7 @@
-
+
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml.cs b/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml.cs
index dcd6c24b..1fa5d83c 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml.cs
+++ b/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml.cs
@@ -330,6 +330,18 @@ namespace Ink_Canvas.Windows.SettingsViews
}
}
+ ///
+ /// ToggleSwitch键盘事件处理
+ ///
+ private void ToggleSwitch_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == System.Windows.Input.Key.Space || e.Key == System.Windows.Input.Key.Enter)
+ {
+ ToggleSwitch_Click(sender, new RoutedEventArgs());
+ e.Handled = true;
+ }
+ }
+
///
/// Slider值变化事件处理
///
@@ -466,31 +478,39 @@ namespace Ink_Canvas.Windows.SettingsViews
var button = sender as Button;
if (button == null) return;
- string name = button.Name;
+ string action = button.Tag?.ToString();
+ if (string.IsNullOrEmpty(action)) action = button.Name;
+
// 这些按钮的功能可能需要调用 MainWindow 中的方法
// 暂时先留空,后续可以根据需要实现
- switch (name)
+ switch (action)
{
+ case "manual_backup":
case "BtnManualBackup":
// TODO: 调用 MainWindow 的备份方法
break;
+ case "restore_backup":
case "BtnRestoreBackup":
// TODO: 调用 MainWindow 的还原方法
break;
+ case "unregister":
case "BtnUnregisterFileAssociation":
// TODO: 调用 MainWindow 的取消文件关联方法
break;
+ case "check":
case "BtnCheckFileAssociation":
- // TODO: 调用 MainWindow 的检查文件关联方法
+ // TODO: 调用 MainWindow 的检查文件关联状态方法
break;
+ case "register":
case "BtnRegisterFileAssociation":
// TODO: 调用 MainWindow 的注册文件关联方法
break;
+ case "dlass_settings":
case "BtnDlassSettingsManage":
// TODO: 调用 MainWindow 的 Dlass 设置管理方法
break;