# 功能提示系統 — 完整討論紀錄
## 2026-05-01 ~ 2026-05-02

---

## 一、原始需求

用戶要求：做一個鍵盤按鍵一覽表，用 PickList 包起來。
- 點 TopMessage 標題 → 叫出 PickList
- 滑鼠移過去 hover 變色，像一個按鈕
- 跟 SH_F10（公司名稱）一樣的機制

---

## 二、需求討論紀錄（按時間順序）

### 2.1 PickList 內容（三類）

```
第一類：特殊按鍵     PgDn:按鍵  ESC:按鍵（目前只要這兩個）
第二類：基本 HOTKEY   F1:廠商 F2:客戶 F3:零件 F4:適用 F5:車種 F6:品名 F8:品牌 F12:日報
第三類：組合 HOTKEY   ALT+F3:倉庫 ALT+F5:銷前 ALT+F6:進前 ALT+F9:列印
                     ALT+5:最近成本 ALT+6:最近成交 ALT+H:功能說明
                     SH+F10:快選 CTRL+F4:進口
移除項目：F11, ALT+F4, ALT+F7, ALT+F8, CTRL+F5, CTRL+F6
```

### 2.2 為什麼要做這個功能

平板/手機使用者沒有實體鍵盤：
- 沒有 F1~F12
- 不可能按出 ALT+F1 ~ ALT+F12
- 有的連 PgDn 和 ESC 都沒有（這兩個是最常用的）
- PickList 就是他們的虛擬鍵盤，用觸控代替按鍵

### 2.3 第二、三類資料來源

不改 APPSETUP.PRG，在 ShowHotKeys() 裡用 SetKey() 檢查哪些 key 有效：
```
IF SetKey(K_F1) != NIL ; AAdd(aKeys, KY_F1+":廠商") ; AAdd(aKeyCode, K_F1) ; ENDIF
```
跟著 APPSETUP 的條件分支走（SetKey 回傳 NIL 就不加）。
顯示用 CustomChar.ch 的 KY_ 圖形鍵標（KY_ALT, KY_F1, KY_5, KY_6, KY_9 等）。

---

## 三、系統類型分類

```
類型             程式                                           功能提示機制                     row 0
──────────────   ────────────────────────────────────────────   ────────────────────────────     ──────────────────────
單據系統          BUYSELL, QUOT, INQUIRY, DOMCMP,               BottomMessage                    TopMessage
                 DOMPUR, DOMARR, FORCMP, FORPUR,
                 FORARR, ENQUIRY, DOMORD, DOMSCH, STOCK
視窗系統          WINDOW, DBFWIN                                WindowMessage                    繼承上層 TopMessage
瀏覽表單系統      WINFORM, P113                                  WinFormMessage → BottomMessage   TopMessage（P11x 呼叫）
陣列瀏覽系統      ARRAY, BRRAY                                   ARRAY:WindowMessage / BRRAY:無   繼承上層 TopMessage
主選單            MENU                                          無（MenuBar 佔 row 0）            MenuBar（不可有 hotzone）
```

---

## 四、aMsReg 作用域（已驗證）

### 4.1 PRIVATE aMsReg 宣告位置

```
程式             行號        用途
──────────────   ─────────   ──────────────────────
STK.PRG          35          PUBLIC aMsReg（全域初始值）
MENU.PRG         27          PullDownMenu 選單層
MENU.PRG         71, 100     選單層級切換時重設
WINDOW.PRG       40          視窗系統隔離
DBFWIN.PRG       41          視窗系統隔離
WINFORM.PRG      24          瀏覽表單系統隔離
ARRAY.PRG        38          陣列瀏覽隔離
TABLE.PRG        42          表格瀏覽隔離
DIALOG.PRG       36          對話框隔離
TLIB.PRG         53          BottomMenu 隔離
TLIB.PRG         579         MessageBox 隔離
TLIB.PRG         1083        QuitSystem 清除
QUOT.PRG         5944        AskPartsBox 子函式隔離
```

### 4.2 單據系統的 aMsReg 繼承鏈（已驗證）

```
PullDownMenu() [MENU.PRG:27 PRIVATE aMsReg := {}]
  └─ &cProgString("StartUp") [MENU.PRG:191 在迴圈內部呼叫]
       └─ P211("StartUp") [P21.PRG:16 無 PRIVATE aMsReg]
            └─ BuySell() [BUYSELL.PRG:25 無 PRIVATE aMsReg]
                 → 繼承 PullDownMenu 的 PRIVATE aMsReg
```

13 個單據系統全部不宣告 PRIVATE aMsReg，全部繼承 MENU 的。

### 4.3 WINFORM 的 scope 問題

