Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
16b2be7
feat: Add visual keymap editor to workbench with CustomKey popover in…
yoichiro Mar 25, 2026
bd67d69
fix: Add local state management and error handling to visual keymap e…
yoichiro Mar 25, 2026
7e2d535
fix: Handle undefined macro props in TabKey and improve keycode resol…
yoichiro Mar 25, 2026
561f684
fix: Give VisualKeycap wrapper explicit size and position for correct…
yoichiro Mar 25, 2026
81277c0
fix: Use ref to track prop changes and prevent local keymap from reve…
yoichiro Mar 25, 2026
220b607
fix: Use refreshCurrentProject=true for visual editor changes to sync…
yoichiro Mar 25, 2026
a49f241
fix: Preserve original formatting, comments, and surrounding code whe…
yoichiro Mar 25, 2026
83e87bc
fix: Preserve preamble/postamble and format keycodes by keyboard row …
yoichiro Mar 25, 2026
99cba13
fix: Use matrix row grouping instead of Y coordinate for keycode line…
yoichiro Mar 25, 2026
b0c01d3
feat: Show full QMK keycode names on keycaps to match source code
yoichiro Mar 25, 2026
c20da92
feat: Show Configure-style keycap labels with meta (shift variant) di…
yoichiro Mar 25, 2026
11f9f88
refactor: Remove configure dependency from VisualKeycap
yoichiro Mar 25, 2026
0b620c7
fix: Support enum name layer indices (e.g. [_QWERTY]) in keymap.c parser
yoichiro Mar 25, 2026
3bb3644
fix: Center keycap labels vertically instead of pushing meta to top
yoichiro Mar 25, 2026
6507b0c
fix: Show all keyboard.json keys and auto-expand keymap.c with KC_NO …
yoichiro Mar 25, 2026
d1a6ecf
feat: Move font size controls to tab row for keymap.c files
yoichiro Mar 25, 2026
e45c193
feat: Highlight custom/unknown keycodes with orange italic style on k…
yoichiro Mar 25, 2026
9e1a4eb
feat: Add tooltip explaining custom keycodes cannot be edited in Visu…
yoichiro Mar 25, 2026
52fb89f
fix: Internationalize custom keycode tooltip with i18next
yoichiro Mar 25, 2026
409c48b
feat: Add Japanese translations for visual keymap editor UI strings
yoichiro Mar 25, 2026
0bbb02e
feat: Show hold function label on keycap for hold/tap keycodes
yoichiro Mar 25, 2026
4265d18
fix: Position hold label at top of keycap for better readability
yoichiro Mar 25, 2026
53371c2
fix: Position hold label at top and tap label at center of keycap
yoichiro Mar 25, 2026
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
13 changes: 12 additions & 1 deletion src/assets/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -479,5 +479,16 @@
"Completed! Great job!": "完了です!素晴らしい!",
"Practice Category": "練習カテゴリー",
"Are you sure you want to reset your typing statistics? This action cannot be undone.": "統計情報をリセットしてもよろしいですか?この操作は元に戻せません。",
"Sentence": "文"
"Sentence": "文",
"Code Editor": "コードエディタ",
"Visual Editor": "ビジュアルエディタ",
"Layer": "レイヤー",
"Add layer": "レイヤーを追加",
"Delete layer": "レイヤーを削除",
"No keyboard.json found in this project.": "このプロジェクトに keyboard.json が見つかりません。",
"Please generate files first to use the visual editor.": "ビジュアルエディタを使用するには、まずファイルを生成してください。",
"Could not parse keyboard.json layout.": "keyboard.json のレイアウトを解析できませんでした。",
"Could not parse keymap.c file.": "keymap.c ファイルを解析できませんでした。",
"Please use the Code Editor to fix syntax errors.": "コードエディタを使用して構文エラーを修正してください。",
"\"{{name}}\" is a custom keycode defined in the source code. It cannot be edited in the Visual Editor.": "\"{{name}}\" はソースコード内で定義されたカスタムキーコードです。ビジュアルエディタでは編集できません。"
}
6 changes: 3 additions & 3 deletions src/components/common/customkey/TabKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@
}

