Many hotkey improvements

This commit is contained in:
Gregory Schier
2024-02-27 10:10:38 -08:00
parent 2705e90016
commit e12b85daae
8 changed files with 104 additions and 84 deletions

View File

@@ -20,7 +20,7 @@ interface Actions {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const DialogContext = createContext<State>({} as any); const DialogContext = createContext<State>({} as State);
export const DialogProvider = ({ children }: { children: React.ReactNode }) => { export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
const [dialogs, setDialogs] = useState<State['dialogs']>([]); const [dialogs, setDialogs] = useState<State['dialogs']>([]);

View File

@@ -119,6 +119,11 @@ export function GrpcConnectionSetupPane({
onGo(); onGo();
}, [activeRequest, onGo]); }, [activeRequest, onGo]);
const handleSend = useCallback(async () => {
if (activeRequest == null) return;
onSend({ message: activeRequest.message });
}, [activeRequest, onGo]);
const tabs: TabItem[] = useMemo( const tabs: TabItem[] = useMemo(
() => [ () => [
{ value: 'message', label: 'Message' }, { value: 'message', label: 'Message' },
@@ -212,52 +217,52 @@ export function GrpcConnectionSetupPane({
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'} {select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
</Button> </Button>
</RadioDropdown> </RadioDropdown>
{!isStreaming && ( {methodType === 'client_streaming' || methodType === 'streaming' ? (
<>
{isStreaming && (
<>
<IconButton
className="border border-highlight"
size="sm"
title="Cancel"
onClick={onCancel}
icon="x"
/>
<IconButton
className="border border-highlight"
size="sm"
title="Commit"
onClick={onCommit}
icon="check"
/>
</>
)}
<IconButton
className="border border-highlight"
size="sm"
title={isStreaming ? 'Connect' : 'Send'}
hotkeyAction="grpc_request.send"
onClick={isStreaming ? handleSend : handleConnect}
icon={isStreaming ? 'sendHorizontal' : 'arrowUpDown'}
/>
</>
) : (
<IconButton <IconButton
className="border border-highlight" className="border border-highlight"
size="sm" size="sm"
title={methodType === 'unary' ? 'Send' : 'Connect'} title={methodType === 'unary' ? 'Send' : 'Connect'}
hotkeyAction="grpc_request.send" hotkeyAction="grpc_request.send"
onClick={handleConnect} onClick={isStreaming ? onCancel : handleConnect}
disabled={methodType === 'no-schema' || methodType === 'no-method'} disabled={methodType === 'no-schema' || methodType === 'no-method'}
icon={ icon={
isStreaming isStreaming
? 'refresh' ? 'x'
: methodType.includes('streaming') : methodType.includes('streaming')
? 'arrowUpDown' ? 'arrowUpDown'
: 'sendHorizontal' : 'sendHorizontal'
} }
/> />
)} )}
{isStreaming && (
<IconButton
className="border border-highlight"
size="sm"
title="Cancel"
onClick={onCancel}
icon="x"
disabled={!isStreaming}
/>
)}
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
<>
<IconButton
className="border border-highlight"
size="sm"
title="to-do"
onClick={onCommit}
icon="check"
/>
<IconButton
className="border border-highlight"
size="sm"
title="to-do"
hotkeyAction="grpc_request.send"
onClick={() => onSend({ message: activeRequest.message ?? '' })}
icon="sendHorizontal"
/>
</>
)}
</HStack> </HStack>
</div> </div>
<Tabs <Tabs

View File

@@ -90,13 +90,17 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC, namespace: NAMESPACE_NO_SYNC,
}); });
useHotKey('http_request.duplicate', async () => { useHotKey(
if (activeRequest?.model === 'http_request') { 'http_request.duplicate',
await duplicateHttpRequest.mutateAsync(); async () => {
} else { if (activeRequest?.model === 'http_request') {
await duplicateGrpcRequest.mutateAsync(); await duplicateHttpRequest.mutateAsync();
} } else {
}); await duplicateGrpcRequest.mutateAsync();
}
},
{ enable: !hidden },
);
const isCollapsed = useCallback( const isCollapsed = useCallback(
(id: string) => collapsed.value?.[id] ?? false, (id: string) => collapsed.value?.[id] ?? false,
@@ -242,25 +246,29 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Backspace', handleDeleteKey); useKeyPressEvent('Backspace', handleDeleteKey);
useKeyPressEvent('Delete', handleDeleteKey); useKeyPressEvent('Delete', handleDeleteKey);
useHotKey('sidebar.focus', async () => { useHotKey(
// Hide the sidebar if it's already focused 'sidebar.focus',
if (!hidden && hasFocus) { async () => {
await hide(); // Hide the sidebar if it's already focused
return; if (!hidden && hasFocus) {
} await hide();
return;
}
// Show the sidebar if it's hidden // Show the sidebar if it's hidden
if (hidden) { if (hidden) {
await show(); await show();
} }
// Select 0 index on focus if none selected // Select 0 index on focus if none selected
focusActiveRequest( focusActiveRequest(
selectedTree != null && selectedId != null selectedTree != null && selectedId != null
? { forced: { id: selectedId, tree: selectedTree } } ? { forced: { id: selectedId, tree: selectedTree } }
: undefined, : undefined,
); );
}); },
{ enable: !hidden },
);
useKeyPressEvent('Enter', (e) => { useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return; if (!hasFocus) return;

View File

@@ -2,12 +2,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import type { Folder } from '../lib/models'; import type { Folder } from '../lib/models';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { foldersQueryKey } from './useFolders'; import { foldersQueryKey } from './useFolders';
import { usePrompt } from './usePrompt'; import { usePrompt } from './usePrompt';
export function useCreateFolder() { export function useCreateFolder() {
const workspaceId = useActiveWorkspaceId(); const workspaceId = useActiveWorkspaceId();
const activeRequest = useActiveRequest();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const prompt = usePrompt(); const prompt = usePrompt();
@@ -28,6 +30,7 @@ export function useCreateFolder() {
placeholder: 'Name', placeholder: 'Name',
})); }));
patch.sortPriority = patch.sortPriority || -Date.now(); patch.sortPriority = patch.sortPriority || -Date.now();
patch.folderId = patch.folderId || activeRequest?.folderId;
return invoke('cmd_create_folder', { workspaceId, ...patch }); return invoke('cmd_create_folder', { workspaceId, ...patch });
}, },
onSettled: () => trackEvent('folder', 'create'), onSettled: () => trackEvent('folder', 'create'),