```
PullDownMenu() [PRIVATE aMsReg := {}]  ← scope A
  └─ P111("StartUp")
       ├─ TopMessage("《廠商基本資料》") → 註冊到 scope A
       └─ WinForm() [PRIVATE aMsReg := {}]  ← scope B（遮蔽 A）
            └─ 主迴圈 HoverUpdate/HitTest 查 scope B（空的）
               → 找不到 TopMessage 的熱區
```

---

## 五、功能提示的需求特性

### 5.1 累加與遮蔽

```
場景                             行為                     機制
──────────────────────────────   ───────────────────────  ──────────────────
進入單據                         建立功能提示               BottomMessage 註冊
切換操作狀態（表頭/表身）         更新功能提示               重新呼叫 BottomMessage
進入子視窗（F1/F2/F3/F4）        遮蔽，顯示子視窗的提示      PRIVATE aMsReg 遮蔽
離開子視窗                       自動恢復上層提示           PRIVATE 離開 scope
功能出現順序不固定                SetKey 觸發，隨機組合      stack 行為處理
```

### 5.2 TopMessage hotzone 的需求

- 出現條件：只要 TopMessage 顯示就有熱區，不顯示就沒有
- 天敵：主選單（MenuBar 佔 row 0，不可有 TopMessage hotzone）
- 穿透性：不管進幾層子功能，只要 TopMessage 還在 row 0，hotzone 就要有效
- 資料來源：讀取當前 scope 的功能提示

### 5.3 主選單永遠不呼叫 TopMessage（已驗證）

```
STK.PRG 行 262: PullDownMenu( "0", ..., .F., .T. )
                                        ^^^
                                        lTitle = .F. → 不呼叫 TopMessage
```

遞迴呼叫也是 .F.：MENU.PRG 行 173。
所以「選單時禁止 hotzone」不需要額外處理 — 天生不會發生。

---

## 六、aMouse vs aMsReg — 為什麼有兩個陣列

```
                 aMouse                                    aMsReg
─────────────    ─────────────────────────────────────     ─────────────────────────────────────────────
誰建立           BottomMessage/WindowMessage 的回傳值       MsAddKey 直接註冊
存放位置         PRIVATE 變數（呼叫者存）                    PRIVATE 變數
格式             {row, col, width,                         {row, colS, colE,
                  displayText, color, nKey}                 nKey, flag, procName, sayStr, color, realFunc}
比對方式         nMCol >= col .AND. nMCol < col+width       nMCol >= colS .AND. nMCol <= colE
Hover 處理       HoverCheck(aMouse, nHover) 傳入參數        HoverUpdate() 直接讀 aMsReg
Click 處理       MsClick(aMouse, nHover) 回傳 nKey          HitTest() 回傳整個 aItem
誰的資料         功能提示（按鍵說明）                         所有滑鼠熱區（任何位置）
用途             專門處理「功能提示列」hover+click           通用滑鼠熱區（任何位置）
```

BrowseKey 中的處理順序：
```
K_MOUSEMOVE:
  1. row 0 公司名稱硬編碼 hover（SaveScreen 方式）← 舊的，應移除
  2. HoverCheck(aMouse)     ← 功能提示列的 hover
  3. HoverUpdate()          ← aMsReg 通用熱區的 hover

K_LBUTTONDOWN:
  1. row 0, col>=56 硬編碼  ← 公司名稱 → K_SH_F10 ← 舊的，應移除
  2. browse 內部             ← return nKey（TBMouse）
  3. MsClick(aMouse)        ← 功能提示列的 click
  4. HitTest()              ← aMsReg 通用熱區的 click
```

---

## 七、功能提示資料的來源

### 7.1 集中管理的

```
來源函式           資料存放               使用者
────────────────   ────────────────────   ─────────────────────────────
BottomMessage      M->aMouse（回傳值）    BUYSELL, WINFORM, 單據系統
WindowMessage      M->aMouse（回傳值）    WINDOW, DBFWIN, ARRAY
```

### 7.2 散落的（直接 MsAddKey）

```
位置                    說明                              aMouse 有無
─────────────────────   ─────────────────────────────────  ──────────
DIALOG.PRG:74-75        Dialog 的 PgDn / ESC              無
MENU.PRG:81, 118        MenuBar 選項                      無（不需要）
TLIB.PRG:56             BottomMenu 功能鍵                 無
TLIB.PRG:308            TopMessage hint mode PgDn         無
TLIB.PRG:323            TopMessage 公司名稱 K_SH_F10      無
TOOLS.PRG:2508-2519     查詢畫面 PgDn / ALT+H             無
WINFORM.PRG:679, 775    WinForm GET 欄位點擊              無
```

---

## 八、公司名稱 K_SH_F10 的運作方式

### 在 BUYSELL 等單據系統中（透過 aMsReg）

