Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 70 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,25 +62,20 @@
- 高風險快速鍵與返回行為維持可用。
- 若執行於 Wine/Proton,開啟螢幕鍵盤時會優先嘗試使用 **Steam 虛擬鍵盤**。
- **Steam Deck 遊戲模式(Gamescope)**:
- 會套用單一視窗與焦點安全限制,避免切換視窗、彈出選單或視窗狀態變更造成黑畫面、失焦或算繪表面異常
- 以無邊框全螢幕模式運行,由 Gamescope 合成器接管視窗佈局與焦點管理;視窗還原(`SW_RESTORE`)、智慧邊界定位等 Windows 視窗行為會自動略過,避免干擾合成器的全螢幕表面
- 開啟螢幕鍵盤時會優先使用 **Steam 虛擬鍵盤**。
- 下列功能目前會停用或受限制:
- 返回前一個視窗(包含複製成功後自動返回)。
- 視窗透明度調整與重設。
- 隱私模式快捷切換。
- 右鍵選單與說明對話框。
- 重新朗讀全文(R3)。
- `LT`/`RT` 的控制器快捷功能,避免與 Steam 虛擬鍵盤的保留按鍵衝突。
- 部分功能因 Gamescope 環境限制,行為會與 Windows 桌面有所不同:
- **視窗透明度**:Gamescope 合成器不支援 Win32 層級視窗透明度,調整不透明度無效。
- **音效提示**:Gamescope 音訊沙盒限制,應用程式層級音效無法播放。
- **A11y 廣播功能**:Gamescope 與 Wine/Proton 環境中無可用的螢幕閱讀器,廣播無作用。
- **`LT`/`RT` 的控制器快捷功能**:仍可能與 Steam 虛擬鍵盤的保留按鍵衝突。
- 若 Gamescope 畫面變黑或應用程式失去焦點,可使用下列方式嘗試恢復;請僅在確有需要時使用,頻繁或不必要地觸發可能造成副作用,若恢復後行為仍不正常,建議直接重新啟動應用程式:
- **控制器救援組合鍵**:按住 **LB + RB**,再按下**上側鍵(Y/△/X)**,觸發視窗算繪表面重建。
- **右鍵選單**:透過觸控螢幕或觸控板開啟右鍵選單,選擇「**恢復 Gamescope 畫面**」。
- **注意事項**:
- 本專案目前的 Linux 相容性調整,重點在於 **Steam Deck/SteamOS 3 + Wine/Proton** 的實際使用情境。
- 即使在桌面模式下可保留完整功能,螢幕鍵盤、輸入法候選窗與焦點恢復行為仍可能受桌面環境設定影響。
- 若應用程式在 Steam Deck 上無法正常啟動、顯示異常或行為不穩定,可嘗試改用較新的 **Proton** 版本,或切換至 **Proton Experimental** 再重新測試。
- 若要在 SteamOS 3 的 Steam 啟動選項中明確指定正體中文語系,可加入:
```shell
HOST_LC_ALL=zh_TW.UTF-8 %command%
```
- 此設定可讓 Wine/Proton 優先繼承 `zh_TW.UTF-8` 語系,改善介面語系、資源挑選與部分在地化行為的穩定性。
- **前提**:SteamOS 本機需先生成對應的 `zh_TW.UTF-8` 語系;若系統尚未提供該 locale,則這個啟動參數不會生效,Wine/Proton 仍可能退回至預設語系。
- 若應用程式在遊戲模式下出現難以解釋的異常行為,建議直接重新啟動應用程式,而非持續嘗試各種恢復操作。

## 三、使用方式 📖

Expand Down Expand Up @@ -131,7 +126,7 @@

本應用程式支援動態 **控制器主按鍵配置模式**。畫面上的按鍵提示、助記詞與實際功能會隨目前設定同步切換,避免 Xbox、PlayStation 與 Nintendo 控制器在顯示與操作上不一致。

- **自動**:在使用 **GameInput** 且成功辨識裝置時,會自動選用最合適的配置。PlayStation 預設採 **PlayStation(× 確認)**,Nintendo 採 Nintendo 配置,其餘則以 Xbox 配置為準。
- **自動**:在使用 **GameInput** 且成功辨識裝置時,會自動選用最合適的配置。PlayStation 預設採 **PlayStation(× 確認)**,Nintendo 採 Nintendo 配置,其餘則以 Xbox 配置為準。使用 **XInput** 時,無法辨識裝置型別,自動模式一律套用 **Xbox** 配置。
- **手動指定優先**:若您在選單中手動指定 Xbox、PlayStation 或 Nintendo 配置,會優先於自動判斷。
- **按鍵名稱對照**:本文中的 **Start 鍵** 與 **Back 鍵**,在部分新式控制器上會分別標示為 **Menu** 與 **View**;實際功能以本文說明為準。

