Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@
return $resultPromise;
}

/**
* ResetSingleKeyBinding 重置单个快捷键到默认值
*/
export function ResetSingleKeyBinding(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(160366089, id) as any;

Check warning on line 71 in frontend/bindings/voidraft/internal/services/keybindingservice.ts

View check run for this annotation

codefactor.io / CodeFactor

frontend/bindings/voidraft/internal/services/keybindingservice.ts#L71

Unexpected any. Specify a different type. (@typescript-eslint/no-explicit-any)

Check warning on line 71 in frontend/bindings/voidraft/internal/services/keybindingservice.ts

View check run for this annotation

codefactor.io / CodeFactor

frontend/bindings/voidraft/internal/services/keybindingservice.ts#L71

'$resultPromise' is never reassigned. Use 'const' instead. (prefer-const)
Comment thread
landaiqing marked this conversation as resolved.
return $resultPromise;
}

/**
* ServiceStartup 服务启动
*/
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export { useConfirm } from './useConfirm';
export type { UseConfirmOptions } from './useConfirm';

export { useKeyRecorder } from './useKeyRecorder';
export type { KeyRecorderState, UseKeyRecorderOptions, UseKeyRecorderReturn } from './useKeyRecorder';

export { usePolling } from './usePolling';
export type { UsePollingOptions, UsePollingReturn } from './usePolling';
176 changes: 176 additions & 0 deletions frontend/src/composables/useKeyRecorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { ref, onUnmounted, readonly, type Ref, type DeepReadonly } from 'vue';

export interface KeyRecorderState {
/** The binding ID currently being recorded */
bindingId: number;
/** Internal key string result (e.g. "Mod-Shift-k") */
result: string;
/** Display parts for UI (e.g. ["Ctrl", "Shift", "K"]) */
displayParts: string[];
/** Whether a non-modifier key has been captured (recording complete) */
completed: boolean;
}

export interface UseKeyRecorderOptions {
isMacOS: boolean;
onComplete?: (bindingId: number, keyString: string) => void;
onCancel?: (bindingId: number) => void;
}

export interface UseKeyRecorderReturn {
recording: DeepReadonly<Ref<KeyRecorderState | null>>;
isRecording: (bindingId: number) => boolean;
startRecording: (bindingId: number) => void;
stopRecording: () => void;
}

const MODIFIER_KEYS = new Set(['Control', 'Shift', 'Alt', 'Meta']);

const KEY_DISPLAY_MAP: Record<string, string> = {
'ArrowUp': '↑',
'ArrowDown': '↓',
'ArrowLeft': '←',
'ArrowRight': '→',
' ': 'Space',
};

const MAC_MODIFIER_DISPLAY: Record<string, string> = {
'Mod': '⌘',
'Alt': '⌥',
'Shift': '⇧',
'Ctrl': '⌃',
};

function normalizeKey(key: string): string {
if (key === ' ') return 'Space';
if (key.length === 1) return key.toLowerCase();
return key;
}

function getDisplayKey(key: string, isMacOS: boolean): string {
if (isMacOS && MAC_MODIFIER_DISPLAY[key]) return MAC_MODIFIER_DISPLAY[key];
if (KEY_DISPLAY_MAP[key]) return KEY_DISPLAY_MAP[key];
if (key.length === 1) return key.toUpperCase();
return key;
}

