feat(autofill): "Clear the field before typing" setting (default ON)
The Ctrl+A + Del sent before each field misbehaves on targets where Ctrl+A isn't select-all (terminals, some remote desktops). New synced setting (autofillClearField, Settings > Autofill) gates it: JS appends clear=0 to cmd://autofill/execute when off; ExecuteAutofill wraps the three SendSelectAllAndDelete calls behind AClearFirst. Absent param = ON, so existing behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -171,9 +171,10 @@ type
|
|||||||
// the caller trays it after the fill.
|
// the caller trays it after the fill.
|
||||||
// Returns False when nothing was typed: elevated target (UIPI would
|
// Returns False when nothing was typed: elevated target (UIPI would
|
||||||
// silently drop the keystrokes) or focus never left our own window.
|
// silently drop the keystrokes) or focus never left our own window.
|
||||||
|
// AClearFirst: send Ctrl+A + Del before each field (user setting).
|
||||||
function ExecuteAutofill(ATargetHWND: HWND;
|
function ExecuteAutofill(ATargetHWND: HWND;
|
||||||
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
|
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
|
||||||
ARestoreAfter: Boolean = False): Boolean;
|
ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean;
|
||||||
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
||||||
property TrayAdded: Boolean read FTrayAdded;
|
property TrayAdded: Boolean read FTrayAdded;
|
||||||
property AutofillRegistered: Boolean read FAutofillRegistered;
|
property AutofillRegistered: Boolean read FAutofillRegistered;
|
||||||
@@ -1187,10 +1188,21 @@ end;
|
|||||||
|
|
||||||
function TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
|
function TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
|
||||||
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
|
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
|
||||||
ARestoreAfter: Boolean = False): Boolean;
|
ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean;
|
||||||
const
|
const
|
||||||
MinimizeSettleMs = 80;
|
MinimizeSettleMs = 80;
|
||||||
FocusSettleDelayMs = 120;
|
FocusSettleDelayMs = 120;
|
||||||
|
|
||||||
|
// Ctrl+A + Del to empty the target field — skipped when the user turned
|
||||||
|
// "Clear the field before typing" off (Ctrl+A isn't select-all everywhere:
|
||||||
|
// terminals, some remote desktops).
|
||||||
|
procedure ClearFieldIfWanted;
|
||||||
|
begin
|
||||||
|
if not AClearFirst then Exit;
|
||||||
|
SendSelectAllAndDelete;
|
||||||
|
Sleep(60);
|
||||||
|
end;
|
||||||
|
|
||||||
var
|
var
|
||||||
OwnFormHwnd: HWND;
|
OwnFormHwnd: HWND;
|
||||||
begin
|
begin
|
||||||
@@ -1233,28 +1245,24 @@ begin
|
|||||||
// no password. Used by the quick-search right-click / Shift+Enter path.
|
// no password. Used by the quick-search right-click / Shift+Enter path.
|
||||||
if AUsernameOnly then
|
if AUsernameOnly then
|
||||||
begin
|
begin
|
||||||
SendSelectAllAndDelete;
|
ClearFieldIfWanted;
|
||||||
Sleep(60);
|
|
||||||
SendUnicodeString(AUsername);
|
SendUnicodeString(AUsername);
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if AUsername = '' then
|
if AUsername = '' then
|
||||||
begin
|
begin
|
||||||
SendSelectAllAndDelete;
|
ClearFieldIfWanted;
|
||||||
Sleep(60);
|
|
||||||
SendUnicodeString(APassword);
|
SendUnicodeString(APassword);
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
SendSelectAllAndDelete;
|
ClearFieldIfWanted;
|
||||||
Sleep(60);
|
|
||||||
SendUnicodeString(AUsername);
|
SendUnicodeString(AUsername);
|
||||||
Sleep(200);
|
Sleep(200);
|
||||||
SendVKey(VK_TAB);
|
SendVKey(VK_TAB);
|
||||||
Sleep(200);
|
Sleep(200);
|
||||||
SendSelectAllAndDelete;
|
ClearFieldIfWanted;
|
||||||
Sleep(60);
|
|
||||||
SendUnicodeString(APassword);
|
SendUnicodeString(APassword);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ type
|
|||||||
// True when the pending autofill should type ONLY the username (quick
|
// True when the pending autofill should type ONLY the username (quick
|
||||||
// search "autofill username" — right-click / Shift+Enter in fill mode).
|
// search "autofill username" — right-click / Shift+Enter in fill mode).
|
||||||
FAutofillPendingUserOnly: Boolean;
|
FAutofillPendingUserOnly: Boolean;
|
||||||
|
// 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;
|
||||||
// Created dynamically in FormCreate so the directive can pick either
|
// Created dynamically in FormCreate so the directive can pick either
|
||||||
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
|
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
|
||||||
// needing two .fmx variants. Aligned to Client to fill the remaining
|
// needing two .fmx variants. Aligned to Client to fill the remaining
|
||||||
@@ -897,6 +900,7 @@ begin
|
|||||||
FAutofillPendingHWND := FAutofillTargetHWND;
|
FAutofillPendingHWND := FAutofillTargetHWND;
|
||||||
FAutofillPendingHide := GetParam('hide_after') = '1';
|
FAutofillPendingHide := GetParam('hide_after') = '1';
|
||||||
FAutofillPendingUserOnly := GetParam('field') = 'user';
|
FAutofillPendingUserOnly := GetParam('field') = 'user';
|
||||||
|
FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON
|
||||||
FAutofillTargetHWND := 0;
|
FAutofillTargetHWND := 0;
|
||||||
|
|
||||||
// Small timer so SetForegroundWindow has time to take effect before
|
// Small timer so SetForegroundWindow has time to take effect before
|
||||||
@@ -1449,7 +1453,7 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
|
|||||||
var
|
var
|
||||||
TargetHwnd: HWND;
|
TargetHwnd: HWND;
|
||||||
PendingUser, PendingPass: string;
|
PendingUser, PendingPass: string;
|
||||||
HideAfter, UserOnly: Boolean;
|
HideAfter, UserOnly, ClearFirst: Boolean;
|
||||||
ForegroundAfter: HWND;
|
ForegroundAfter: HWND;
|
||||||
begin
|
begin
|
||||||
TargetHwnd := FAutofillPendingHWND;
|
TargetHwnd := FAutofillPendingHWND;
|
||||||
@@ -1457,11 +1461,13 @@ begin
|
|||||||
PendingPass := FAutofillPendingPass;
|
PendingPass := FAutofillPendingPass;
|
||||||
HideAfter := FAutofillPendingHide;
|
HideAfter := FAutofillPendingHide;
|
||||||
UserOnly := FAutofillPendingUserOnly;
|
UserOnly := FAutofillPendingUserOnly;
|
||||||
|
ClearFirst := FAutofillPendingClear;
|
||||||
FAutofillPendingHWND := 0;
|
FAutofillPendingHWND := 0;
|
||||||
FAutofillPendingUser := '';
|
FAutofillPendingUser := '';
|
||||||
FAutofillPendingPass := '';
|
FAutofillPendingPass := '';
|
||||||
FAutofillPendingHide := False;
|
FAutofillPendingHide := False;
|
||||||
FAutofillPendingUserOnly := False;
|
FAutofillPendingUserOnly := False;
|
||||||
|
FAutofillPendingClear := False;
|
||||||
|
|
||||||
TTimer(Sender).Enabled := False;
|
TTimer(Sender).Enabled := False;
|
||||||
TTimer(Sender).Free;
|
TTimer(Sender).Free;
|
||||||
@@ -1470,7 +1476,7 @@ begin
|
|||||||
// (quick-search beside the target), it stays visible during the fill; if it
|
// (quick-search beside the target), it stays visible during the fill; if it
|
||||||
// started hidden, the MinimizeToTray below re-hides it anyway.
|
// started hidden, the MinimizeToTray below re-hides it anyway.
|
||||||
var LFillOk := FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass,
|
var LFillOk := FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass,
|
||||||
UserOnly, not HideAfter);
|
UserOnly, not HideAfter, ClearFirst);
|
||||||
ForegroundAfter := GetForegroundWindow;
|
ForegroundAfter := GetForegroundWindow;
|
||||||
LogLine(Format('Autofill executed — target=%s, foreground_after=%s, ok=%s',
|
LogLine(Format('Autofill executed — target=%s, foreground_after=%s, ok=%s',
|
||||||
[IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8),
|
[IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8),
|
||||||
|
|||||||
+14
@@ -628,6 +628,20 @@
|
|||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>
|
||||||
|
Clear the field before typing
|
||||||
|
<small class="setting-hint">
|
||||||
|
Sends Ctrl+A + Del to empty the target field first.
|
||||||
|
Disable for apps where Ctrl+A isn't "select all"
|
||||||
|
(terminals, some remote desktops).
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="settingAutofillClear">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div id="settingAutofillHotkeysRow" style="margin-top:8px">
|
<div id="settingAutofillHotkeysRow" style="margin-top:8px">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<span style="font-size:12px;color:var(--text-dim)">
|
<span style="font-size:12px;color:var(--text-dim)">
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ const Bridge = (() => {
|
|||||||
cmd('cmd://autofill/execute?username=' + encodeURIComponent(username) +
|
cmd('cmd://autofill/execute?username=' + encodeURIComponent(username) +
|
||||||
'&password=' + encodeURIComponent(password) +
|
'&password=' + encodeURIComponent(password) +
|
||||||
(hideAfter ? '&hide_after=1' : '') +
|
(hideAfter ? '&hide_after=1' : '') +
|
||||||
(field === 'user' ? '&field=user' : ''));
|
(field === 'user' ? '&field=user' : '') +
|
||||||
|
(state.autofillClearField ? '' : '&clear=0'));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Ask Delphi to bring the main window to front (used when the
|
// Ask Delphi to bring the main window to front (used when the
|
||||||
@@ -643,6 +644,7 @@ const state = {
|
|||||||
quickUnlockEnabled: localStorage.getItem('quickUnlockEnabled') === '1',
|
quickUnlockEnabled: localStorage.getItem('quickUnlockEnabled') === '1',
|
||||||
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
||||||
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
||||||
|
autofillClearField: localStorage.getItem('autofillClearField') !== '0', // default ON
|
||||||
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
||||||
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
||||||
// 'F1'..'F12'). Default: Ctrl+Shift+L / Ctrl+Shift+P. Combos are
|
// 'F1'..'F12'). Default: Ctrl+Shift+L / Ctrl+Shift+P. Combos are
|
||||||
@@ -6341,6 +6343,7 @@ function openSettings() {
|
|||||||
$('#settingFaviconsRow').style.display = Bridge.active ? '' : 'none';
|
$('#settingFaviconsRow').style.display = Bridge.active ? '' : 'none';
|
||||||
$('#settingFaviconActionsRow').style.display = Bridge.active ? 'flex' : 'none';
|
$('#settingFaviconActionsRow').style.display = Bridge.active ? 'flex' : 'none';
|
||||||
$('#settingAutofill').checked = state.autofillEnabled;
|
$('#settingAutofill').checked = state.autofillEnabled;
|
||||||
|
$('#settingAutofillClear').checked = state.autofillClearField;
|
||||||
$('#settingAutofillRow').style.display = Bridge.active ? '' : 'none';
|
$('#settingAutofillRow').style.display = Bridge.active ? '' : 'none';
|
||||||
// Hotkey capture buttons — labels reflect current combos.
|
// Hotkey capture buttons — labels reflect current combos.
|
||||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
@@ -6750,6 +6753,9 @@ const SYNCED_SETTING_KEYS = [
|
|||||||
// Unlock method (pw / pin / both). The PIN blob itself is device-local
|
// Unlock method (pw / pin / both). The PIN blob itself is device-local
|
||||||
// DPAPI so this synced setting only carries the user's preferred mode.
|
// DPAPI so this synced setting only carries the user's preferred mode.
|
||||||
'unlockMode',
|
'unlockMode',
|
||||||
|
// Send Ctrl+A + Del before typing each autofill field (default ON).
|
||||||
|
// OFF for targets where Ctrl+A isn't select-all (terminals, RDP).
|
||||||
|
'autofillClearField',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sets `data-editor-position` on <body> so CSS can swap the slideover
|
// Sets `data-editor-position` on <body> so CSS can swap the slideover
|
||||||
@@ -6830,6 +6836,9 @@ async function loadServerSettings() {
|
|||||||
case 'unlockMode':
|
case 'unlockMode':
|
||||||
localStorage.setItem('unlockMode', String(v || 'pw'));
|
localStorage.setItem('unlockMode', String(v || 'pw'));
|
||||||
break;
|
break;
|
||||||
|
case 'autofillClearField':
|
||||||
|
localStorage.setItem('autofillClearField', v ? '1' : '0');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Apply visual settings immediately.
|
// Apply visual settings immediately.
|
||||||
@@ -7660,6 +7669,11 @@ async function init() {
|
|||||||
const lbl = autofillComboLabel(state.autofillHotkeyFull);
|
const lbl = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
toast(state.autofillEnabled ? ('Autofill enabled (' + lbl + ')') : 'Autofill disabled');
|
toast(state.autofillEnabled ? ('Autofill enabled (' + lbl + ')') : 'Autofill disabled');
|
||||||
});
|
});
|
||||||
|
$('#settingAutofillClear').addEventListener('change', e => {
|
||||||
|
state.autofillClearField = e.target.checked;
|
||||||
|
localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0');
|
||||||
|
saveServerSettings();
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Hotkey capture buttons ----
|
// ---- Hotkey capture buttons ----
|
||||||
// Click → button label becomes "Press combo…" → next keydown captures.
|
// Click → button label becomes "Press combo…" → next keydown captures.
|
||||||
|
|||||||
Reference in New Issue
Block a user