```
TopMessage 註冊 MsAddKey → aMsReg
  → GETSYS K_LBUTTONDOWN → MsHitTest → hb_keyPut(K_SH_F10)
  → SetKey(K_SH_F10) handler 觸發
```

### 在 WINFORM 中（透過 BrowseKey 硬編碼）

```
TopMessage 註冊到外層 aMsReg（scope A）
WinForm 自己的 aMsReg（scope B）是空的
但公司名稱仍然有效 — 因為 BrowseKey 有硬編碼：
  K_MOUSEMOVE:    row 0 右側 SaveScreen/RestScreen hover（行 1048-1060）
  K_LBUTTONDOWN:  row 0 col>=56 → hb_keyPut(K_SH_F10)（行 1092-1094）
不依賴 aMsReg，所以不受 scope 影響
```

---

## 九、設計方案：oMs:aTopReg

### 9.1 核心設計

```
現在（有問題）：
  TopMessage 熱區 → aMsReg（PRIVATE）→ 被子功能遮蔽
  公司名稱        → BrowseKey 硬編碼  → 有效但不乾淨

方案（統一）：
  TopMessage 熱區 → oMs:aTopReg（DATA）→ 穿透所有 scope
  公司名稱        → oMs:aTopReg（DATA）→ 同一機制，移除硬編碼
```

### 9.2 修改範圍

```
mouse_handler.prg:
  1. DATA aTopReg INIT {}
  2. DATA aGlobalKeys INIT {}
  3. HoverUpdate() — 先掃 aTopReg，再掃 aMsReg
  4. HitTest() — 先掃 aTopReg（nKey=NIL → Eval codeblock），再掃 aMsReg
  5. AddKey/MsAddKey — 加第 7 參數 cRealFunc
  6. 移除 BrowseKey row 0 硬編碼（公司名稱 hover + click）

TLIB.PRG TopMessage():
  1. oMs:aTopReg := {}（每次呼叫清空重建）
  2. 註冊 ESC → oMs:aTopReg（日期右側）
  3. 註冊 PgDn → oMs:aTopReg（公司名稱左側）
  4. 註冊標題 → oMs:aTopReg（中間，nKey=NIL，codeblock={|| ShowHotKeys()}）
  5. 註冊公司名稱 → oMs:aTopReg（最右側，K_SH_F10）

TLIB.PRG ShowHotKeys():
  1. 第一類：PgDn + ESC（固定）
  2. 第二、三類：用 SetKey() 檢查 + KY_ 圖形鍵標
  3. PickList 顯示 → 選擇後 hb_keyPut(nKey)

MENU.PRG:
  1. &cProgString("StartUp") 返回後 oMs:aTopReg := {}（清除殘留）
```

### 9.3 生命週期

```
主選單（oMs:aTopReg = {}，沒有 TopMessage）
  ↓ 選擇選單項目
進入單據 → TopMessage() 被呼叫 → oMs:aTopReg 建立
  ↓ 進入子功能（F1/F3/F4 SetKey → Window()）
  Window() 宣告 PRIVATE aMsReg（遮蔽）
  但 oMs:aTopReg 不受影響 ← scope-free
  ↓ ESC 離開子功能
  PRIVATE aMsReg 恢復
  oMs:aTopReg 不變
  ↓ ESC 回主選單
&cProgString 返回 → oMs:aTopReg := {}（清除）
```

### 9.4 路線 A 驗證：從主選單按 F1

```
主選單（oMs:aTopReg = {}）
  → F1 SetKey → Window(p, l, v, K_F1)  ← 不是 P111()
  → Window() 不呼叫 TopMessage
  → oMs:aTopReg 仍為空 → 沒有 hotzone → 安全
```

### 9.5 PgDn/ESC 的處理規則

```
規則：PgDn/ESC 只要有 TopMessage 就有
     不依賴 lHintMode
     不改任何 PRG 的 TopMessage 呼叫

TopMessage() 內部：
  oMs:aTopReg := {}
  AAdd ESC（日期右側）    ← 永遠加
  AAdd PgDn（公司左側）   ← 永遠加
  AAdd 標題（中間）       ← 永遠加
  AAdd 公司（最右側）     ← 永遠加
```

### 9.6 TopMessage 的 row 0 佈局

```
[日期][ESC]     [標題（可點擊→PickList）]     [PgDn][公司名稱]
```

---

## 十、BUYSELL 的 TopMessage 呼叫（已驗證）

```
行 158: TopMessage( acTitle[1], 0 )            → 初始畫面（進入 BottomMenu 前）
行 286: TopMessage( acTitle[1], 0, .T. )       → 進入表頭 READ（不是表身！）
行 288: Read                                    → 表頭編輯（單號、客戶、日期...）
行 291: TopMessage( acTitle[1], 0 )            → 表頭 READ 結束
        ↓ 繼續處理...
行 1012: Read                                   → 表身編輯（沒有 TopMessage 呼叫）
```