Expand Down Expand Up @@ -253,7 +248,7 @@
- 輕推搖桿至藍點剛好碰到實線環(Enter)時,狀態列數字即為此時的死區觸發點;可用來確認死區大小是否符合需求。
- 若藍點在靜置時已超出虛線環(Exit),代表校正後的偏移仍超出離開閾值,需提高 `ThumbDeadzoneEnter` 或重設校正狀態。
- **操作**:按「重設目前校正狀態」後,偏移量歸零,兩點立即重合;靜置幾秒後系統會重新學習漂移並自動補償。狀態列下方的數字格式為「死區進入/離開 Enter/Exit」,即目前設定的閾值。
- **控制器主按鍵配置** 🎯:可在 **自動**、**Xbox**、**PlayStation(○ 確認)**、**PlayStation(× 確認)** 與 **Nintendo** 之間切換。若選擇 **自動** 並使用 **GameInput**,系統會依裝置型別自動判斷最適合的配置;若手動指定,則以手動選擇為準。
- **控制器主按鍵配置** 🎯:可在 **自動**、**Xbox**、**PlayStation(○ 確認)**、**PlayStation(× 確認)** 與 **Nintendo** 之間切換。若選擇 **自動** 並使用 **GameInput**,系統會依裝置型別自動判斷最適合的配置;使用 **XInput** 時,自動模式無法辨識裝置型別,一律套用 **Xbox** 配置。若手動指定,則以手動選擇為準。
- **重新啟動提示** 🔄:若您變更了需在下次啟動後才完整套用的項目(例如系統主題/高對比狀態、遊戲控制器輸入 API 或歷程容量),標題列與右鍵選單會同步顯示提示,並提供「立即重新啟動程式」快捷項目。
- **開啟資料夾** 📂:快速開啟位於 `%AppData%\InputBox` 的應用程式資料儲存路徑,方便手動備份或修改設定檔。
- **開啟日誌資料夾** 📋:快速開啟位於 `%LocalAppData%\InputBox\Logs` 的日誌檔案存放路徑,方便查閱或回報錯誤記錄。若日誌資料夾尚未建立(即程式從未發生例外),會以警告對話框與螢幕報讀雙重方式提示資料夾不存在。
Expand Down Expand Up @@ -307,7 +302,7 @@
| 參數名稱 | 類型 | 預設值 | 有效範圍 | 說明 |
| :--- | :--- | :--- | :--- | :--- |
| `GamepadProviderType` | 字串 | `"XInput"` | - | 選擇遊戲控制器輸入 API:`"XInput"` 或 `"GameInput"`。若 GameInput 初始化失敗將自動退避至 XInput。**備註**:使用 **GameInput** 時,遊戲控制器可能需在實際產生輸入(如按鍵或搖桿操作)後,才會開始回報狀態。初次連線時若尚未有反應,請稍候或操作遊戲控制器一次即可。 |
| `GamepadFaceButtonModeType` | 字串 | `"Auto"` | `"Auto"`<br>`"Xbox"`<br>`"PlayStationTraditional"`<br>`"PlayStationCrossConfirm"`<br>`"Nintendo"` | 選擇控制器主按鍵配置模式。`"Auto"` 會在可辨識裝置型別時自動套用對應的顯示與功能對照;偵測到 PlayStation 時預設優先使用 **PlayStation(× 確認)**,Nintendo 則採 Nintendo 配置若手動指定模式,會優先於自動判斷。 |
| `GamepadFaceButtonModeType` | 字串 | `"Auto"` | `"Auto"`<br>`"Xbox"`<br>`"PlayStationTraditional"`<br>`"PlayStationCrossConfirm"`<br>`"Nintendo"` | 選擇控制器主按鍵配置模式。`"Auto"` 會在可辨識裝置型別時自動套用對應的顯示與功能對照;偵測到 PlayStation 時預設優先使用 **PlayStation(× 確認)**,Nintendo 則採 Nintendo 配置。使用 **XInput** 時,自動模式無法辨識裝置型別,一律套用 **Xbox** 配置。若手動指定模式,會優先於自動判斷。 |
| `ThumbDeadzoneEnter` | 整數 | `7849` | `0 ~ 30000` | XInput 標準值(0~32767),無單位。搖桿推動觸發閾值。若控制器搖桿因磨損而產生偏移,可提高此值。 |
| `ThumbDeadzoneExit` | 整數 | `2500` | `0 ~ 30000` | XInput 標準值(0~32767),無單位。搖桿回彈重置閾值。此值必須顯著低於觸發閾值以防止抖動。 |
| `RepeatInitialDelayFrames` | 整數(幀) | `30` | `1 ~ 300` | 長按方向鍵時,開始重複輸入前的延遲(幀)。1 幀約為 16.6ms(60 FPS)。 |
Expand Down Expand Up @@ -416,16 +411,67 @@ Windows 設定 → 時間與語言 → 輸入 → 觸控式鍵盤 → 「顯示
- 將 **Armory Crate SE** 的控制模式手動改為「桌面」。
- 直接使用觸控螢幕進行拖曳操作。