/**
* Composable for capturing keyboard shortcuts via keydown events.
* Captures the full key combination (modifiers + main key) in a single interaction.
*/
export function useKeyRecorder(options: UseKeyRecorderOptions): UseKeyRecorderReturn {
const { isMacOS, onComplete, onCancel } = options;

Check warning on line 62 in frontend/src/composables/useKeyRecorder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "onCancel".

See more on https://sonarcloud.io/project/issues?id=landaiqing_voidraft&issues=AZ2HcGpL1AF3ee_IjxQJ&open=AZ2HcGpL1AF3ee_IjxQJ&pullRequest=49

const recording = ref<KeyRecorderState | null>(null);

const buildParts = (e: KeyboardEvent): { internalParts: string[]; displayParts: string[] } => {
const internalParts: string[] = [];
const displayParts: string[] = [];

const hasCtrlOrMeta = e.ctrlKey || e.metaKey;
if (hasCtrlOrMeta) {
internalParts.push('Mod');
displayParts.push(getDisplayKey('Mod', isMacOS));
}

if (e.altKey) {
internalParts.push('Alt');
displayParts.push(getDisplayKey('Alt', isMacOS));
}

if (e.shiftKey) {
internalParts.push('Shift');
displayParts.push(getDisplayKey('Shift', isMacOS));
}

return { internalParts, displayParts };
};

const onKeyDown = (e: KeyboardEvent) => {
if (!recording.value) return;

e.preventDefault();
e.stopPropagation();

const { internalParts, displayParts } = buildParts(e);

if (MODIFIER_KEYS.has(e.key)) {
recording.value = {
...recording.value,
result: '',
displayParts,
completed: false,
};
return;
}

const mainKey = normalizeKey(e.key);
internalParts.push(mainKey);
displayParts.push(getDisplayKey(e.key, isMacOS));

const keyString = internalParts.join('-');

recording.value = {
...recording.value,
result: keyString,
displayParts,
completed: true,
};

const bindingId = recording.value.bindingId;
cleanup();
recording.value = null;
onComplete?.(bindingId, keyString);
};

const onKeyUp = (e: KeyboardEvent) => {
if (!recording.value) return;
e.preventDefault();
e.stopPropagation();
};

const preventDefaults = (e: KeyboardEvent) => {

Check warning on line 132 in frontend/src/composables/useKeyRecorder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Update this function so that its implementation is not identical to the one on line 126.

See more on https://sonarcloud.io/project/issues?id=landaiqing_voidraft&issues=AZ2HcGpL1AF3ee_IjxQK&open=AZ2HcGpL1AF3ee_IjxQK&pullRequest=49
if (!recording.value) return;
e.preventDefault();
e.stopPropagation();
};

const cleanup = () => {
document.removeEventListener('keydown', onKeyDown, true);
document.removeEventListener('keyup', onKeyUp, true);
document.removeEventListener('keypress', preventDefaults, true);
};

const startRecording = (bindingId: number) => {
cleanup();

recording.value = {
bindingId,
result: '',
displayParts: [],
completed: false,
};

document.addEventListener('keydown', onKeyDown, true);
document.addEventListener('keyup', onKeyUp, true);
document.addEventListener('keypress', preventDefaults, true);
};

const stopRecording = () => {
cleanup();
recording.value = null;
};

const isRecording = (bindingId: number): boolean => {
return recording.value?.bindingId === bindingId;
};

onUnmounted(cleanup);

return {
recording: readonly(recording) as DeepReadonly<Ref<KeyRecorderState | null>>,
isRecording,
startRecording,
stopRecording,
};
}
119 changes: 115 additions & 4 deletions frontend/src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ export default {
preventDefault: 'Prevent Default',
keybinding: 'Keybinding'
},
keyPlaceholder: 'Enter key, press Enter to add',
invalidFormat: 'Invalid format',
conflict: 'Conflict: {command}',
maxKeysReached: 'Maximum 4 keys allowed',
saveFailed: 'Failed to save keybinding',
resetSingle: 'Reset to default',
confirmSave: 'Confirm save',
conflictWith: 'Conflicts with "{command}"',
commands: {
showSearch: 'Show search panel',
hideSearch: 'Hide search panel',
Expand Down Expand Up @@ -244,6 +244,117 @@ export default {
customThemeColors: 'Custom Theme Colors',
resetToDefault: 'Reset to Default',
colorValue: 'Color Value',
themeColors: {
// Editor UI colors
background: 'Background',
backgroundSecondary: 'Secondary Background',
foreground: 'Foreground Text',
cursor: 'Cursor Color',
selection: 'Selection Highlight',
activeLine: 'Active Line Background',
lineNumber: 'Line Number',
activeLineNumber: 'Active Line Number',
diffInserted: 'Diff Inserted',
diffDeleted: 'Diff Deleted',
diffChanged: 'Diff Changed',
borderColor: 'Border Color',
matchingBracket: 'Matching Bracket',
searchMatch: 'Search Match Background',
searchMatchSelected: 'Selected Match Background',
searchMatchSelectedOutline: 'Selected Match Outline',
// Syntax - Comments
comment: 'Comment',
lineComment: 'Line Comment',
blockComment: 'Block Comment',
docComment: 'Doc Comment',
// Syntax - Names / Identifiers
name: 'Name',
variableName: 'Variable Name',
typeName: 'Type Name',
tagName: 'Tag Name',
propertyName: 'Property Name',
attributeName: 'Attribute Name',
className: 'Class Name',
labelName: 'Label Name',
namespace: 'Namespace',
macroName: 'Macro Name',
// Syntax - Literals
literal: 'Literal',
string: 'String',
docString: 'Doc String',
character: 'Character',
attributeValue: 'Attribute Value',
number: 'Number',
integer: 'Integer',
float: 'Float',
bool: 'Boolean',
regexp: 'Regular Expression',
escape: 'Escape Sequence',
color: 'Color Literal',
url: 'URL',
// Syntax - Keywords
keyword: 'Keyword',
self: 'Self / This',
null: 'Null / Nil',
atom: 'Atom',
unit: 'Unit',
modifier: 'Modifier',
operatorKeyword: 'Operator Keyword',
controlKeyword: 'Control Keyword',
definitionKeyword: 'Definition Keyword',
moduleKeyword: 'Module Keyword',
// Syntax - Operators
operator: 'Operator',
derefOperator: 'Dereference Operator',
arithmeticOperator: 'Arithmetic Operator',
logicOperator: 'Logic Operator',
bitwiseOperator: 'Bitwise Operator',
compareOperator: 'Comparison Operator',
updateOperator: 'Assignment Operator',
definitionOperator: 'Definition Operator',
typeOperator: 'Type Operator',
controlOperator: 'Control Operator',
// Syntax - Punctuation
punctuation: 'Punctuation',
separator: 'Separator',
bracket: 'Bracket',
angleBracket: 'Angle Bracket',
squareBracket: 'Square Bracket',
paren: 'Parenthesis',
brace: 'Brace',
// Syntax - Document / Content
content: 'Content',
heading: 'Heading',
heading1: 'Heading 1',
heading2: 'Heading 2',
heading3: 'Heading 3',
heading4: 'Heading 4',
heading5: 'Heading 5',
heading6: 'Heading 6',
contentSeparator: 'Content Separator',
list: 'List',
quote: 'Blockquote',
emphasis: 'Emphasis (Italic)',
strong: 'Strong (Bold)',
link: 'Link',
monospace: 'Monospace',
strikethrough: 'Strikethrough',
inserted: 'Inserted',
deleted: 'Deleted',
changed: 'Changed',
invalid: 'Invalid',
meta: 'Meta',
documentMeta: 'Document Meta',
annotation: 'Annotation',
processingInstruction: 'Processing Instruction',
// Syntax - Misc
definition: 'Definition',
constant: 'Constant',
function: 'Function',
standard: 'Standard',
local: 'Local Variable',
special: 'Special',
},
lineHeight: 'Line Height',
tabSettings: 'Tab Settings',
tabSize: 'Tab Size',
Expand Down
Loading
Loading