diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 91be884..d166fa1 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -127,8 +127,8 @@ type procedure ShowFirstTimeBalloon; function FindFMXAppWindow: HWND; public - // Tray balloon (gated by the "Show tray notifications" setting). Used - // for messages the user must see while the window is hidden — e.g. an + // Tray balloon (caller applies its own gating setting). Used for + // messages the user must see while the window is hidden — e.g. an // autofill blocked by an elevated target. procedure ShowBalloon(const ATitle, AText: string; AWarning: Boolean = False); constructor Create(AMainForm: TForm); @@ -623,8 +623,8 @@ procedure TPMBridge.ShowBalloon(const ATitle, AText: string; var LBalloon: TNotifyIconData; begin - // Gated by the "Show tray notifications" user setting. - if not FShowNotifications then Exit; + // No gating here — each caller applies its own setting (first-time tray + // balloon → FShowNotifications; autofill-blocked → autofillFailBalloon). // Build a separate TNotifyIconData with NIF_INFO set, NIM_MODIFY on the // same uID. szInfo/szInfoTitle carry the balloon content. FillChar(LBalloon, SizeOf(LBalloon), 0); @@ -1091,6 +1091,12 @@ procedure SendSelectAllAndDelete; var LInputs: array[0..5] of TInput; begin + // This is the ONLY real-VK chord we inject (password chars go out as + // KEYEVENTF_UNICODE / VK_PACKET, which can't match a RegisterHotKey). + // If the user re-presses Ctrl+Shift mid-sequence, our VK_A below becomes + // a physical Ctrl+Shift+A = our own new-entry hotkey firing mid-fill. + // Force-release right here, at the risky instant — not just once up front. + ForceReleaseModifiers; FillChar(LInputs, SizeOf(LInputs), 0); LInputs[0].Itype := INPUT_KEYBOARD; LInputs[0].ki.wVk := VK_CONTROL; diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 55f4e90..259d853 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -108,6 +108,9 @@ type // Send Ctrl+A + Del before typing each field (user setting, default ON; // OFF for targets where Ctrl+A isn't select-all — terminals, RDP). FAutofillPendingClear: Boolean; + // Tray balloon when the fill fails while the window is hidden (user + // setting "Tray alert when autofill is blocked", default ON). + FAutofillPendingNotify: Boolean; // Created dynamically in FormCreate so the directive can pick either // TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without // needing two .fmx variants. Aligned to Client to fill the remaining @@ -900,7 +903,8 @@ begin FAutofillPendingHWND := FAutofillTargetHWND; FAutofillPendingHide := GetParam('hide_after') = '1'; FAutofillPendingUserOnly := GetParam('field') = 'user'; - FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON + FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON + FAutofillPendingNotify := GetParam('notify') <> '0'; // absent = ON FAutofillTargetHWND := 0; // Small timer so SetForegroundWindow has time to take effect before @@ -1453,7 +1457,7 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject); var TargetHwnd: HWND; PendingUser, PendingPass: string; - HideAfter, UserOnly, ClearFirst: Boolean; + HideAfter, UserOnly, ClearFirst, NotifyFail: Boolean; ForegroundAfter: HWND; begin TargetHwnd := FAutofillPendingHWND; @@ -1462,12 +1466,14 @@ begin HideAfter := FAutofillPendingHide; UserOnly := FAutofillPendingUserOnly; ClearFirst := FAutofillPendingClear; + NotifyFail := FAutofillPendingNotify; FAutofillPendingHWND := 0; FAutofillPendingUser := ''; FAutofillPendingPass := ''; FAutofillPendingHide := False; FAutofillPendingUserOnly := False; FAutofillPendingClear := False; + FAutofillPendingNotify := False; TTimer(Sender).Enabled := False; TTimer(Sender).Free; @@ -1487,9 +1493,9 @@ begin 'if(window.Bridge&&typeof Bridge.onAutofillResult==="function")' + 'Bridge.onAutofillResult(' + BoolToStr(LFillOk, True).ToLower + ')'); // Window hidden (tray) → the in-app failure toast is invisible; surface - // the block via a tray balloon instead (gated by the tray-notifications - // setting inside ShowBalloon). - if (not LFillOk) and + // the block via a tray balloon instead (gated by the dedicated + // "Tray alert when autofill is blocked" setting, default ON). + if (not LFillOk) and NotifyFail and ((not Self.Visible) or IsIconic(WindowHandleToPlatform(Self.Handle).Wnd)) then FBridge.ShowBalloon('Autofill blocked', 'The target window runs as administrator — Windows silently blocks ' + diff --git a/index.html b/index.html index 4e3c214..b949def 100644 --- a/index.html +++ b/index.html @@ -642,6 +642,19 @@ +
+ + Tray alert when autofill is blocked + + Shows a Windows balloon if the fill fails while the + app is hidden (e.g. the target runs as administrator). + + + +
diff --git a/js/app.js b/js/app.js index 5414a7a..2da266d 100644 --- a/js/app.js +++ b/js/app.js @@ -142,7 +142,8 @@ const Bridge = (() => { '&password=' + encodeURIComponent(password) + (hideAfter ? '&hide_after=1' : '') + (field === 'user' ? '&field=user' : '') + - (state.autofillClearField ? '' : '&clear=0')); + (state.autofillClearField ? '' : '&clear=0') + + (state.autofillFailBalloon ? '' : '¬ify=0')); }, // Ask Delphi to bring the main window to front (used when the @@ -645,6 +646,7 @@ const state = { recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON autofillClearField: localStorage.getItem('autofillClearField') !== '0', // default ON + autofillFailBalloon: localStorage.getItem('autofillFailBalloon') !== '0', // default ON // Hotkey combos. Each combo = { ctrl, shift, alt, win, key }. // key is the uppercase character or VK label ('A'..'Z', '0'..'9', // 'F1'..'F12'). Default: Ctrl+Shift+L / Ctrl+Shift+P. Combos are @@ -6344,6 +6346,7 @@ function openSettings() { $('#settingFaviconActionsRow').style.display = Bridge.active ? 'flex' : 'none'; $('#settingAutofill').checked = state.autofillEnabled; $('#settingAutofillClear').checked = state.autofillClearField; + $('#settingAutofillFailBalloon').checked = state.autofillFailBalloon; $('#settingAutofillRow').style.display = Bridge.active ? '' : 'none'; // Hotkey capture buttons — labels reflect current combos. $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); @@ -6756,6 +6759,8 @@ const SYNCED_SETTING_KEYS = [ // Send Ctrl+A + Del before typing each autofill field (default ON). // OFF for targets where Ctrl+A isn't select-all (terminals, RDP). 'autofillClearField', + // Tray balloon when a fill fails while the window is hidden (default ON). + 'autofillFailBalloon', ]; // Sets `data-editor-position` on so CSS can swap the slideover @@ -6839,6 +6844,9 @@ async function loadServerSettings() { case 'autofillClearField': localStorage.setItem('autofillClearField', v ? '1' : '0'); break; + case 'autofillFailBalloon': + localStorage.setItem('autofillFailBalloon', v ? '1' : '0'); + break; } }); // Apply visual settings immediately. @@ -7674,6 +7682,11 @@ async function init() { localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0'); saveServerSettings(); }); + $('#settingAutofillFailBalloon').addEventListener('change', e => { + state.autofillFailBalloon = e.target.checked; + localStorage.setItem('autofillFailBalloon', e.target.checked ? '1' : '0'); + saveServerSettings(); + }); // ---- Hotkey capture buttons ---- // Click → button label becomes "Press combo…" → next keydown captures.