private createMacroBuffer(): IMacroBuffer {
const macroBufferBytes = this.props.macroBufferBytes!;
const macroMaxBufferSize = this.props.macroMaxBufferSize!;
const macroMaxCount = this.props.macroMaxCount!;
const macroBufferBytes = this.props.macroBufferBytes ?? new Uint8Array(0);
const macroMaxBufferSize = this.props.macroMaxBufferSize ?? 0;
const macroMaxCount = this.props.macroMaxCount ?? 0;
const macroBuffer: IMacroBuffer = new MacroBuffer(
macroBufferBytes,
macroMaxCount,
Expand All @@ -128,7 +128,7 @@
const macroBuffer = this.createMacroBuffer();
const keymaps = [
...KeyCategory.basic(labelLang),
...KeyCategory.functions(

Check warning on line 131 in src/components/common/customkey/TabKey.tsx

View workflow job for this annotation

GitHub Actions / Build (20.x)

Replace `⏎········labelLang,⏎········this.props.customKeycodes⏎······` with `labelLang,·this.props.customKeycodes`
labelLang,
this.props.customKeycodes
),
Expand Down
230 changes: 182 additions & 48 deletions src/components/workbench/breadboard/Breadboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';

Check warning on line 1 in src/components/workbench/breadboard/Breadboard.tsx

View workflow job for this annotation

GitHub Actions / Build (20.x)

Replace `·useCallback,·useEffect,·useMemo,·useState,·useRef·` with `⏎··useCallback,⏎··useEffect,⏎··useMemo,⏎··useState,⏎··useRef,⏎`
import {
BreadboardActionsType,
BreadboardStateType,
Expand Down Expand Up @@ -47,6 +47,7 @@
import { useUserPurchaseHook } from './UserPurchaseHook';
import LayoutPreviewDialog from '../dialogs/LayoutPreviewDialog';
import KeyboardJsonEditorDialog from '../dialogs/KeyboardJsonEditorDialog';
import VisualKeymapEditor from '../visualeditor/VisualKeymapEditor';

type OwnProps = {};
type BreadboardProps = OwnProps &
Expand Down Expand Up @@ -362,23 +363,12 @@
flexDirection: 'column',
}}
>
<WorkbenchSourceCodeEditor
<EditorWithVisualTab
project={props.currentProject}
file={
props.currentProject === undefined
? undefined
: props.selectedFile === undefined
? undefined
: props.selectedFile.fileType === 'keyboard'
? props.currentProject.keyboardFiles.find(
(file) => file.id === props.selectedFile!.fileId
)
: props.currentProject.keymapFiles.find(
(file) => file.id === props.selectedFile!.fileId
)
}
selectedFile={props.selectedFile}
onChangeCode={onChangeCode}
fontSize={editorFontSize}
onUpdateFile={props.updateWorkbenchProjectFile!}
editorFontSize={editorFontSize}
onFontSizeChange={setEditorFontSize}
/>
</Paper>
Expand Down Expand Up @@ -567,12 +557,153 @@
);
}

type EditorWithVisualTabProps = {
project: IWorkbenchProject | undefined;
selectedFile:
| { fileId: string; fileType: IBuildableFirmwareFileType }
| undefined;
onChangeCode: (file: IWorkbenchProjectFile, code: string) => void;
// eslint-disable-next-line no-unused-vars
onUpdateFile: (
project: IWorkbenchProject,
file: IWorkbenchProjectFile,
path: string,
code: string,
refreshCurrentProject: boolean
) => void;
editorFontSize: number;
onFontSizeChange: (size: number) => void;
};

function EditorWithVisualTab(props: EditorWithVisualTabProps) {
const [editorTab, setEditorTab] = useState(0);

const file = useMemo(() => {
if (props.project === undefined || props.selectedFile === undefined) {
return undefined;
}
return props.selectedFile.fileType === 'keyboard'
? props.project.keyboardFiles.find(
(f) => f.id === props.selectedFile!.fileId
)
: props.project.keymapFiles.find(
(f) => f.id === props.selectedFile!.fileId
);
}, [props.project, props.selectedFile]);

const isKeymapCFile =
props.selectedFile?.fileType === 'keymap' &&

Check warning on line 595 in src/components/workbench/breadboard/Breadboard.tsx

View workflow job for this annotation

GitHub Actions / Build (20.x)

Delete `⏎···`
file?.path?.endsWith('.c');

const keyboardJsonCode = useMemo(() => {
if (!props.project) return undefined;
const kbFile = props.project.keyboardFiles.find(
(f) => f.path === 'keyboard.json'
);
return kbFile?.code;
}, [props.project]);

// Reset to Code Editor tab when file changes or is not a keymap
useEffect(() => {
if (!isKeymapCFile) {
setEditorTab(0);
}
}, [isKeymapCFile, file?.id]);

const handleVisualEditorCodeChange = useCallback(
(code: string) => {
if (file && props.project) {
// Update the file with refreshCurrentProject=true so the Redux state
// reflects the change immediately (needed for Code Editor tab sync).
props.onUpdateFile(props.project, file, file.path, code, true);
}
},
[file, props.project, props.onUpdateFile]
);

return (
<>
{isKeymapCFile && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
borderBottom: 1,
borderColor: 'divider',
}}
>
<Tabs
value={editorTab}
onChange={(_, newValue) => setEditorTab(newValue)}
sx={{ minHeight: 32, flexGrow: 1 }}
>
<Tab
label={t('Code Editor')}
sx={{ minHeight: 32, py: 0.5, textTransform: 'none' }}
/>
<Tab
label={t('Visual Editor')}
sx={{ minHeight: 32, py: 0.5, textTransform: 'none' }}
/>
</Tabs>
{editorTab === 0 && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, pr: 1 }}>

