fix(app): browser pane keyboard tweaks
- Refresh icon: use single-arrow RotateCw instead of two-arrow RefreshCw - URL bar selects all text on focus - Cmd+R now reloads the active browser pane (tracked via IPC) when the URL bar has focus, instead of reloading the Electron window - Forward Paseo shortcut keys (b, e, w, t, k, /, \, comma, digits, enter, arrows) from the embedded webview to the main window so sidebar/tab/etc. shortcuts still fire when focus is inside the loaded page
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
createElement,
|
createElement,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Pressable, Text, TextInput, View } from "react-native";
|
import { Pressable, Text, TextInput, View } from "react-native";
|
||||||
import { ArrowLeft, ArrowRight, MousePointer2, RefreshCw } from "lucide-react-native";
|
import { ArrowLeft, ArrowRight, MousePointer2, RotateCw } from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import {
|
import {
|
||||||
buildWorkspaceAttachmentScopeKey,
|
buildWorkspaceAttachmentScopeKey,
|
||||||
@@ -336,13 +336,27 @@ export function BrowserPane({
|
|||||||
const updateBrowserRef = useRef(updateBrowser);
|
const updateBrowserRef = useRef(updateBrowser);
|
||||||
updateBrowserRef.current = updateBrowser;
|
updateBrowserRef.current = updateBrowser;
|
||||||
|
|
||||||
const focusUrlBar = useCallback(() => {
|
const selectUrlBar = useCallback(() => {
|
||||||
urlInputRef.current?.focus();
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
getTextInputNativeElement(urlInputRef.current)?.select();
|
getTextInputNativeElement(urlInputRef.current)?.select();
|
||||||
}, 0);
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const markActivePane = useCallback(() => {
|
||||||
|
if (!isElectronRuntime()) return;
|
||||||
|
void getDesktopHost()?.browser?.setActivePane?.(browserIdRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUrlBarFocus = useCallback(() => {
|
||||||
|
markActivePane();
|
||||||
|
selectUrlBar();
|
||||||
|
}, [markActivePane, selectUrlBar]);
|
||||||
|
|
||||||
|
const focusUrlBar = useCallback(() => {
|
||||||
|
urlInputRef.current?.focus();
|
||||||
|
selectUrlBar();
|
||||||
|
}, [selectUrlBar]);
|
||||||
|
|
||||||
const syncNavigationState = useCallback((input?: { syncUrl?: boolean }) => {
|
const syncNavigationState = useCallback((input?: { syncUrl?: boolean }) => {
|
||||||
const webview = webviewRef.current;
|
const webview = webviewRef.current;
|
||||||
if (!webview || !domReadyRef.current) {
|
if (!webview || !domReadyRef.current) {
|
||||||
@@ -466,6 +480,7 @@ export function BrowserPane({
|
|||||||
};
|
};
|
||||||
const handleWebviewFocus = () => {
|
const handleWebviewFocus = () => {
|
||||||
onFocusPane?.();
|
onFocusPane?.();
|
||||||
|
markActivePane();
|
||||||
};
|
};
|
||||||
|
|
||||||
webview.addEventListener("did-start-loading", handleStartLoading);
|
webview.addEventListener("did-start-loading", handleStartLoading);
|
||||||
@@ -937,7 +952,7 @@ export function BrowserPane({
|
|||||||
onPress={handleRefresh}
|
onPress={handleRefresh}
|
||||||
style={baseIconButtonStyle}
|
style={baseIconButtonStyle}
|
||||||
>
|
>
|
||||||
<RefreshCw size={16} color={theme.colors.foregroundMuted} />
|
<RotateCw size={16} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.urlBarWrap}>
|
<View style={styles.urlBarWrap}>
|
||||||
@@ -946,6 +961,7 @@ export function BrowserPane({
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
onChangeText={setDraftUrl}
|
onChangeText={setDraftUrl}
|
||||||
|
onFocus={handleUrlBarFocus}
|
||||||
onSubmitEditing={handleNavigateDraftUrl}
|
onSubmitEditing={handleNavigateDraftUrl}
|
||||||
placeholder="Enter URL"
|
placeholder="Enter URL"
|
||||||
placeholderTextColor={theme.colors.foregroundMuted}
|
placeholderTextColor={theme.colors.foregroundMuted}
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ export interface DesktopBrowserShortcutEvent {
|
|||||||
action: "focus-url";
|
action: "focus-url";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DesktopBrowserBridge {
|
||||||
|
setActivePane?: (browserId: string | null) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DesktopInvokeBridge {
|
export interface DesktopInvokeBridge {
|
||||||
invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
|
invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
@@ -88,6 +92,7 @@ export interface DesktopHostBridge {
|
|||||||
notification?: DesktopNotificationBridge;
|
notification?: DesktopNotificationBridge;
|
||||||
opener?: DesktopOpenerBridge;
|
opener?: DesktopOpenerBridge;
|
||||||
menu?: DesktopMenuBridge;
|
menu?: DesktopMenuBridge;
|
||||||
|
browser?: DesktopBrowserBridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { getShortcutOs } from "@/utils/shortcut-platform";
|
|||||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
||||||
import { isNative } from "@/constants/platform";
|
import { isNative } from "@/constants/platform";
|
||||||
|
import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
|
||||||
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
||||||
import { getRelativeSidebarShortcutTarget } from "@/utils/sidebar-shortcuts";
|
import { getRelativeSidebarShortcutTarget } from "@/utils/sidebar-shortcuts";
|
||||||
import { useActiveServerId } from "@/hooks/use-active-server-id";
|
import { useActiveServerId } from "@/hooks/use-active-server-id";
|
||||||
@@ -418,6 +419,26 @@ export function useKeyboardShortcuts({
|
|||||||
window.addEventListener("keyup", handleKeyUp, true);
|
window.addEventListener("keyup", handleKeyUp, true);
|
||||||
window.addEventListener("blur", handleBlurOrHide);
|
window.addEventListener("blur", handleBlurOrHide);
|
||||||
document.addEventListener("visibilitychange", handleBlurOrHide);
|
document.addEventListener("visibilitychange", handleBlurOrHide);
|
||||||
|
|
||||||
|
const forwardedKeySubscription = isElectronRuntime()
|
||||||
|
? getDesktopHost()?.events?.on?.("browser-forwarded-key", (payload) => {
|
||||||
|
if (!payload || typeof payload !== "object") return;
|
||||||
|
const p = payload as Record<string, unknown>;
|
||||||
|
if (typeof p.key !== "string") return;
|
||||||
|
window.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
key: p.key,
|
||||||
|
code: typeof p.code === "string" ? p.code : "",
|
||||||
|
metaKey: p.meta === true,
|
||||||
|
ctrlKey: p.control === true,
|
||||||
|
shiftKey: p.shift === true,
|
||||||
|
altKey: p.alt === true,
|
||||||
|
bubbles: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (chordStateRef.current.timeoutId !== null) {
|
if (chordStateRef.current.timeoutId !== null) {
|
||||||
clearTimeout(chordStateRef.current.timeoutId);
|
clearTimeout(chordStateRef.current.timeoutId);
|
||||||
@@ -431,6 +452,11 @@ export function useKeyboardShortcuts({
|
|||||||
window.removeEventListener("keyup", handleKeyUp, true);
|
window.removeEventListener("keyup", handleKeyUp, true);
|
||||||
window.removeEventListener("blur", handleBlurOrHide);
|
window.removeEventListener("blur", handleBlurOrHide);
|
||||||
document.removeEventListener("visibilitychange", handleBlurOrHide);
|
document.removeEventListener("visibilitychange", handleBlurOrHide);
|
||||||
|
if (typeof forwardedKeySubscription === "function") {
|
||||||
|
forwardedKeySubscription();
|
||||||
|
} else {
|
||||||
|
void forwardedKeySubscription?.then((dispose) => dispose());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
bindings,
|
bindings,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import type { WebContents } from "electron";
|
import { webContents as allWebContents, type WebContents } from "electron";
|
||||||
|
|
||||||
const browserIdsByWebContentsId = new Map<number, string>();
|
const browserIdsByWebContentsId = new Map<number, string>();
|
||||||
|
let activeBrowserPaneId: string | null = null;
|
||||||
|
|
||||||
export function registerPaseoBrowserWebContents(contents: WebContents, browserId: string): void {
|
export function registerPaseoBrowserWebContents(contents: WebContents, browserId: string): void {
|
||||||
browserIdsByWebContentsId.set(contents.id, browserId);
|
browserIdsByWebContentsId.set(contents.id, browserId);
|
||||||
contents.once("destroyed", () => {
|
contents.once("destroyed", () => {
|
||||||
browserIdsByWebContentsId.delete(contents.id);
|
browserIdsByWebContentsId.delete(contents.id);
|
||||||
|
if (activeBrowserPaneId === browserId) {
|
||||||
|
activeBrowserPaneId = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,3 +19,21 @@ export function getPaseoBrowserIdForWebContents(contents: WebContents | null): s
|
|||||||
}
|
}
|
||||||
return browserIdsByWebContentsId.get(contents.id) ?? null;
|
return browserIdsByWebContentsId.get(contents.id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setActivePaseoBrowserPaneId(browserId: string | null): void {
|
||||||
|
activeBrowserPaneId = browserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActivePaseoBrowserWebContents(): WebContents | null {
|
||||||
|
if (!activeBrowserPaneId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (const [contentsId, browserId] of browserIdsByWebContentsId) {
|
||||||
|
if (browserId !== activeBrowserPaneId) continue;
|
||||||
|
const contents = allWebContents.fromId(contentsId);
|
||||||
|
if (contents && !contents.isDestroyed()) {
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { app, Menu, BrowserWindow, ipcMain, webContents } from "electron";
|
import { app, Menu, BrowserWindow, ipcMain, webContents } from "electron";
|
||||||
import { getPaseoBrowserIdForWebContents } from "./browser-webviews.js";
|
import {
|
||||||
|
getActivePaseoBrowserWebContents,
|
||||||
|
getPaseoBrowserIdForWebContents,
|
||||||
|
} from "./browser-webviews.js";
|
||||||
|
|
||||||
interface ShowContextMenuInput {
|
interface ShowContextMenuInput {
|
||||||
kind?: "terminal";
|
kind?: "terminal";
|
||||||
@@ -20,8 +23,12 @@ function getFocusedPaseoBrowserWebContents(): Electron.WebContents | null {
|
|||||||
return getPaseoBrowserIdForWebContents(focusedContents) ? focusedContents : null;
|
return getPaseoBrowserIdForWebContents(focusedContents) ? focusedContents : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReloadTargetBrowserWebContents(): Electron.WebContents | null {
|
||||||
|
return getFocusedPaseoBrowserWebContents() ?? getActivePaseoBrowserWebContents();
|
||||||
|
}
|
||||||
|
|
||||||
function reloadFocusedContentsOrWindow(win: BrowserWindow, options?: { ignoreCache?: boolean }) {
|
function reloadFocusedContentsOrWindow(win: BrowserWindow, options?: { ignoreCache?: boolean }) {
|
||||||
const browserContents = getFocusedPaseoBrowserWebContents();
|
const browserContents = getReloadTargetBrowserWebContents();
|
||||||
if (browserContents) {
|
if (browserContents) {
|
||||||
if (options?.ignoreCache) {
|
if (options?.ignoreCache) {
|
||||||
browserContents.reloadIgnoringCache();
|
browserContents.reloadIgnoringCache();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { setupApplicationMenu } from "./features/menu.js";
|
|||||||
import {
|
import {
|
||||||
getPaseoBrowserIdForWebContents,
|
getPaseoBrowserIdForWebContents,
|
||||||
registerPaseoBrowserWebContents,
|
registerPaseoBrowserWebContents,
|
||||||
|
setActivePaseoBrowserPaneId,
|
||||||
} from "./features/browser-webviews.js";
|
} from "./features/browser-webviews.js";
|
||||||
import { parseOpenProjectPathFromArgv } from "./open-project-routing.js";
|
import { parseOpenProjectPathFromArgv } from "./open-project-routing.js";
|
||||||
import { getDesktopSettingsStore } from "./settings/desktop-settings-electron.js";
|
import { getDesktopSettingsStore } from "./settings/desktop-settings-electron.js";
|
||||||
@@ -76,6 +77,33 @@ function preventUnsafeBrowserWebviewNavigation(
|
|||||||
}
|
}
|
||||||
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
||||||
const BROWSER_SHORTCUT_EVENT = "paseo:event:browser-shortcut";
|
const BROWSER_SHORTCUT_EVENT = "paseo:event:browser-shortcut";
|
||||||
|
const BROWSER_FORWARDED_KEY_EVENT = "paseo:event:browser-forwarded-key";
|
||||||
|
|
||||||
|
const FORWARDED_PASEO_SHORTCUT_KEYS = new Set([
|
||||||
|
"b",
|
||||||
|
"e",
|
||||||
|
"w",
|
||||||
|
"t",
|
||||||
|
"k",
|
||||||
|
"/",
|
||||||
|
"\\",
|
||||||
|
",",
|
||||||
|
".",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"enter",
|
||||||
|
"arrowleft",
|
||||||
|
"arrowright",
|
||||||
|
"arrowup",
|
||||||
|
"arrowdown",
|
||||||
|
]);
|
||||||
const DESKTOP_SMOKE_ENV = "PASEO_DESKTOP_SMOKE";
|
const DESKTOP_SMOKE_ENV = "PASEO_DESKTOP_SMOKE";
|
||||||
const DESKTOP_SMOKE_STOP_REQUEST = "paseo-smoke-stop";
|
const DESKTOP_SMOKE_STOP_REQUEST = "paseo-smoke-stop";
|
||||||
app.setName("Paseo");
|
app.setName("Paseo");
|
||||||
@@ -105,6 +133,16 @@ function isBrowserLocationInput(input: Electron.Input): boolean {
|
|||||||
return (input.meta || input.control) && input.key.toLowerCase() === "l";
|
return (input.meta || input.control) && input.key.toLowerCase() === "l";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isForwardablePaseoShortcutInput(input: Electron.Input): boolean {
|
||||||
|
if (input.type !== "keyDown") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!input.meta && !input.control) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return FORWARDED_PASEO_SHORTCUT_KEYS.has(input.key.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
// In dev mode, detect git worktrees and isolate each instance so multiple
|
// In dev mode, detect git worktrees and isolate each instance so multiple
|
||||||
// Electron windows can run side-by-side (separate userData = separate lock).
|
// Electron windows can run side-by-side (separate userData = separate lock).
|
||||||
let devWorktreeName: string | null = null;
|
let devWorktreeName: string | null = null;
|
||||||
@@ -177,6 +215,10 @@ ipcMain.handle("paseo:get-pending-open-project", () => {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("paseo:browser:set-active-pane", (_event, browserId: unknown) => {
|
||||||
|
setActivePaseoBrowserPaneId(typeof browserId === "string" ? browserId : null);
|
||||||
|
});
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
||||||
]);
|
]);
|
||||||
@@ -317,6 +359,18 @@ async function createMainWindow(): Promise<void> {
|
|||||||
action: "focus-url",
|
action: "focus-url",
|
||||||
...(focusedBrowserId ? { browserId: focusedBrowserId } : {}),
|
...(focusedBrowserId ? { browserId: focusedBrowserId } : {}),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isForwardablePaseoShortcutInput(input)) {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.webContents.send(BROWSER_FORWARDED_KEY_EVENT, {
|
||||||
|
key: input.key,
|
||||||
|
code: input.code,
|
||||||
|
meta: input.meta,
|
||||||
|
control: input.control,
|
||||||
|
shift: input.shift,
|
||||||
|
alt: input.alt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
contents.setWindowOpenHandler(({ url }) => {
|
contents.setWindowOpenHandler(({ url }) => {
|
||||||
|
|||||||
@@ -57,4 +57,8 @@ contextBridge.exposeInMainWorld("paseoDesktop", {
|
|||||||
showContextMenu: (input?: Record<string, unknown>) =>
|
showContextMenu: (input?: Record<string, unknown>) =>
|
||||||
ipcRenderer.invoke("paseo:menu:showContextMenu", input),
|
ipcRenderer.invoke("paseo:menu:showContextMenu", input),
|
||||||
},
|
},
|
||||||
|
browser: {
|
||||||
|
setActivePane: (browserId: string | null) =>
|
||||||
|
ipcRenderer.invoke("paseo:browser:set-active-pane", browserId),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user