View File

@@ -3,13 +3,14 @@ import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import type { GrpcRequest } from '../lib/models'; import type { GrpcRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes'; import { useAppRoutes } from './useAppRoutes';
export function useCreateGrpcRequest() { export function useCreateGrpcRequest() {
const workspaceId = useActiveWorkspaceId(); const workspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId(); const activeEnvironmentId = useActiveEnvironmentId();
const activeRequest = null; const activeRequest = useActiveRequest();
const routes = useAppRoutes(); const routes = useAppRoutes();
return useMutation< return useMutation<
@@ -24,13 +25,13 @@ export function useCreateGrpcRequest() {
if (patch.sortPriority === undefined) { if (patch.sortPriority === undefined) {
if (activeRequest != null) { if (activeRequest != null) {
// Place above currently-active request // Place above currently-active request
// patch.sortPriority = activeRequest.sortPriority + 0.0001; patch.sortPriority = activeRequest.sortPriority + 0.0001;
} else { } else {
// Place at the very top // Place at the very top
patch.sortPriority = -Date.now(); patch.sortPriority = -Date.now();
} }
} }
// patch.folderId = patch.folderId; // TODO: || activeRequest?.folderId; patch.folderId = patch.folderId || activeRequest?.folderId;
return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch }); return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch });
}, },
onSettled: () => trackEvent('grpc_request', 'create'), onSettled: () => trackEvent('grpc_request', 'create'),

View File