### 5. 在 Steam Deck 遊戲模式下切換應用程式後,返回本應用程式時輸入框未取得焦點,控制器按鈕無反應 🎮
### 5. 在 Steam Deck 遊戲模式下,應用程式失去焦點,控制器功能無回應 🎮

這是遊戲模式(Gamescope)下的視窗焦點限制,屬於預期行為。
在下列情況下,遊戲模式(Gamescope)可能導致應用程式失去焦點,造成遊戲控制器功能無作用:

**解決方法** ✅:
- **偶發**:複製文字到剪貼簿後。
- **切換**:在多個執行中的 Gamescope 之間切換,再切回本應用程式時。

**解決方法** ✅(任選其一):

- **觸控螢幕**:用手指直接點擊輸入框,讓應用程式取得焦點。
- **觸控板**:以觸控板搭配 **Steam 鍵 + R2(RT)** 觸發滑鼠左鍵點擊,點擊輸入框以恢復焦點。
- **控制器救援組合鍵**:按住 **LB + RB**,再按下**上側鍵(Y/△/X)**,強制重建視窗算繪表面,恢復焦點與控制器操作。
- **右鍵選單**:透過觸控螢幕或觸控板開啟右鍵選單,選擇「**恢復 Gamescope 畫面**」。

> ⚠️ **注意**:觸控板搭配 Steam 鍵 + R2(RT)的操作,需先在 Steam 的**控制器設定**中,將觸控板的對應動作設為「滑鼠」模式,並確認相關組合鍵已啟用。

### 6. 長時間使用或 Steam Deck 休眠後喚醒,Steam 虛擬鍵盤無法叫出 ⌨️

在 Steam Deck 遊戲模式下,長時間維持應用程式開啟,或 Steam Deck 休眠後重新喚醒,Steam 虛擬鍵盤可能因內部狀態失效而無法正常呼叫。

**解決方法** ✅:重新啟動應用程式。

### 7. 在 Steam Deck 遊戲模式下,透明度調整無效 🌓

這是預期行為。Gamescope 合成器不支援 Win32 層級視窗透明度(`WS_EX_LAYERED`),因此在遊戲模式下調整不透明度不會有任何視覺效果。

### 8. 在 Steam Deck 遊戲模式下,音效提示無聲 🔊

這是預期行為。Gamescope 的音訊沙盒環境限制了應用程式層級音效的播放,音效提示在遊戲模式下無法作用。

### 9. A11y 廣播功能在 Steam Deck、Wine/Proton 與 Gamescope 下無作用 ♿

這是預期行為。Steam Deck 的 Gamescope 與 Wine/Proton 環境中沒有可供應用程式使用的螢幕閱讀器或無障礙廣播接收端,A11y 廣播功能無法產生效果。

### 10. 在 Steam Deck 上應用程式無法正常啟動、顯示異常或行為不穩定 🐧

可嘗試在 Steam 的遊戲屬性中,改用較新的 **Proton** 版本,或切換至 **Proton Experimental** 後重新啟動。

### 11. 在 SteamOS 3 遊戲模式下,介面語系顯示不正確 🌐

可在 Steam 的啟動選項中加入下列設定,讓 Wine/Proton 優先繼承指定語系:

```shell
HOST_LC_ALL=<locale> %command%
```

將 `<locale>` 替換為對應的語系識別碼。以下列出本應用程式支援語系的常見對應值:

