diff --git a/CLAUDE.md b/CLAUDE.md index e641960..2868266 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,6 +190,21 @@ Détails : - Le `X` **poste** `WM_LAZY_CLOSE` à la fenêtre message-only au lieu de détruire sur place : libérer une fenêtre depuis son propre gestionnaire libérerait, en plein envoi, l'instance qui lui sert de window proc. +- **Transparence au repos** (`WS_EX_LAYERED` + `SetLayeredWindowAttributes`) : + pleine opacité quand la souris est dessus, `FLazyAlphaPct` sinon. Réglée par + **clic droit sur la barrette** → 100/75/50/25 %, persistée en pref DPAPI + (`lazyAlphaPct`, défaut 60) — **pas** dans `settings_json`, qui exige une + session alors que la barrette peut être allumée coffre fermé. Le menu + appartient à la fenêtre message-only : `TrackPopupMenu` réclame un + `SetForegroundWindow` pour se refermer seul, et le faire sur la barrette + détruirait le `NOACTIVATE` qui la fait fonctionner. Deux portes vers ce menu — + `WM_NCRBUTTONUP` (le fond est non-client à cause de `HTCAPTION`) et + `WM_CONTEXTMENU` (remonté par les boutons). +- Le survol est **sondé** par un `SetTimer` de 200 ms, pas par `TrackMouseEvent` : + avec quatre fenêtres enfants, passer de la barrette à un bouton envoie déjà un + `WM_MOUSELEAVE` au parent. Comparer le curseur au rectangle ignore ce piège. +- Transparente **n'est pas traversante** : la barrette avale toujours les clics + sur toute sa surface (`WS_EX_TRANSPARENT` contredirait ses boutons). - **Thème** : boutons `BS_OWNERDRAW` + `WM_DRAWITEM`, palette recopiée des variables de `css/style.css` en COLORREF (`$00BBGGRR` — octets **inversés** par rapport à l'hexa CSS). `SetPreferredAppMode(ForceDark)` ne themise que ce diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 58a7c2f..28259a9 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -32,7 +32,7 @@ uses System.SysUtils, System.Classes, System.Math, System.Types, FMX.Types, FMX.Forms, Winapi.Windows, Winapi.ShellAPI, Winapi.Messages, - PM.SingleInstance; + PM.SingleInstance, PM.UserPrefs; type // ------------------------------------------------------------------------- @@ -157,7 +157,19 @@ type // sous SetPreferredAppMode(ForceDark), qui ne themise que ce que le systeme // dessine lui-meme (menus, ascenseurs, infobulles). FDarkTheme: Boolean; + // Opacite AU REPOS, en pourcentage - la barrette redevient pleine des que + // la souris entre. 100 = jamais transparente. Vit en pref DPAPI et non dans + // settings_json : elle peut etre allumee coffre ferme, quand aucune session + // n'existe pour lire les reglages du serveur. + FLazyAlphaPct: Integer; + // Dernier etat connu du survol, pour ne reposer l'opacite que sur un + // changement - SetLayeredWindowAttributes force un repaint a chaque appel. + FLazyHover: Boolean; procedure LazyWindowHandler(var AMsg: TMessage); + /// Pose l'opacite qui convient a l'etat de survol donne. + procedure ApplyLazyAlpha(AHover: Boolean); + /// Menu du clic droit sur la barrette : le choix de l'opacite au repos. + procedure ShowLazyMenu; /// Peint un bouton de la barrette (BS_OWNERDRAW). Le systeme garde le suivi /// du clic et l'etat enfonce ; nous ne reprenons que les couleurs. procedure DrawLazyButton(ADraw: PDrawItemStruct); @@ -391,6 +403,17 @@ const LAZY_ID_PWD = 102; LAZY_ID_TOTP = 103; LAZY_ID_CLOSE = 104; + // LE SONDAGE PLUTOT QUE TrackMouseEvent. La barrette a quatre fenetres + // enfants : passer d'elle a un bouton envoie deja un WM_MOUSELEAVE au parent, + // et il faudrait donc suivre la souris sur les cinq fenetres pour recoller + // les morceaux. Comparer le curseur au rectangle cinq fois par seconde ne + // connait pas ce piege, et ne coute rien de mesurable. + LAZY_TIMER_ID = 1; + LAZY_POLL_MS = 200; + // Cle de la pref, et opacite par defaut. 60 % laisse lire ce qu'il y a + // derriere sans faire disparaitre la barrette du regard. + LAZY_ALPHA_KEY = 'lazyAlphaPct'; + LAZY_ALPHA_DEFAULT = 60; LAZY_W = 236; LAZY_H = 40; @@ -1075,6 +1098,56 @@ begin DT_CENTER or DT_VCENTER or DT_SINGLELINE); end; +procedure TPMBridge.ApplyLazyAlpha(AHover: Boolean); +begin + if FLazyWnd = 0 then Exit; + // TRANSPARENTE N'EST PAS TRAVERSANTE : la barrette continue d'avaler les + // clics sur toute sa surface. C'est un allegement du regard, pas un passage. + if AHover or (FLazyAlphaPct >= 100) then + SetLayeredWindowAttributes(FLazyWnd, 0, 255, LWA_ALPHA) + else + SetLayeredWindowAttributes(FLazyWnd, 0, + MulDiv(255, FLazyAlphaPct, 100), LWA_ALPHA); +end; + +procedure TPMBridge.ShowLazyMenu; +const + CHOICES: array[0..3] of Integer = (100, 75, 50, 25); +var + LMenu: HMENU; + LPt: TPoint; + LCmd: Cardinal; + I: Integer; +begin + LMenu := CreatePopupMenu; + if LMenu = 0 then Exit; + try + for I := Low(CHOICES) to High(CHOICES) do + AppendMenu(LMenu, + MF_STRING or IfThen(FLazyAlphaPct = CHOICES[I], MF_CHECKED, MF_UNCHECKED), + CHOICES[I], PChar(Format('%d%% when the mouse leaves', [CHOICES[I]]))); + + GetCursorPos(LPt); + // Le proprietaire du menu est la fenetre message-only, JAMAIS la barrette : + // TrackPopupMenu exige un SetForegroundWindow pour se refermer tout seul, et + // le faire sur la barrette detruirait le NOACTIVATE qui la fait fonctionner. + SetForegroundWindow(FMsgWindow); + LCmd := Cardinal(TrackPopupMenu(LMenu, + TPM_RETURNCMD or TPM_RIGHTBUTTON or TPM_NONOTIFY, + LPt.X, LPt.Y, 0, FMsgWindow, nil)); + PostMessage(FMsgWindow, WM_NULL, 0, 0); + + if LCmd > 0 then + begin + FLazyAlphaPct := LCmd; + SetPref(LAZY_ALPHA_KEY, IntToStr(FLazyAlphaPct)); + ApplyLazyAlpha(FLazyHover); + end; + finally + DestroyMenu(LMenu); + end; +end; + function TPMBridge.LazyBarVisible: Boolean; begin Result := FLazyWnd <> 0; @@ -1084,8 +1157,31 @@ procedure TPMBridge.LazyWindowHandler(var AMsg: TMessage); var LRect: TRect; LBrush: HBRUSH; + LPt: TPoint; + LIn: Boolean; begin case AMsg.Msg of + WM_TIMER: + begin + GetCursorPos(LPt); + GetWindowRect(FLazyWnd, LRect); + LIn := PtInRect(LRect, LPt); + if LIn <> FLazyHover then + begin + FLazyHover := LIn; + ApplyLazyAlpha(LIn); + end; + Exit; + end; + // Deux portes pour un seul menu : le fond de la barrette est declare + // non-client (HTCAPTION, pour pouvoir la trainer) et rend donc un clic droit + // NC, tandis qu'un clic droit sur un bouton remonte en WM_CONTEXTMENU. + WM_NCRBUTTONUP, WM_CONTEXTMENU: + begin + ShowLazyMenu; + AMsg.Result := 0; + Exit; + end; // THE STRIP IS ITS OWN TITLE BAR. It has none to grab, and it will end up // sitting over the very field it fills - so every pixel that is not a // button drags it out of the way. @@ -1166,7 +1262,11 @@ begin if FLazyWnd = 0 then Exit; SetWindowLong(FLazyWnd, GWL_EXSTYLE, GetWindowLong(FLazyWnd, GWL_EXSTYLE) or - WS_EX_NOACTIVATE or WS_EX_TOPMOST or WS_EX_TOOLWINDOW); + WS_EX_NOACTIVATE or WS_EX_TOPMOST or WS_EX_TOOLWINDOW or WS_EX_LAYERED); + + FLazyAlphaPct := StrToIntDef(GetPref(LAZY_ALPHA_KEY), LAZY_ALPHA_DEFAULT); + if (FLazyAlphaPct < 25) or (FLazyAlphaPct > 100) then + FLazyAlphaPct := LAZY_ALPHA_DEFAULT; MakeButton(LAZY_ID_FULL, 'Full', 5, 56); MakeButton(LAZY_ID_PWD, 'Password', 67, 76); @@ -1183,6 +1283,12 @@ begin SetWindowPos(FLazyWnd, HWND_TOPMOST, LWork.Right - LAZY_W - 24, LWork.Bottom - LAZY_H - 24, LAZY_W, LAZY_H, SWP_NOACTIVATE or SWP_SHOWWINDOW); + + // La souris est ailleurs a la seconde ou la barrette parait : elle nait donc + // au repos, et non pleine puis fondue une fraction de seconde plus tard. + FLazyHover := False; + ApplyLazyAlpha(False); + SetTimer(FLazyWnd, LAZY_TIMER_ID, LAZY_POLL_MS, nil); end; procedure TPMBridge.MsgWindowHandler(var AMsg: TMessage);