重要發現：
- 行 288 的 READ 是表頭，不是表身
- 行 286 的 .T. 是「進入表頭 READ」的標記
- 表身 READ（行 1012）前後沒有 TopMessage 呼叫

---

## 十一、測試結果紀錄

```
測試項目                                  結果
─────────────────────────────────────     ──────
BUYSELL 標題 hover 變色                   OK
BUYSELL 標題 click → PickList             OK（用 SetKey + KY_ 圖形鍵標）
WINFORM 標題 hover + click                OK（oMs:aTopReg 解決 scope 問題）
公司名稱 hover + click                    OK（移至 oMs:aTopReg 後正常）
主選單返回清除 aTopReg                     OK
GETSYS K_MOUSEMOVE + HoverUpdate          OK（行 155-156, 347-349）
ShowHotKeys PickList 內容                  OK（三類 + KY_ 圖形鍵標）
PgDn hover + click                        有問題（見第十二節）
```

---

## 十二、PgDn/ESC 的問題與教訓

### 12.1 錯誤做法

把 PgDn/ESC 綁到 lHintMode 上，導致：
- 違反規格「PgDn/ESC 只要有 TopMessage 就有」
- 需要修改 13 個 PRG 的 TopMessage 呼叫
- lHintMode 的原始語意被破壞
- 反覆修改 IF lHintMode / IF !lHintMode 造成混亂

### 12.2 正確做法（尚未實作）

PgDn/ESC 永遠註冊在 oMs:aTopReg，不受 lHintMode 影響。
不改任何 PRG 的 TopMessage 呼叫。

### 12.3 嚴重錯誤

未經許可擅自還原檔案（cp backup → 原檔），毀掉三個多小時的成果。

---

## 十三、目前檔案狀態（2026-05-02 02:00）

```
檔案                    狀態
─────────────────────   ───────────────────────────────────────
TLIB.PRG                被擅自還原到 2251 備份，丟失所有 aTopReg/ShowHotKeys 變更
mouse_handler.prg       被擅自還原後重新套用變更（可能完整）
BUYSELL.PRG             可能有殘留的 .T./.F. 修改
其他 12 個 PRG          可能有殘留的 .T./.F. 修改
P11.PRG                 被擅自還原到備份
P113.PRG                可能有殘留修改
MENU.PRG                可能仍有 oMs:aTopReg := {} 變更
APPSETUP.PRG            已還原到備份（無修改）
```

---

## 十四、待完成工作

1. 恢復 TLIB.PRG 的所有變更（TopMessage aTopReg + ShowHotKeys）
2. 確認 mouse_handler.prg 的變更完整
3. 確認 13 個 PRG 的 TopMessage 呼叫恢復原狀（不改）
4. PgDn/ESC 永遠註冊，不綁 lHintMode
5. ESC 放日期右側，PgDn 放公司名稱左側
6. 測試全部功能
7. 清理不需要的備份檔
8. 更新報告和 UpdateList


## 十五、CJK 半字問題分析（待修正）

### 根因
dbEditEx 滾輪操作時，CallUser（畫底部）被完全跳過：
- 行 279-280: nKey != 0 → EXIT stabilize 迴圈
- 行 285-288: nKey == 0 檢查 → CallUser + forceStable 被跳過
- 底部完全沒重繪，但 browse 的 WM_PAINT 覆蓋了底部行

### dbEditEx vs ArrayBrowse 差異

```
                     dbEditEx                    ArrayBrowse
滾輪後畫底部          ❌ 被跳過                    ✅ 每次迴圈都畫（行173）
forceStable 時機      CallUser 之後（行288）       底部之前（行168）
滾輪 stabilize        下一輪才做                   即時做（行309）
底部繪製順序          callback → stabilize         stabilize → 底部
```

### 修正方向
在 dbEditEx 滾輪處理（行 381-401）加 lDoIdleCall := .T.，
確保下一輪執行 CallUser 重繪底部。
或：滾輪後加 oBrowse:forceStable() 確保 stabilize 完成。

### 之前失敗的修正（全部還原）
1. PaintText CJK break 移除 → 根因不在邊界擴展
2. Deferred to DE_IDLE → 滾輪跳過 CallUser，deferred 也沒觸發
3. DispBegin/DispEnd → 不影響 Windows GDI 層的 capture
4. post-stabilize callback mode=5 → CallUser 內部重算 nMode 為 DE_EXCEPT
5. hb_gtFlush → flush 的是 Harbour buffer，不是 GDI buffer
6. lFirst bottom draw → 只解決首次顯示，不解決滾輪

### 半字問題列入 UNDO_LIST