Check warning on line 650 in src/components/workbench/breadboard/Breadboard.tsx

View workflow job for this annotation

GitHub Actions / Build (20.x)

Replace `·sx={{·display:·'flex',·alignItems:·'center',·gap:·0.5,·pr:·1·}}` with `⏎··············sx={{·display:·'flex',·alignItems:·'center',·gap:·0.5,·pr:·1·}}⏎············`
<IconButton
size="small"
onClick={() =>
props.onFontSizeChange(Math.max(8, props.editorFontSize - 2))
}
>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
A-
</Typography>
</IconButton>
<Typography variant="caption" color="text.secondary">
{props.editorFontSize}px
</Typography>
<IconButton
size="small"
onClick={() =>
props.onFontSizeChange(Math.min(32, props.editorFontSize + 2))
}
>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
A+
</Typography>
</IconButton>
</Box>
)}
</Box>
)}
{editorTab === 0 || !isKeymapCFile ? (
<WorkbenchSourceCodeEditor
project={props.project}
file={file}
onChangeCode={props.onChangeCode}
fontSize={props.editorFontSize}
onFontSizeChange={props.onFontSizeChange}
showFontSizeControls={!isKeymapCFile}
/>
) : (
file && (
<VisualKeymapEditor
keymapCode={file.code}
keyboardJsonCode={keyboardJsonCode}
onChangeCode={handleVisualEditorCodeChange}
/>
)
)}
</>
);
}

type WorkbenchSourceCodeEditorProps = {
project: IWorkbenchProject | undefined;
file: IWorkbenchProjectFile | undefined;
onChangeCode: (file: IWorkbenchProjectFile, code: string) => void;
fontSize: number;
onFontSizeChange: (size: number) => void;
showFontSizeControls?: boolean;
};

function WorkbenchSourceCodeEditor(props: WorkbenchSourceCodeEditorProps) {
Expand Down Expand Up @@ -649,42 +780,45 @@
};

if (props.project !== undefined && props.file !== undefined) {
const showControls = props.showFontSizeControls !== false;
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '2px 8px',
gap: 4,
borderBottom: '1px solid #e0e0e0',
}}
>
<IconButton
size="small"
onClick={() =>
props.onFontSizeChange(Math.max(8, props.fontSize - 2))
}
>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
A-
</Typography>
</IconButton>
<Typography variant="caption" color="text.secondary">
{props.fontSize}px
</Typography>
<IconButton
size="small"
onClick={() =>
props.onFontSizeChange(Math.min(32, props.fontSize + 2))
}
{showControls && (
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '2px 8px',
gap: 4,
borderBottom: '1px solid #e0e0e0',
}}
>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
A+
<IconButton
size="small"
onClick={() =>
props.onFontSizeChange(Math.max(8, props.fontSize - 2))
}
>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
A-
</Typography>
</IconButton>
<Typography variant="caption" color="text.secondary">
{props.fontSize}px
</Typography>
</IconButton>
</div>
<IconButton
size="small"
onClick={() =>
props.onFontSizeChange(Math.min(32, props.fontSize + 2))
}
>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
A+
</Typography>
</IconButton>
</div>
)}
<div style={{ flex: 1 }}>
<Editor
language={getLanguage(props.file.path)}
Expand Down
65 changes: 65 additions & 0 deletions src/components/workbench/visualeditor/LayerSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { Box, IconButton, Tab, Tabs, Tooltip } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import { t } from 'i18next';

type LayerSelectorProps = {
layerCount: number;
selectedLayer: number;
// eslint-disable-next-line no-unused-vars
onSelectLayer: (layer: number) => void;
onAddLayer: () => void;
onDeleteLayer: () => void;
};

export default function LayerSelector({
layerCount,
selectedLayer,
onSelectLayer,
onAddLayer,
onDeleteLayer,
}: LayerSelectorProps) {
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
borderBottom: 1,
borderColor: 'divider',
}}
>
<Tabs
value={selectedLayer}
onChange={(_, newValue) => onSelectLayer(newValue)}
variant="scrollable"
scrollButtons="auto"
sx={{ flexGrow: 1, minHeight: 36 }}
>
{Array.from({ length: layerCount }, (_, i) => (
<Tab
key={i}
label={`${t('Layer')} ${i}`}
sx={{ minHeight: 36, py: 0.5, textTransform: 'none' }}
/>
))}
</Tabs>
<Tooltip title={t('Add layer')}>
<IconButton size="small" onClick={onAddLayer}>
<AddIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('Delete layer')}>
<span>
<IconButton
size="small"
onClick={onDeleteLayer}
disabled={layerCount <= 1}
>
<DeleteIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</Box>
);
}
Loading
Loading