emoemu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warning Dialog Component
|
|
3
|
+
*
|
|
4
|
+
* A general-purpose warning dialog that displays a message and allows
|
|
5
|
+
* the user to choose between continuing (OK) or exiting the application.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Box, Text } from 'ink';
|
|
9
|
+
import { useDialogNavigation } from '../hooks/useDialogNavigation';
|
|
10
|
+
import { DialogContainer } from '../DialogContainer';
|
|
11
|
+
import { launchDialog, type DialogRenderOptions } from '../NativeDialog';
|
|
12
|
+
import { DIALOG_BOX_PADDING } from '..';
|
|
13
|
+
|
|
14
|
+
export type WarningChoice = 'ok' | 'exit';
|
|
15
|
+
|
|
16
|
+
interface WarningDialogProps {
|
|
17
|
+
message: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
onChoice: (choice: WarningChoice) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const WarningDialog = ({ message, title = 'Warning', onChoice }: WarningDialogProps) => {
|
|
23
|
+
const options: { label: string; choice: WarningChoice; color: string }[] = [
|
|
24
|
+
{ label: 'OK', choice: 'ok', color: 'green' },
|
|
25
|
+
{ label: 'Exit', choice: 'exit', color: 'red' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const { selectedIndex } = useDialogNavigation({
|
|
29
|
+
itemCount: options.length,
|
|
30
|
+
onSelect: (index) => onChoice(options[index].choice),
|
|
31
|
+
onCancel: () => onChoice('ok'),
|
|
32
|
+
horizontal: true,
|
|
33
|
+
spaceToSelect: true,
|
|
34
|
+
onCtrlC: () => onChoice('exit'),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DialogContainer>
|
|
39
|
+
{(boxWidth) => (
|
|
40
|
+
<>
|
|
41
|
+
{/* Main Dialog Box */}
|
|
42
|
+
<Box
|
|
43
|
+
flexDirection="column"
|
|
44
|
+
borderStyle="round"
|
|
45
|
+
borderColor="yellow"
|
|
46
|
+
paddingX={2}
|
|
47
|
+
paddingY={1}
|
|
48
|
+
width={boxWidth}
|
|
49
|
+
>
|
|
50
|
+
{/* Header with warning icon */}
|
|
51
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
52
|
+
<Text bold color="yellow">{'\u26A0'} {title}</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
|
|
55
|
+
{/* Message */}
|
|
56
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
57
|
+
<Text wrap="wrap">{message}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
|
|
60
|
+
{/* Separator */}
|
|
61
|
+
<Box justifyContent="center" marginY={1}>
|
|
62
|
+
<Text color="gray" dimColor>{'─'.repeat(boxWidth - DIALOG_BOX_PADDING - 2)}</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
|
|
65
|
+
{/* Buttons - horizontal layout */}
|
|
66
|
+
<Box justifyContent="center" gap={2}>
|
|
67
|
+
{options.map((option, index) => (
|
|
68
|
+
<Box
|
|
69
|
+
key={option.choice}
|
|
70
|
+
paddingX={2}
|
|
71
|
+
borderStyle={selectedIndex === index ? 'round' : 'single'}
|
|
72
|
+
borderColor={selectedIndex === index ? option.color : 'gray'}
|
|
73
|
+
>
|
|
74
|
+
<Text
|
|
75
|
+
color={selectedIndex === index ? option.color : 'gray'}
|
|
76
|
+
bold={selectedIndex === index}
|
|
77
|
+
>
|
|
78
|
+
{option.label}
|
|
79
|
+
</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
))}
|
|
82
|
+
</Box>
|
|
83
|
+
</Box>
|
|
84
|
+
|
|
85
|
+
{/* Footer */}
|
|
86
|
+
<Box marginTop={1}>
|
|
87
|
+
<Text color="gray" dimColor>
|
|
88
|
+
{'\u2190\u2192'} Navigate {'\u23CE'}/Space Select ESC Continue
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
</DialogContainer>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Show the warning dialog and get user's choice
|
|
99
|
+
*
|
|
100
|
+
* @param message - The warning message to display
|
|
101
|
+
* @param options - Dialog render options (native mode, title, etc.)
|
|
102
|
+
* @returns 'ok' if user chooses to continue, 'exit' if user chooses to exit
|
|
103
|
+
*/
|
|
104
|
+
export const showWarningDialog = (
|
|
105
|
+
message: string,
|
|
106
|
+
options: DialogRenderOptions & { title?: string } = {}
|
|
107
|
+
): Promise<WarningChoice> => launchDialog<WarningChoice>(
|
|
108
|
+
(onChoice) => <WarningDialog message={message} title={options.title} onChoice={onChoice} />,
|
|
109
|
+
'ok',
|
|
110
|
+
{ ...options, title: options.title ?? 'emoemu - Warning' },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
export default WarningDialog;
|
package/src/ui/consts.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Terminal Dimensions
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
/** Default terminal dimensions */
|
|
6
|
+
export const DEFAULT_TERM_WIDTH = 80;
|
|
7
|
+
export const DEFAULT_TERM_HEIGHT = 24;
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Timing
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/** Delay for stdin settling (ms) */
|
|
14
|
+
export const STDIN_SETTLE_DELAY_MS = 100;
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Dialog
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/** Dialog box padding (used by multiple dialogs) */
|
|
21
|
+
export const DIALOG_BOX_PADDING = 4;
|
|
22
|
+
|
|
23
|
+
/** Default minimum width for dialog boxes */
|
|
24
|
+
export const DIALOG_BOX_MIN_WIDTH = 50;
|
|
25
|
+
|
|
26
|
+
/** Padding subtracted from box width when rendering separator lines */
|
|
27
|
+
export const SEPARATOR_LINE_PADDING = 6;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to clear the terminal on mount and trigger a re-render.
|
|
3
|
+
*
|
|
4
|
+
* When a component clears the terminal in useEffect, Ink doesn't know the
|
|
5
|
+
* screen was cleared, so it won't re-render until user input. This hook
|
|
6
|
+
* handles both clearing and forcing a re-render so content displays immediately.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
10
|
+
import { CLEAR_TERMINAL_SEQUENCE } from './consts';
|
|
11
|
+
|
|
12
|
+
export * from './consts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Clear the terminal on mount and return whether the component is ready to render.
|
|
16
|
+
*
|
|
17
|
+
* @returns true after the terminal has been cleared and re-render triggered
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const MyComponent = () => {
|
|
21
|
+
* const ready = useClearTerminal();
|
|
22
|
+
* if (!ready) return null;
|
|
23
|
+
* return <Box>Content</Box>;
|
|
24
|
+
* };
|
|
25
|
+
*/
|
|
26
|
+
export const useClearTerminal = (): boolean => {
|
|
27
|
+
const [ready, setReady] = useState(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Clear screen, move cursor to home position, and hide cursor
|
|
31
|
+
process.stdout.write(CLEAR_TERMINAL_SEQUENCE);
|
|
32
|
+
// Trigger re-render so Ink draws the component
|
|
33
|
+
setReady(true);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return ready;
|
|
37
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog Navigation Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages selectedIndex state, keyboard (useInput), and gamepad navigation
|
|
5
|
+
* for dialog components with a list of selectable options.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { useInput, useApp } from 'ink';
|
|
10
|
+
import { useGamepad } from '../useGamepad';
|
|
11
|
+
|
|
12
|
+
interface UseDialogNavigationOptions {
|
|
13
|
+
/** Number of selectable items */
|
|
14
|
+
itemCount: number;
|
|
15
|
+
/** Called when the user confirms a selection (Enter, number key, gamepad A) */
|
|
16
|
+
onSelect: (index: number) => void;
|
|
17
|
+
/** Called when the user cancels (ESC, gamepad B) */
|
|
18
|
+
onCancel: () => void;
|
|
19
|
+
/** Enable left/right arrow navigation in addition to up/down */
|
|
20
|
+
horizontal?: boolean;
|
|
21
|
+
/** Accept space bar as confirmation (in addition to Enter) */
|
|
22
|
+
spaceToSelect?: boolean;
|
|
23
|
+
/** Custom CTRL-C handler; if omitted, CTRL-C is not specially handled */
|
|
24
|
+
onCtrlC?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook that provides standard dialog navigation: up/down arrows, Enter to select,
|
|
29
|
+
* ESC to cancel, number shortcuts, and gamepad support.
|
|
30
|
+
*/
|
|
31
|
+
export const useDialogNavigation = ({
|
|
32
|
+
itemCount,
|
|
33
|
+
onSelect,
|
|
34
|
+
onCancel,
|
|
35
|
+
horizontal = false,
|
|
36
|
+
spaceToSelect = false,
|
|
37
|
+
onCtrlC,
|
|
38
|
+
}: UseDialogNavigationOptions) => {
|
|
39
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
|
|
42
|
+
const selectAndExit = (index: number) => {
|
|
43
|
+
onSelect(index);
|
|
44
|
+
exit();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const cancelAndExit = () => {
|
|
48
|
+
onCancel();
|
|
49
|
+
exit();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
useInput((input, key) => {
|
|
53
|
+
// Optional CTRL-C handling
|
|
54
|
+
if (onCtrlC && (input === '\x03' || (key.ctrl && input === 'c'))) {
|
|
55
|
+
onCtrlC();
|
|
56
|
+
exit();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (key.escape) {
|
|
61
|
+
cancelAndExit();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (key.upArrow || (horizontal && key.leftArrow)) {
|
|
66
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (key.downArrow || (horizontal && key.rightArrow)) {
|
|
71
|
+
setSelectedIndex(prev => Math.min(itemCount - 1, prev + 1));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (key.return || (spaceToSelect && input === ' ')) {
|
|
76
|
+
selectAndExit(selectedIndex);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Number shortcuts (1-based)
|
|
81
|
+
const num = parseInt(input, 10);
|
|
82
|
+
if (num >= 1 && num <= itemCount) {
|
|
83
|
+
selectAndExit(num - 1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
useGamepad({
|
|
88
|
+
onUp: () => setSelectedIndex(prev => Math.max(0, prev - 1)),
|
|
89
|
+
onDown: () => setSelectedIndex(prev => Math.min(itemCount - 1, prev + 1)),
|
|
90
|
+
...(horizontal && {
|
|
91
|
+
onLeft: () => setSelectedIndex(prev => Math.max(0, prev - 1)),
|
|
92
|
+
onRight: () => setSelectedIndex(prev => Math.min(itemCount - 1, prev + 1)),
|
|
93
|
+
}),
|
|
94
|
+
onConfirm: () => selectAndExit(selectedIndex),
|
|
95
|
+
onCancel: cancelAndExit,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return { selectedIndex, setSelectedIndex };
|
|
99
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Gamepad acceleration settings for held direction buttons
|
|
2
|
+
|
|
3
|
+
/** Delay before repeating starts (ms) */
|
|
4
|
+
export const INITIAL_DELAY_MS = 400;
|
|
5
|
+
|
|
6
|
+
/** Starting repeat interval (ms) */
|
|
7
|
+
export const INITIAL_REPEAT_MS = 200;
|
|
8
|
+
|
|
9
|
+
/** Fastest repeat interval (ms) */
|
|
10
|
+
export const MIN_REPEAT_MS = 40;
|
|
11
|
+
|
|
12
|
+
/** Time to reach max speed (ms) */
|
|
13
|
+
export const ACCELERATION_TIME_MS = 1500;
|
|
14
|
+
|
|
15
|
+
// Ease-in-out curve coefficients (smoothstep function: 3t^2 - 2t^3)
|
|
16
|
+
|
|
17
|
+
/** Smoothstep cubic factor */
|
|
18
|
+
export const EASE_CUBIC_FACTOR = 3;
|
|
19
|
+
|
|
20
|
+
/** Smoothstep cubic divisor */
|
|
21
|
+
export const EASE_CUBIC_DIVISOR = 2;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamepad Hook for UI Components
|
|
3
|
+
*
|
|
4
|
+
* Provides gamepad navigation support for Ink-based UI components.
|
|
5
|
+
* Handles Up/Down for navigation, A for confirm, B for cancel.
|
|
6
|
+
* Directional inputs support accelerating repeat when held.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef } from 'react';
|
|
10
|
+
import { GamepadManager } from '@/input/GamepadManager';
|
|
11
|
+
import { StandardButton } from '@/core/button';
|
|
12
|
+
import {
|
|
13
|
+
INITIAL_DELAY_MS,
|
|
14
|
+
INITIAL_REPEAT_MS,
|
|
15
|
+
MIN_REPEAT_MS,
|
|
16
|
+
ACCELERATION_TIME_MS,
|
|
17
|
+
EASE_CUBIC_FACTOR,
|
|
18
|
+
EASE_CUBIC_DIVISOR,
|
|
19
|
+
} from './consts';
|
|
20
|
+
|
|
21
|
+
export * from './consts';
|
|
22
|
+
|
|
23
|
+
export interface GamepadCallbacks {
|
|
24
|
+
onUp?: () => void;
|
|
25
|
+
onDown?: () => void;
|
|
26
|
+
onLeft?: () => void;
|
|
27
|
+
onRight?: () => void;
|
|
28
|
+
onConfirm?: () => void; // A button
|
|
29
|
+
onCancel?: () => void; // B button
|
|
30
|
+
onStart?: () => void; // Start button
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Direction = 'up' | 'down' | 'left' | 'right';
|
|
34
|
+
|
|
35
|
+
const DIRECTION_CALLBACKS: Record<Direction, keyof Pick<GamepadCallbacks, 'onUp' | 'onDown' | 'onLeft' | 'onRight'>> = {
|
|
36
|
+
up: 'onUp',
|
|
37
|
+
down: 'onDown',
|
|
38
|
+
left: 'onLeft',
|
|
39
|
+
right: 'onRight',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const fireDirectionalCallback = (callbacks: GamepadCallbacks, direction: Direction): void => {
|
|
43
|
+
callbacks[DIRECTION_CALLBACKS[direction]]?.();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface RepeatState {
|
|
47
|
+
direction: Direction;
|
|
48
|
+
startTime: number;
|
|
49
|
+
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook for gamepad input in UI components.
|
|
54
|
+
* Automatically manages GamepadManager lifecycle.
|
|
55
|
+
* Directional buttons accelerate when held.
|
|
56
|
+
*
|
|
57
|
+
* @param callbacks Object with callback functions for different inputs
|
|
58
|
+
* @param enabled Whether gamepad input is enabled (default: true)
|
|
59
|
+
*/
|
|
60
|
+
export const useGamepad = (callbacks: GamepadCallbacks, enabled: boolean = true): void => {
|
|
61
|
+
const managerRef = useRef<GamepadManager | null>(null);
|
|
62
|
+
const callbacksRef = useRef(callbacks);
|
|
63
|
+
const repeatStateRef = useRef<RepeatState | null>(null);
|
|
64
|
+
|
|
65
|
+
// Keep callbacks ref up to date
|
|
66
|
+
callbacksRef.current = callbacks;
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!enabled) {return;}
|
|
70
|
+
|
|
71
|
+
// Calculate repeat interval based on how long button has been held
|
|
72
|
+
const getRepeatInterval = (heldDuration: number): number => {
|
|
73
|
+
if (heldDuration < INITIAL_DELAY_MS) {
|
|
74
|
+
return INITIAL_DELAY_MS - heldDuration;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Calculate acceleration progress (0 to 1)
|
|
78
|
+
const accelerationProgress = Math.min(
|
|
79
|
+
1,
|
|
80
|
+
(heldDuration - INITIAL_DELAY_MS) / ACCELERATION_TIME_MS
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Ease-in-out curve for smooth acceleration (smoothstep)
|
|
84
|
+
const easedProgress = accelerationProgress * accelerationProgress * (EASE_CUBIC_FACTOR - EASE_CUBIC_DIVISOR * accelerationProgress);
|
|
85
|
+
|
|
86
|
+
// Interpolate between initial and minimum repeat interval
|
|
87
|
+
return INITIAL_REPEAT_MS - (INITIAL_REPEAT_MS - MIN_REPEAT_MS) * easedProgress;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Fire callback for direction and schedule next repeat
|
|
91
|
+
const fireAndSchedule = (direction: Direction) => {
|
|
92
|
+
const cb = callbacksRef.current;
|
|
93
|
+
const state = repeatStateRef.current;
|
|
94
|
+
|
|
95
|
+
if (!state || state.direction !== direction) {return;}
|
|
96
|
+
|
|
97
|
+
// Fire the appropriate callback
|
|
98
|
+
fireDirectionalCallback(cb, direction);
|
|
99
|
+
|
|
100
|
+
// Schedule next repeat with accelerated interval
|
|
101
|
+
const heldDuration = Date.now() - state.startTime;
|
|
102
|
+
const interval = getRepeatInterval(heldDuration);
|
|
103
|
+
|
|
104
|
+
state.timeoutId = setTimeout(() => fireAndSchedule(direction), interval);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Start repeat for a direction
|
|
108
|
+
const startRepeat = (direction: Direction) => {
|
|
109
|
+
// Cancel any existing repeat
|
|
110
|
+
stopRepeat();
|
|
111
|
+
|
|
112
|
+
// Fire callback immediately
|
|
113
|
+
fireDirectionalCallback(callbacksRef.current, direction);
|
|
114
|
+
|
|
115
|
+
// Start repeat state
|
|
116
|
+
repeatStateRef.current = {
|
|
117
|
+
direction,
|
|
118
|
+
startTime: Date.now(),
|
|
119
|
+
timeoutId: setTimeout(() => fireAndSchedule(direction), INITIAL_DELAY_MS),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Stop any active repeat
|
|
124
|
+
const stopRepeat = () => {
|
|
125
|
+
const state = repeatStateRef.current;
|
|
126
|
+
if (state?.timeoutId) {
|
|
127
|
+
clearTimeout(state.timeoutId);
|
|
128
|
+
}
|
|
129
|
+
repeatStateRef.current = null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Map button to direction
|
|
133
|
+
const buttonToDirection = (button: StandardButton): Direction | null => {
|
|
134
|
+
switch (button) {
|
|
135
|
+
case StandardButton.Up:
|
|
136
|
+
case StandardButton.LeftStickUp:
|
|
137
|
+
return 'up';
|
|
138
|
+
case StandardButton.Down:
|
|
139
|
+
case StandardButton.LeftStickDown:
|
|
140
|
+
return 'down';
|
|
141
|
+
case StandardButton.Left:
|
|
142
|
+
case StandardButton.LeftStickLeft:
|
|
143
|
+
return 'left';
|
|
144
|
+
case StandardButton.Right:
|
|
145
|
+
case StandardButton.LeftStickRight:
|
|
146
|
+
return 'right';
|
|
147
|
+
default:
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Create and start the gamepad manager
|
|
153
|
+
const manager = new GamepadManager();
|
|
154
|
+
managerRef.current = manager;
|
|
155
|
+
|
|
156
|
+
manager.onButtonChange = (_port, button, pressed) => {
|
|
157
|
+
const cb = callbacksRef.current;
|
|
158
|
+
const direction = buttonToDirection(button);
|
|
159
|
+
|
|
160
|
+
if (direction) {
|
|
161
|
+
// Directional buttons with acceleration
|
|
162
|
+
if (pressed) {
|
|
163
|
+
startRepeat(direction);
|
|
164
|
+
} else {
|
|
165
|
+
// Only stop if this is the direction currently being held
|
|
166
|
+
if (repeatStateRef.current?.direction === direction) {
|
|
167
|
+
stopRepeat();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if (pressed) {
|
|
171
|
+
// Non-directional buttons: fire on press only
|
|
172
|
+
switch (button) {
|
|
173
|
+
case StandardButton.A:
|
|
174
|
+
cb.onConfirm?.();
|
|
175
|
+
break;
|
|
176
|
+
case StandardButton.B:
|
|
177
|
+
cb.onCancel?.();
|
|
178
|
+
break;
|
|
179
|
+
case StandardButton.Start:
|
|
180
|
+
cb.onStart?.();
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
manager.start();
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
stopRepeat();
|
|
190
|
+
manager.stop();
|
|
191
|
+
managerRef.current = null;
|
|
192
|
+
};
|
|
193
|
+
}, [enabled]);
|
|
194
|
+
};
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Module Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './consts';
|
|
6
|
+
export { launchBrowser, importDirectory } from './App';
|
|
7
|
+
export type { BrowserResult, NetplayOptions } from './App';
|
|
8
|
+
export { selectCore } from './CoreSelector';
|
|
9
|
+
export type { CoreSelection } from './CoreSelector';
|
|
10
|
+
export { showSaveStateDialog, showCorruptedStateDialog } from './SaveStateDialog';
|
|
11
|
+
export type { SaveStateInfo, SaveStateChoice, CorruptedStateInfo, CorruptedStateChoice } from './SaveStateDialog';
|
|
12
|
+
export { showNetplayDisconnectedDialog } from './NetplayDisconnectedDialog';
|
|
13
|
+
export type { DisconnectInfo, DisconnectChoice } from './NetplayDisconnectedDialog';
|
|
14
|
+
export { showWarningDialog } from './WarningDialog';
|
|
15
|
+
export type { WarningChoice } from './WarningDialog';
|
|
16
|
+
export { scanDirectory, groupBySystem, validateRomFile } from '../frontend/romScanner';
|
|
17
|
+
export type { RomInfo, RomMetadata, ValidateRomResult } from '../frontend/romScanner';
|
|
18
|
+
export { GamepadProvider, useGamepadContext } from './GamepadContext';
|
|
19
|
+
export type { GamepadCallbacks } from './GamepadContext';
|
|
20
|
+
export { AppCapabilitiesProvider, useAppCapabilities, useKittyGraphicsSupported, useNativeSupported } from './AppCapabilities';
|
|
21
|
+
export type { AppCapabilities } from './AppCapabilities';
|
|
22
|
+
export { ConfigProvider, useConfig } from './ConfigContext';
|
|
23
|
+
export { showDuplicateCrcPrompt } from './DuplicateCrcPrompt';
|
|
24
|
+
export type { DuplicateCrcInfo, DuplicateCrcChoice } from './DuplicateCrcPrompt';
|
|
25
|
+
export type { DialogRenderOptions } from './NativeDialog';
|
|
26
|
+
export { useClearTerminal } from './hooks/useClearTerminal';
|
|
27
|
+
export { useGamepad, type GamepadCallbacks as HookGamepadCallbacks } from './hooks/useGamepad';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Bit shift for second byte */
|
|
2
|
+
export const BYTE_SHIFT_1 = 8;
|
|
3
|
+
|
|
4
|
+
/** Center value for unsigned 8-bit analog input (0-255 range) */
|
|
5
|
+
export const ANALOG_CENTER_8BIT = 128;
|
|
6
|
+
|
|
7
|
+
/** Default deadzone for 8-bit analog input */
|
|
8
|
+
export const ANALOG_DEADZONE_8BIT = 50;
|
|
9
|
+
|
|
10
|
+
/** Default deadzone for signed 16-bit analog input */
|
|
11
|
+
export const ANALOG_DEADZONE_SIGNED = 8000;
|
|
12
|
+
|
|
13
|
+
/** Maximum positive value for signed 16-bit integer */
|
|
14
|
+
export const INT16_MAX = 32767;
|
|
15
|
+
|
|
16
|
+
/** Full range of unsigned 16-bit integer (for signed conversion) */
|
|
17
|
+
export const UINT16_RANGE = 65536;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer Reading Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for reading binary data and gamepad input processing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StandardButton } from '../../core/button';
|
|
8
|
+
import {
|
|
9
|
+
BYTE_SHIFT_1,
|
|
10
|
+
ANALOG_CENTER_8BIT,
|
|
11
|
+
ANALOG_DEADZONE_8BIT,
|
|
12
|
+
ANALOG_DEADZONE_SIGNED,
|
|
13
|
+
INT16_MAX,
|
|
14
|
+
UINT16_RANGE,
|
|
15
|
+
} from './consts';
|
|
16
|
+
|
|
17
|
+
export * from './consts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* D-pad direction state
|
|
21
|
+
*/
|
|
22
|
+
export interface DpadState {
|
|
23
|
+
up: boolean;
|
|
24
|
+
down: boolean;
|
|
25
|
+
left: boolean;
|
|
26
|
+
right: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read unsigned 16-bit little-endian value from buffer
|
|
31
|
+
*/
|
|
32
|
+
export const readUint16LE = (data: Buffer, offset: number): number =>
|
|
33
|
+
data[offset] | (data[offset + 1] << BYTE_SHIFT_1);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read signed 16-bit little-endian value from buffer
|
|
37
|
+
* Converts unsigned 16-bit to signed (-32768 to +32767)
|
|
38
|
+
*/
|
|
39
|
+
export const readInt16LE = (data: Buffer, offset: number): number => {
|
|
40
|
+
const raw = readUint16LE(data, offset);
|
|
41
|
+
return raw > INT16_MAX ? raw - UINT16_RANGE : raw;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply analog stick values to d-pad buttons with deadzone
|
|
46
|
+
* For signed analog values centered at 0 (-32768 to +32767)
|
|
47
|
+
*/
|
|
48
|
+
export const applySignedAnalogToDpad = (
|
|
49
|
+
buttons: Map<StandardButton, boolean>,
|
|
50
|
+
x: number,
|
|
51
|
+
y: number,
|
|
52
|
+
deadzone: number = ANALOG_DEADZONE_SIGNED
|
|
53
|
+
): void => {
|
|
54
|
+
if (x < -deadzone) { buttons.set(StandardButton.Left, true); }
|
|
55
|
+
if (x > deadzone) { buttons.set(StandardButton.Right, true); }
|
|
56
|
+
if (y < -deadzone) { buttons.set(StandardButton.Down, true); }
|
|
57
|
+
if (y > deadzone) { buttons.set(StandardButton.Up, true); }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert analog stick values to digital d-pad state
|
|
62
|
+
* For unsigned values (0-255 range with 128 as center)
|
|
63
|
+
*/
|
|
64
|
+
export const analogToDpad = (
|
|
65
|
+
x: number,
|
|
66
|
+
y: number,
|
|
67
|
+
deadzone: number = ANALOG_DEADZONE_8BIT,
|
|
68
|
+
center: number = ANALOG_CENTER_8BIT
|
|
69
|
+
): DpadState => ({
|
|
70
|
+
left: x < center - deadzone,
|
|
71
|
+
right: x > center + deadzone,
|
|
72
|
+
up: y < center - deadzone,
|
|
73
|
+
down: y > center + deadzone,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Neutral d-pad state (no buttons pressed)
|
|
78
|
+
*/
|
|
79
|
+
const DPAD_NEUTRAL: DpadState = { up: false, down: false, left: false, right: false };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Standard HID hat values: 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8/15=center
|
|
83
|
+
*/
|
|
84
|
+
const STANDARD_HAT_MAP: Record<number, DpadState> = {
|
|
85
|
+
0: { up: true, down: false, left: false, right: false }, // N
|
|
86
|
+
1: { up: true, down: false, left: false, right: true }, // NE
|
|
87
|
+
2: { up: false, down: false, left: false, right: true }, // E
|
|
88
|
+
3: { up: false, down: true, left: false, right: true }, // SE
|
|
89
|
+
4: { up: false, down: true, left: false, right: false }, // S
|
|
90
|
+
5: { up: false, down: true, left: true, right: false }, // SW
|
|
91
|
+
6: { up: false, down: false, left: true, right: false }, // W
|
|
92
|
+
7: { up: true, down: false, left: true, right: false }, // NW
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Xbox-style hat values: 0=none, 1=N, 2=NE, 3=E, 4=SE, 5=S, 6=SW, 7=W, 8=NW
|
|
97
|
+
*/
|
|
98
|
+
const XBOX_HAT_MAP: Record<number, DpadState> = {
|
|
99
|
+
0: DPAD_NEUTRAL, // None
|
|
100
|
+
1: { up: true, down: false, left: false, right: false }, // N
|
|
101
|
+
2: { up: true, down: false, left: false, right: true }, // NE
|
|
102
|
+
3: { up: false, down: false, left: false, right: true }, // E
|
|
103
|
+
4: { up: false, down: true, left: false, right: true }, // SE
|
|
104
|
+
5: { up: false, down: true, left: false, right: false }, // S
|
|
105
|
+
6: { up: false, down: true, left: true, right: false }, // SW
|
|
106
|
+
7: { up: false, down: false, left: true, right: false }, // W
|
|
107
|
+
8: { up: true, down: false, left: true, right: false }, // NW
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse hat switch value to d-pad state
|
|
112
|
+
* @param hat - Hat switch value from HID report
|
|
113
|
+
* @param xboxStyle - If true, use Xbox 1-indexed hat values (0=none, 1=N, etc.)
|
|
114
|
+
*/
|
|
115
|
+
export const hatToDpad = (hat: number, xboxStyle: boolean = false): DpadState =>
|
|
116
|
+
(xboxStyle ? XBOX_HAT_MAP : STANDARD_HAT_MAP)[hat] ?? DPAD_NEUTRAL;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply d-pad state to button map
|
|
120
|
+
*/
|
|
121
|
+
export const applyDpadToButtons = (
|
|
122
|
+
buttons: Map<StandardButton, boolean>,
|
|
123
|
+
dpad: DpadState
|
|
124
|
+
): void => {
|
|
125
|
+
if (dpad.up) { buttons.set(StandardButton.Up, true); }
|
|
126
|
+
if (dpad.down) { buttons.set(StandardButton.Down, true); }
|
|
127
|
+
if (dpad.left) { buttons.set(StandardButton.Left, true); }
|
|
128
|
+
if (dpad.right) { buttons.set(StandardButton.Right, true); }
|
|
129
|
+
};
|