Docs refresh: add user manual & architecture review; update README with accurate details
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
# Fix 03 — History drop-down: draw above everything + dynamic scrolling
|
||||
|
||||
**Files touched:** `src/main.cpp` only. No changes to `history.h`, `CMakeLists.txt`, or any other file.
|
||||
**Estimated effort:** 1–2 hours including testing.
|
||||
**Convention reminder:** this is a companion fix note (Fix-01, Fix-02, …) — do not edit older docs.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why the drop-down draws *behind* the transcript box (read this first)
|
||||
|
||||
The history drop-down is currently **painted onto the main window's surface** inside
|
||||
`PaintSurface()` (the block that starts `if (g_histOpen >= 0 && g_view == View::Main)`).
|
||||
|
||||
The transcript box, however, is **not** part of that painted surface. It is a real Win32
|
||||
child window — the `EDIT` control with ID `ID_EDIT_TEXT`. Two Win32 rules make the current
|
||||
approach impossible to fix by re-ordering paint calls:
|
||||
|
||||
1. **Child windows always render above the parent's client-area painting.** Whatever the
|
||||
parent draws in its own `WM_PAINT` sits *underneath* every child HWND. There is no
|
||||
"draw later so it ends up on top" — the EDIT is simply not on our canvas.
|
||||
2. The main window is created with **`WS_CLIPCHILDREN`** (see `CreateWindowExW` in
|
||||
`wWinMain`). That flag explicitly removes the EDIT's rectangle from the region the
|
||||
parent is even allowed to paint into. Our GDI+ drawing inside the EDIT's rect is
|
||||
silently clipped away. That is exactly why you only see the strip of drop-down in the
|
||||
gap *below* the EDIT in the screenshot.
|
||||
|
||||
A surface-painted drop-down also can never extend past the app window's edge, and we are
|
||||
re-implementing hover/scroll/click routing by hand. All three problems go away with the
|
||||
same fix.
|
||||
|
||||
## 2. The fix — strategy
|
||||
|
||||
**Stop painting the drop-down on the surface. Use a real top-level popup window instead.**
|
||||
|
||||
The app already contains exactly the right mechanism and we reuse it:
|
||||
|
||||
- The **microphone selector** already opens a small floating window of class `DictPopup`
|
||||
(`PopupProc` + `ShowSelectPopup` in `main.cpp`). It is created with
|
||||
`WS_POPUP | WS_EX_TOPMOST | WS_EX_TOOLWINDOW`, i.e. a top-level window that floats
|
||||
above the app, above the EDIT control, above *everything*, and is not clipped by the
|
||||
app window's edges at all.
|
||||
- The selection plumbing for history **already exists**: in `WndProc`, the
|
||||
`WM_APP_SELECT` handler already has an `ID_SEL_HISTORY` branch that archives the
|
||||
current text and loads the chosen entry. It was wired up but never used. We will not
|
||||
touch it — we just finally route clicks into it.
|
||||
|
||||
So the work is:
|
||||
|
||||
1. Upgrade the shared popup so it can **scroll** (max ~8 visible rows, mouse wheel,
|
||||
scrollbar thumb), positions itself **above or below** the anchor depending on screen
|
||||
space, and uses **DPI-scaled** row heights.
|
||||
2. Point the **History button** at `ShowSelectPopup(...)` with `ID_SEL_HISTORY`.
|
||||
3. **Delete** every line of the old surface-painted drop-down.
|
||||
|
||||
Bonus: the mic selector automatically gains scrolling and screen-edge handling too.
|
||||
|
||||
---
|
||||
|
||||
## 3. Step 1 — Replace `PopupState`
|
||||
|
||||
In `src/main.cpp`, find this (one line plus the variable under it):
|
||||
|
||||
```cpp
|
||||
struct PopupState { std::vector<std::wstring> items; int sel; int hot; HWND owner; int ctrlId; };
|
||||
static PopupState g_pop;
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```cpp
|
||||
struct PopupState {
|
||||
std::vector<std::wstring> items;
|
||||
int sel = -1; // index drawn in accent colour (current selection), -1 = none
|
||||
int hot = -1; // ABSOLUTE index of the row under the mouse, -1 = none
|
||||
HWND owner = nullptr;
|
||||
int ctrlId = 0; // posted back in WM_APP_SELECT wParam
|
||||
int scroll = 0; // index of the first visible row
|
||||
int visRows = 0; // number of rows visible in the popup
|
||||
int rowH = 30; // row height in px (DPI-scaled at open time)
|
||||
int pad = 3; // inner padding in px
|
||||
int wheelAccum = 0; // accumulates wheel deltas < 120 (trackpads)
|
||||
};
|
||||
static PopupState g_pop;
|
||||
|
||||
static const int kPopupMaxVisible = 8; // rows shown before the list scrolls
|
||||
```
|
||||
|
||||
> Note `hot` and the value posted back are now **absolute** item indices (index into the
|
||||
> full list), not visible-row indices. This matters once the list scrolls.
|
||||
|
||||
## 4. Step 2 — Replace `PopupProc` (and add one helper above it)
|
||||
|
||||
Find the existing `LRESULT CALLBACK PopupProc(HWND h, UINT m, WPARAM w, LPARAM l)` and
|
||||
replace the **entire function** with the code below. Also add the small
|
||||
`PopupRowFromY` helper **immediately above** `PopupProc` (it must be defined before it).
|
||||
|
||||
```cpp
|
||||
// Convert a client-area Y coordinate inside the popup to an ABSOLUTE item index.
|
||||
// Returns -1 if the point is not on a row.
|
||||
static int PopupRowFromY(int y) {
|
||||
int row = (y - g_pop.pad) / g_pop.rowH; // visible row 0..visRows-1
|
||||
if (row < 0 || row >= g_pop.visRows) return -1;
|
||||
int abs = g_pop.scroll + row; // absolute item index
|
||||
if (abs >= (int)g_pop.items.size()) return -1;
|
||||
return abs;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK PopupProc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
||||
switch (m) {
|
||||
case WM_MOUSEMOVE: {
|
||||
int row = PopupRowFromY(GET_Y_LPARAM(l));
|
||||
if (row != g_pop.hot) { g_pop.hot = row; InvalidateRect(h, nullptr, FALSE); }
|
||||
TRACKMOUSEEVENT t{ sizeof(t), TME_LEAVE, h, 0 }; TrackMouseEvent(&t);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
g_pop.hot = -1; InvalidateRect(h, nullptr, FALSE);
|
||||
return 0;
|
||||
case WM_MOUSEWHEEL: {
|
||||
int maxScroll = (int)g_pop.items.size() - g_pop.visRows;
|
||||
if (maxScroll <= 0) return 0; // everything fits: nothing to scroll
|
||||
g_pop.wheelAccum += GET_WHEEL_DELTA_WPARAM(w);
|
||||
int steps = g_pop.wheelAccum / WHEEL_DELTA; // whole notches only
|
||||
if (steps != 0) {
|
||||
g_pop.wheelAccum -= steps * WHEEL_DELTA;
|
||||
g_pop.scroll -= steps * 3; // 3 rows per wheel notch
|
||||
g_pop.scroll = std::max(0, std::min(g_pop.scroll, maxScroll));
|
||||
POINT pt; GetCursorPos(&pt); ScreenToClient(h, &pt);
|
||||
g_pop.hot = PopupRowFromY(pt.y); // keep hover correct after scroll
|
||||
InvalidateRect(h, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_KEYDOWN:
|
||||
if (w == VK_ESCAPE) DestroyWindow(h); // Esc closes the popup
|
||||
return 0;
|
||||
case WM_LBUTTONUP: {
|
||||
int row = PopupRowFromY(GET_Y_LPARAM(l));
|
||||
if (row >= 0)
|
||||
PostMessageW(g_pop.owner, WM_APP_SELECT, (WPARAM)g_pop.ctrlId, (LPARAM)row);
|
||||
DestroyWindow(h);
|
||||
return 0;
|
||||
}
|
||||
case WM_ACTIVATE:
|
||||
if (LOWORD(w) == WA_INACTIVE) DestroyWindow(h); // click-away closes
|
||||
return 0;
|
||||
case WM_ERASEBKGND: return 1;
|
||||
case WM_PAINT: {
|
||||
PAINTSTRUCT ps; HDC hdc = BeginPaint(h, &ps);
|
||||
RECT rc; GetClientRect(h, &rc);
|
||||
HDC mem = CreateCompatibleDC(hdc);
|
||||
HBITMAP bmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
|
||||
HBITMAP old = (HBITMAP)SelectObject(mem, bmp);
|
||||
{
|
||||
Graphics g(mem);
|
||||
g.SetSmoothingMode(SmoothingModeAntiAlias);
|
||||
g.SetTextRenderingHint(TextRenderingHintClearTypeGridFit);
|
||||
Rect all(0, 0, rc.right, rc.bottom);
|
||||
FillRound(g, T_CARD, all, 10);
|
||||
StrokeRound(g, T_FAINT, all, 10, 1.0f);
|
||||
|
||||
int n = (int)g_pop.items.size();
|
||||
bool hasBar = n > g_pop.visRows;
|
||||
int barW = std::max(3, (int)(4 * g_dpiScale));
|
||||
int rowW = rc.right - 2 * g_pop.pad - (hasBar ? barW + g_pop.pad : 0);
|
||||
int last = std::min(n, g_pop.scroll + g_pop.visRows);
|
||||
|
||||
for (int i = g_pop.scroll; i < last; ++i) {
|
||||
int vis = i - g_pop.scroll;
|
||||
Rect row(g_pop.pad, g_pop.pad + vis * g_pop.rowH, rowW, g_pop.rowH - 2);
|
||||
if (i == g_pop.hot) FillRound(g, T_CARD_HI, row, 7);
|
||||
RectF tb((REAL)row.X + 9, (REAL)row.Y, (REAL)row.Width - 12, (REAL)row.Height);
|
||||
DrawTextC(g, g_pop.items[i].c_str(), *g_gpUI,
|
||||
(i == g_pop.sel) ? T_ACCENT : T_TEXT,
|
||||
tb, StringAlignmentNear, StringAlignmentCenter);
|
||||
}
|
||||
if (hasBar) {
|
||||
int trackH = rc.bottom - 2 * g_pop.pad;
|
||||
int thumbH = std::max((int)(20 * g_dpiScale), trackH * g_pop.visRows / n);
|
||||
int maxScroll = n - g_pop.visRows;
|
||||
int thumbY = g_pop.pad + (trackH - thumbH) * g_pop.scroll / maxScroll;
|
||||
Rect thumb(rc.right - g_pop.pad - barW, thumbY, barW, thumbH);
|
||||
FillRound(g, T_DIM, thumb, barW / 2);
|
||||
}
|
||||
}
|
||||
BitBlt(hdc, 0, 0, rc.right, rc.bottom, mem, 0, 0, SRCCOPY);
|
||||
SelectObject(mem, old); DeleteObject(bmp); DeleteDC(mem);
|
||||
EndPaint(h, &ps);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return DefWindowProc(h, m, w, l);
|
||||
}
|
||||
```
|
||||
|
||||
Two deliberate changes from the old version, so you don't think something was lost:
|
||||
|
||||
- The old paint code built a `Font f(mem, g_fUI)` every frame. We now use the cached
|
||||
`*g_gpUI` GDI+ font — same convention as Fix-02 (cached `g_gp*` fonts everywhere).
|
||||
- The mouse-wheel handler works because the popup has keyboard focus
|
||||
(`SetFocus(p)` is called when it opens) — Windows delivers `WM_MOUSEWHEEL` to the
|
||||
focused window. Don't remove the `SetFocus` call in Step 3.
|
||||
|
||||
## 5. Step 3 — Replace `ShowSelectPopup`
|
||||
|
||||
Replace the **entire** existing `ShowSelectPopup` function with:
|
||||
|
||||
```cpp
|
||||
void ShowSelectPopup(HWND owner, int ctrlId, const std::vector<std::wstring>& items, int sel, const RectF& anchor) {
|
||||
static bool reg = false;
|
||||
if (!reg) {
|
||||
WNDCLASSEXW wc{ sizeof(wc) };
|
||||
wc.lpfnWndProc = PopupProc;
|
||||
wc.hInstance = hInst;
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wc.hbrBackground = CreateSolidBrush(CR_SURFACE);
|
||||
wc.lpszClassName = L"DictPopup";
|
||||
RegisterClassExW(&wc);
|
||||
reg = true;
|
||||
}
|
||||
if (items.empty()) return;
|
||||
|
||||
float s = g_dpiScale;
|
||||
int rowH = (int)(30 * s);
|
||||
int pad = (int)(3 * s); if (pad < 3) pad = 3;
|
||||
int n = (int)items.size();
|
||||
int visRows = std::min(n, kPopupMaxVisible);
|
||||
|
||||
g_pop = PopupState{}; // reset everything (incl. scroll/wheelAccum)
|
||||
g_pop.items = items;
|
||||
g_pop.sel = sel;
|
||||
g_pop.owner = owner;
|
||||
g_pop.ctrlId = ctrlId;
|
||||
g_pop.visRows = visRows;
|
||||
g_pop.rowH = rowH;
|
||||
g_pop.pad = pad;
|
||||
if (sel >= 0 && n > visRows) // scroll so the current selection is visible
|
||||
g_pop.scroll = std::max(0, std::min(sel - visRows / 2, n - visRows));
|
||||
|
||||
int h = visRows * rowH + 2 * pad;
|
||||
int wdt = std::max((int)anchor.Width, (int)(260 * s)); // never narrower than 260px
|
||||
|
||||
// Anchor rect is in CLIENT coordinates; convert its top-left to screen.
|
||||
POINT tl{ (LONG)anchor.X, (LONG)anchor.Y };
|
||||
ClientToScreen(owner, &tl);
|
||||
int anchorTop = tl.y;
|
||||
int anchorBottom = tl.y + (int)anchor.Height;
|
||||
int x = tl.x;
|
||||
|
||||
// Place below the anchor if it fits on the monitor's work area, otherwise above.
|
||||
HMONITOR mon = MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO mi{ sizeof(mi) };
|
||||
GetMonitorInfo(mon, &mi);
|
||||
int y;
|
||||
if (anchorBottom + 2 + h <= mi.rcWork.bottom) y = anchorBottom + 2; // open downward
|
||||
else y = anchorTop - h - 2; // flip upward
|
||||
if (y < mi.rcWork.top) y = mi.rcWork.top;
|
||||
if (x + wdt > mi.rcWork.right) x = mi.rcWork.right - wdt;
|
||||
if (x < mi.rcWork.left) x = mi.rcWork.left;
|
||||
|
||||
HWND p = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TOPMOST, L"DictPopup", L"",
|
||||
WS_POPUP, x, y, wdt, h, owner, nullptr, hInst, nullptr);
|
||||
if (!p) return;
|
||||
|
||||
int corner = DWMWCP_ROUND;
|
||||
DwmSetWindowAttribute(p, DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(corner));
|
||||
ShowWindow(p, SW_SHOWNA);
|
||||
SetFocus(p); // required: wheel + Esc are delivered to the focused window
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The old debug `MessageBoxW("Popup failed: ...")` is intentionally removed (it was
|
||||
scaffolding). Failure now just silently returns.
|
||||
- Because the popup is a top-level window, it may legitimately extend **past the app
|
||||
window's edges** — that is correct and expected. It only clamps to the *monitor's*
|
||||
work area.
|
||||
- It positions below the button when there is monitor space, otherwise flips above —
|
||||
standard combo-box behaviour. Since the History button sits near the bottom of the
|
||||
app window, it will usually still have screen room below and will open downward,
|
||||
floating *over* whatever is beneath. If you want it to **always open upward** like
|
||||
the current build, replace the placement `if/else` with just:
|
||||
`int y = anchorTop - h - 2; if (y < mi.rcWork.top) y = mi.rcWork.top;`
|
||||
|
||||
## 6. Step 4 — Rewire the History button
|
||||
|
||||
In `OnClick(HWND hWnd, WK kind)`, find the `case WK::History:` block:
|
||||
|
||||
```cpp
|
||||
case WK::History: {
|
||||
if (g_histOpen >= 0) { g_histOpen = -1; InvalidateRect(hWnd, nullptr, FALSE); break; }
|
||||
if (g_history.empty()) { SetStatus(hWnd, L"No history yet"); break; }
|
||||
g_histOpen = (int)g_history.size();
|
||||
g_histScroll = 0; g_histHot = -1;
|
||||
InvalidateRect(hWnd, nullptr, FALSE);
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```cpp
|
||||
case WK::History: {
|
||||
g_history = LoadHistoryIndex(); // refresh in case files changed
|
||||
if (g_history.empty()) { SetStatus(hWnd, L"No history yet"); break; }
|
||||
std::vector<std::wstring> labels;
|
||||
labels.reserve(g_history.size());
|
||||
for (const auto& e : g_history) labels.push_back(e.label);
|
||||
ShowSelectPopup(hWnd, ID_SEL_HISTORY, labels, -1, g_w[(int)WK::History].r);
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
**Do not write any new selection-handling code.** When the user clicks a row, the popup
|
||||
posts `WM_APP_SELECT` with `wParam = ID_SEL_HISTORY` and `lParam = absolute index`. The
|
||||
existing `else if (ctrlId == ID_SEL_HISTORY ...)` branch inside `WndProc`'s
|
||||
`WM_APP_SELECT` handler already does everything (archive current text, load the entry,
|
||||
set `g_lastLoadedText`, refresh the index, update the placeholder, show "Loaded from
|
||||
history"). Leave it exactly as it is.
|
||||
|
||||
## 7. Step 5 — Delete the legacy painted drop-down (6 deletions)
|
||||
|
||||
All in `src/main.cpp`. Delete **only** what is quoted — the surrounding code stays.
|
||||
|
||||
**5a — globals.** Between `int g_setHot = -1;` and `Downloader g_dl;`, delete these three lines:
|
||||
|
||||
```cpp
|
||||
int g_histOpen = -1;
|
||||
int g_histScroll = 0;
|
||||
int g_histHot = -1;
|
||||
```
|
||||
|
||||
(Keep `g_setHot`, keep `std::vector<HistoryEntry> g_history;`, keep `g_lastLoadedText`.)
|
||||
|
||||
**5b — the paint block in `PaintSurface`.** Delete this whole block (it sits right
|
||||
before `RECT vr = g_vuRect;`):
|
||||
|
||||
```cpp
|
||||
if (g_histOpen >= 0 && g_view == View::Main) {
|
||||
RectF& anchor = g_w[(int)WK::History].r;
|
||||
...
|
||||
if (g_histOpen > maxVis) {
|
||||
...
|
||||
FillRound(g, T_DIM, thumb, 3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(~30 lines, from `if (g_histOpen >= 0 ...` down to the matching closing brace.)
|
||||
|
||||
**5c — the hit-test function.** Delete the entire `HistDropHitTest` function (it sits
|
||||
just above `int HitTest(POINT p)`):
|
||||
|
||||
```cpp
|
||||
static int HistDropHitTest(POINT p) {
|
||||
...
|
||||
return realIdx;
|
||||
}
|
||||
```
|
||||
|
||||
**5d — in `WM_MOUSEMOVE`.** Keep the `POINT p{...};` line (it is used by `HitTest(p)`
|
||||
below); delete only this block:
|
||||
|
||||
```cpp
|
||||
if (g_histOpen > 0) {
|
||||
int dh = HistDropHitTest(p);
|
||||
if (dh != g_histHot) { g_histHot = dh; InvalidateRect(hWnd, nullptr, FALSE); }
|
||||
}
|
||||
```
|
||||
|
||||
**5e — in `WM_MOUSELEAVE`.** Delete this line:
|
||||
|
||||
```cpp
|
||||
if (g_histHot >= 0) { g_histHot = -1; InvalidateRect(hWnd, nullptr, FALSE); }
|
||||
```
|
||||
|
||||
**5f — in `WM_LBUTTONDOWN`.** Delete this block (keep `g_active = g_hot;` and below):
|
||||
|
||||
```cpp
|
||||
if (g_histOpen > 0) {
|
||||
POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
||||
if (HistDropHitTest(p) < 0) { g_histOpen = -1; g_histHot = -1; g_active = -1; InvalidateRect(hWnd, nullptr, FALSE); return 0; }
|
||||
}
|
||||
```
|
||||
|
||||
**5g — in `WM_LBUTTONUP`.** Delete this entire block (the popup + `WM_APP_SELECT`
|
||||
path replaces it; keep the settings branch above it and `ReleaseCapture()` below it):
|
||||
|
||||
```cpp
|
||||
if (g_histOpen > 0) {
|
||||
POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
||||
int dh = HistDropHitTest(p);
|
||||
if (dh >= 0 && dh < (int)g_history.size()) {
|
||||
std::wstring cur = GetEditText(hWnd);
|
||||
if (!cur.empty() && cur != g_lastLoadedText)
|
||||
ArchiveSession(cur);
|
||||
std::wstring text = ReadFileUtf8(g_history[dh].path);
|
||||
SetWindowTextW(GetDlgItem(hWnd, ID_EDIT_TEXT), text.c_str());
|
||||
g_lastLoadedText = text;
|
||||
g_editDirty = false;
|
||||
g_history = LoadHistoryIndex();
|
||||
UpdatePlaceholder(hWnd);
|
||||
SetStatus(hWnd, L"Loaded from history");
|
||||
}
|
||||
g_histOpen = -1; g_histHot = -1;
|
||||
InvalidateRect(hWnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**5h — in `WM_MOUSEWHEEL`.** Delete this block (keep the `View::Settings` scroll block
|
||||
above it):
|
||||
|
||||
```cpp
|
||||
if (g_histOpen > 6) {
|
||||
g_histScroll -= GET_WHEEL_DELTA_WPARAM(wParam) / 60;
|
||||
g_histScroll = std::max(0, std::min(g_histScroll, g_histOpen - 6));
|
||||
InvalidateRect(hWnd, nullptr, FALSE);
|
||||
}
|
||||
```
|
||||
|
||||
**Verification after Step 5:** Ctrl+F the file for `g_hist` — the only remaining
|
||||
matches must be `g_history` (the vector). Search for `HistDropHitTest` — zero matches.
|
||||
If anything else matches, you missed a deletion and the compiler will tell you too
|
||||
(undeclared identifier `g_histOpen`).
|
||||
|
||||
## 8. Step 6 — (Optional polish) make the button a true toggle
|
||||
|
||||
Known small quirk of popup-based menus: clicking the History button **while the popup is
|
||||
open** first closes it (the click deactivates the popup → `WM_ACTIVATE` destroys it),
|
||||
then the button's click-handler immediately reopens it. So the button acts as
|
||||
"reopen", not "toggle closed". The mic selector has always behaved this way; if nobody
|
||||
has complained, skip this step. To make it a real toggle:
|
||||
|
||||
Add next to `g_pop`:
|
||||
|
||||
```cpp
|
||||
static DWORD g_popClosedTick = 0; // when the popup last closed
|
||||
static int g_popClosedId = 0; // which ctrlId it was showing
|
||||
```
|
||||
|
||||
Add a case to `PopupProc`:
|
||||
|
||||
```cpp
|
||||
case WM_DESTROY:
|
||||
g_popClosedTick = GetTickCount();
|
||||
g_popClosedId = g_pop.ctrlId;
|
||||
return 0;
|
||||
```
|
||||
|
||||
Then make the first line of `case WK::History:` (and optionally `WK::SelAudio`):
|
||||
|
||||
```cpp
|
||||
if (g_popClosedId == ID_SEL_HISTORY && GetTickCount() - g_popClosedTick < 250) {
|
||||
g_popClosedId = 0; // this click was the user toggling the popup shut — swallow it
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Build & test checklist
|
||||
|
||||
Build exactly as usual (`cmake --build build --config Release --target win-dictation`).
|
||||
Then verify, in order:
|
||||
|
||||
1. **Core bug:** with 3+ history entries, click History. The list must appear **fully on
|
||||
top of the transcript box** (and on top of the window edge if it extends past it).
|
||||
Nothing hidden behind the EDIT control.
|
||||
2. **Scrolling:** create 10+ entries (record short clips and press Clear between them, or
|
||||
copy-paste extra `.txt` files in the `history\` folder using the same
|
||||
`YYYY-MM-DD_HHMMSS.txt` name format). Open History: max 8 rows visible, scrollbar
|
||||
thumb on the right, mouse wheel scrolls 3 rows per notch, hover highlight stays
|
||||
correct while scrolling.
|
||||
3. **Selection:** click an entry → transcript loads it, status shows "Loaded from
|
||||
history", and the text that was in the box beforehand got archived (check the
|
||||
`history\` folder gained a file).
|
||||
4. **Dismiss:** click anywhere outside → closes. Press Esc → closes. Click an entry →
|
||||
closes.
|
||||
5. **Regression — mic selector:** the Microphone popup must still open and select
|
||||
devices correctly (it shares all this code). It now also scrolls if a machine has
|
||||
9+ capture devices and never clips at the screen edge.
|
||||
6. **Placement:** drag the app window to the very bottom of the screen → popup flips
|
||||
upward. Drag it high up → popup opens downward (or stays upward if you chose the
|
||||
always-up variant).
|
||||
7. **Few items:** with 1–2 entries the popup is exactly that tall, no scrollbar.
|
||||
8. **Empty:** with an empty `history\` folder, clicking History shows "No history yet"
|
||||
and no popup.
|
||||
9. **DPI:** if you can, test at 125%/150% scaling — hover highlight must line up with
|
||||
the cursor (row height is now DPI-scaled; it previously was hard-coded 30px).
|
||||
|
||||
## 10. Tuning knobs (all in one place)
|
||||
|
||||
| What | Where | Default |
|
||||
|---|---|---|
|
||||
| Visible rows before scrolling | `kPopupMaxVisible` | 8 |
|
||||
| Rows per wheel notch | `g_pop.scroll -= steps * 3;` in `PopupProc` | 3 |
|
||||
| Minimum popup width | `std::max((int)anchor.Width, (int)(260 * s))` | 260 px |
|
||||
| Open direction | placement `if/else` in `ShowSelectPopup` | below, flip up if no room |
|
||||
| Row height | `rowH = (int)(30 * s)` | 30 px @ 100% DPI |
|
||||
|
||||
## 11. Do NOT touch
|
||||
|
||||
- `#define ID_SEL_HISTORY 1019` — stays.
|
||||
- The `ID_SEL_HISTORY` branch inside `WM_APP_SELECT` in `WndProc` — stays as-is; it is
|
||||
the selection handler now.
|
||||
- `g_history`, `g_lastLoadedText`, `LoadHistoryIndex()`, `ArchiveSession()`, all of
|
||||
`history.h` — unchanged.
|
||||
- The `WK::History` widget rect / `DrawSelectSurface(g, w, L"History")` painting of the
|
||||
button itself — unchanged; only what happens on click changes.
|
||||
Reference in New Issue
Block a user