@@ -38,7 +38,7 @@ export function useGrpc(
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'), mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
}); });
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000); const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 500);
const reflect = useQuery<ReflectResponseService[], string>({ const reflect = useQuery<ReflectResponseService[], string>({
enabled: req != null, enabled: req != null,
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl], queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl],

View File

@@ -4,6 +4,8 @@ import { capitalize } from '../lib/capitalize';
import { debounce } from '../lib/debounce'; import { debounce } from '../lib/debounce';
import { useOsInfo } from './useOsInfo'; import { useOsInfo } from './useOsInfo';
const HOLD_KEYS = ['Shift', 'CmdCtrl', 'Alt', 'Meta'];
export type HotkeyAction = export type HotkeyAction =
| 'popup.close' | 'popup.close'
| 'environmentEditor.toggle' | 'environmentEditor.toggle'
@@ -61,21 +63,6 @@ export function useHotKey(
action: HotkeyAction | null, action: HotkeyAction | null,
callback: (e: KeyboardEvent) => void, callback: (e: KeyboardEvent) => void,
options: Options = {}, options: Options = {},
) {
useAnyHotkey((hkAction, e) => {
// Triggered hotkey!
if (hkAction === action) {
e.preventDefault();
e.stopPropagation();
console.log('TRIGGERED HOTKEY', hkAction, options);
callback(e);
}
}, options);
}
function useAnyHotkey(
callback: (action: HotkeyAction, e: KeyboardEvent) => void,
options: Options,
) { ) {
const currentKeys = useRef<Set<string>>(new Set()); const currentKeys = useRef<Set<string>>(new Set());
const callbackRef = useRef(callback); const callbackRef = useRef(callback);
@@ -87,7 +74,7 @@ function useAnyHotkey(
}, [callback]); }, [callback]);
useEffect(() => { useEffect(() => {
// Sometimes the keyup event doesn't fire, so we clear the keys after a timeout // Sometimes the keyup event doesn't fire (eg, cmd+Tab), so we clear the keys after a timeout
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000); const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000);
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
@@ -95,26 +82,41 @@ function useAnyHotkey(
return; return;
} }
currentKeys.current.add(normalizeKey(e.key, os)); const key = normalizeKey(e.key, os);
currentKeys.current.add(normalizeKey(key, os));
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) { for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
for (const hkKey of hkKeys) { for (const hkKey of hkKeys) {
const keys = hkKey.split('+'); const keys = hkKey.split('+');
if ( if (
keys.length === currentKeys.current.size && keys.length === currentKeys.current.size &&
keys.every((key) => currentKeys.current.has(key)) keys.every((key) => currentKeys.current.has(key)) &&
hkAction === action
) { ) {
callbackRef.current(hkAction, e); e.preventDefault();
e.stopPropagation();
callbackRef.current(e);
} }
} }
} }
clearCurrentKeys(); clearCurrentKeys();
}; };
const up = (e: KeyboardEvent) => { const up = (e: KeyboardEvent) => {
if (options.enable === false) { if (options.enable === false) {
return; return;
} }
currentKeys.current.delete(normalizeKey(e.key, os)); const key = normalizeKey(e.key, os);
currentKeys.current.delete(normalizeKey(key, os));
// Clear all keys if no longer holding modifier
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;
// As you see, the ":" is not removed because it turned into ";" when shift was released
const isHoldingModifier = HOLD_KEYS.some((k) => currentKeys.current.has(k));
if (!isHoldingModifier) {
currentKeys.current.clear();
}
}; };
document.addEventListener('keydown', down, { capture: true }); document.addEventListener('keydown', down, { capture: true });
document.addEventListener('keyup', up, { capture: true }); document.addEventListener('keyup', up, { capture: true });

View File

@@ -20,9 +20,10 @@ if (osType !== 'Darwin') {
const settings = await getSettings(); const settings = await getSettings();
setAppearanceOnDocument(settings.appearance as Appearance); setAppearanceOnDocument(settings.appearance as Appearance);
window.addEventListener('keypress', (e) => { window.addEventListener('keydown', (e) => {
// Don't go back in history on backspace // Hack to not go back in history on backspace. Check for document body
if (e.key === 'Backspace') e.preventDefault(); // or else it will prevent backspace in input fields.
if (e.key === 'Backspace' && e.target === document.body) e.preventDefault();
}); });
createRoot(document.getElementById('root') as HTMLElement).render( createRoot(document.getElementById('root') as HTMLElement).render(