| 語系 | `<locale>` 值 |
| :--- | :--- |
| 德文 | `de_DE.UTF-8` |
| 英文 | `en_US.UTF-8` |
| 法文 | `fr_FR.UTF-8` |
| 日文 | `ja_JP.UTF-8` |
| 韓文 | `ko_KR.UTF-8` |
| 簡體中文 | `zh_CN.UTF-8` |
| 正體中文 | `zh_TW.UTF-8` |

- **觸控**:用手指直接點擊輸入框一下,即可讓輸入框取得焦點。
- **觸控板**:以觸控板搭配 **Steam 鍵 + 滑鼠右鍵(R1)**,將游標移至輸入框後按下確認,即可恢復控制器操作。
此設定可讓 Wine/Proton 優先繼承指定語系,改善介面語系、資源挑選與部分在地化行為的穩定性。

> ⚠️ **注意**:使用觸控板搭配 Steam 鍵 + 滑鼠右鍵(R1)的操作方式,需先在 Steam 的**控制器設定**中,將觸控板的對應動作配置為「滑鼠」模式,並確認相關組合鍵已啟用,才能正常使用
> **前提**:SteamOS 本機需先生成對應的 locale。若系統尚未提供,此啟動參數不會生效,Wine/Proton 仍可能退回預設語系。可在 SteamOS 的桌面模式終端機以 `locale -a` 確認目前已生成的 locale 清單

## 六、應用程式的設計原則 🛡️

Expand Down
16 changes: 15 additions & 1 deletion src/InputBox/Core/Controls/GamepadCalibrationDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ private void SubscribeGamepadEvents()
_gamepadController.StartPressed += HandleGamepadConfirm;
_gamepadController.BPressed += profile.ConfirmOnSouth ? HandleGamepadCancel : HandleGamepadConfirm;
_gamepadController.BackPressed += HandleGamepadCancel;
_gamepadController.YPressed += HandleGamescopeSurfaceRecovery;
_gamepadController.LeftPressed += HandleDPadPrevious;
_gamepadController.LeftRepeat += HandleDPadPrevious;
_gamepadController.UpPressed += HandleDPadPrevious;
Expand Down Expand Up @@ -540,6 +541,7 @@ private void UnsubscribeGamepadEvents()
_gamepadController.BPressed -= HandleGamepadConfirm;
_gamepadController.BPressed -= HandleGamepadCancel;
_gamepadController.BackPressed -= HandleGamepadCancel;
_gamepadController.YPressed -= HandleGamescopeSurfaceRecovery;
_gamepadController.LeftPressed -= HandleDPadPrevious;
_gamepadController.LeftRepeat -= HandleDPadPrevious;
_gamepadController.UpPressed -= HandleDPadPrevious;
Expand Down Expand Up @@ -654,6 +656,18 @@ private void HandleGamepadCancel()
}
}

/// <summary>
/// 處理 Gamescope 專用 surface recovery 組合鍵。
/// </summary>
private void HandleGamescopeSurfaceRecovery()
{
GamescopeSurfaceRecovery.TryRecoverFromGamepadChord(
this,
RecreateHandle,
_gamepadController,
context: "GamepadCalibrationDialog Gamescope surface recovery 失敗");
}

/// <summary>
/// 處理 D-Pad 向前(左/上)輸入,在搖桿不干擾時移動焦點至前一個按鈕。
/// </summary>
Expand Down Expand Up @@ -1201,4 +1215,4 @@ protected override void Dispose(bool disposing)

base.Dispose(disposing);
}
}
}
41 changes: 15 additions & 26 deletions src/InputBox/Core/Controls/GamepadMessageBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ private void SubscribeGamepadEvents()
_gamepadController.StartPressed += HandleGamepadA;
_gamepadController.BPressed += profile.ConfirmOnSouth ? HandleGamepadB : HandleGamepadA;
_gamepadController.BackPressed += HandleGamepadB;
_gamepadController.YPressed += HandleGamescopeSurfaceRecovery;
_gamepadController.LeftPressed += HandleDPadLeft;
_gamepadController.LeftRepeat += HandleDPadLeft;
_gamepadController.RightPressed += HandleDPadRight;
Expand All @@ -110,6 +111,7 @@ private void UnsubscribeGamepadEvents()
_gamepadController.BPressed -= HandleGamepadA;
_gamepadController.BPressed -= HandleGamepadB;
_gamepadController.BackPressed -= HandleGamepadB;
_gamepadController.YPressed -= HandleGamescopeSurfaceRecovery;
_gamepadController.LeftPressed -= HandleDPadLeft;
_gamepadController.LeftRepeat -= HandleDPadLeft;
_gamepadController.RightPressed -= HandleDPadRight;
Expand Down Expand Up @@ -994,6 +996,18 @@ private void HandleGamepadB()
});
}

/// <summary>
/// 處理 Gamescope 專用 surface recovery 組合鍵。
/// </summary>
private void HandleGamescopeSurfaceRecovery()
{
GamescopeSurfaceRecovery.TryRecoverFromGamepadChord(
this,
RecreateHandle,
_gamepadController,
context: "GamepadMessageBox Gamescope surface recovery 失敗");
}

/// <summary>
/// 處理遊戲控制器 DPad 左鍵事件,將焦點移動到上一個可用按鈕,並播報新焦點按鈕名稱給螢幕閱讀器
/// </summary>
Expand Down Expand Up @@ -1119,31 +1133,6 @@ public static DialogResult Show(
MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1,
IGamepadController? gamepad = null)
{
// Gamescope (遊戲模式) 防護:
// 攔截所有會建立新視窗表面的 ShowDialog 呼叫,改為 Log 紀錄並回傳預設值。
// 這能防止 Gamescope 合成器因視窗切換導致的主介面渲染鏈中斷與黑屏。
if (SystemHelper.IsRunningOnGamescope())
{
LoggerService.LogInfo($"[Gamescope Intercept] {caption}: {text} (Buttons: {buttons})");

if (gamepad != null)
{
FeedbackService.VibrateAsync(gamepad, VibrationPatterns.ActionFail, CancellationToken.None).SafeFireAndForget();
}

// 回傳安全預設值:單一按鈕回傳 OK,確認型按鈕回傳 No 或 Cancel。
return buttons switch
{
MessageBoxButtons.OK => DialogResult.OK,
MessageBoxButtons.OKCancel => DialogResult.Cancel,
MessageBoxButtons.YesNo => DialogResult.No,
MessageBoxButtons.YesNoCancel => DialogResult.Cancel,
MessageBoxButtons.RetryCancel => DialogResult.Cancel,
MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore,
_ => DialogResult.OK,
};
}

using GamepadMessageBox dlg = new(text, caption, buttons, icon, defaultButton);

dlg.GamepadController = gamepad;
Expand Down Expand Up @@ -1186,4 +1175,4 @@ public static DialogResult Show(
MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1,
IGamepadController? gamepad = null)
=> Show(null, text, caption, buttons, icon, defaultButton, gamepad);
}
}
16 changes: 15 additions & 1 deletion src/InputBox/Core/Controls/HelpDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ private void BindGamepadEvents()
GamepadController.BackPressed += OnGamepadClose;
GamepadController.StartPressed += OnGamepadClose;
GamepadController.APressed += OnGamepadClose;
GamepadController.YPressed += OnGamepadSurfaceRecovery;
GamepadController.LeftTriggerPressed += OnGamepadPageUp;
GamepadController.RightTriggerPressed += OnGamepadPageDown;
GamepadController.LeftTriggerRepeat += OnGamepadPageUp;
Expand All @@ -919,6 +920,7 @@ private void UnbindGamepadEvents()
GamepadController.BackPressed -= OnGamepadClose;
GamepadController.StartPressed -= OnGamepadClose;
GamepadController.APressed -= OnGamepadClose;
GamepadController.YPressed -= OnGamepadSurfaceRecovery;
GamepadController.LeftTriggerPressed -= OnGamepadPageUp;
GamepadController.RightTriggerPressed -= OnGamepadPageDown;
GamepadController.LeftTriggerRepeat -= OnGamepadPageUp;
Expand Down Expand Up @@ -1038,6 +1040,18 @@ private void OnGamepadClose()
});
}

/// <summary>
/// 處理 Gamescope 專用 surface recovery 組合鍵。
/// </summary>
private void OnGamepadSurfaceRecovery()
{
GamescopeSurfaceRecovery.TryRecoverFromGamepadChord(
this,
RecreateHandle,
GamepadController,
context: "HelpDialog Gamescope surface recovery 失敗");
}

#endregion

#region 關閉按鈕視覺特效
Expand Down Expand Up @@ -1076,4 +1090,4 @@ private void UpdateButtonMinimumSize()
}

#endregion
}
}
Loading
Loading