* Change so that keybindings are stored as lists, so that a key can be bound to ultiple commands (and commands can return false, to let another command execute).

* Implement ability to reorder user key bindings in settings.
This commit is contained in:
Jonatan Heyman 2025-04-13 12:39:09 +02:00
parent edeb3aee4b
commit 7a2740ef19
10 changed files with 198 additions and 123 deletions

View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -25,12 +25,15 @@ const schema = {
"keymap": { "enum": ["default", "emacs"], default:"default" }, "keymap": { "enum": ["default", "emacs"], default:"default" },
"emacsMetaKey": { "enum": [null, "alt", "meta"], default: null }, "emacsMetaKey": { "enum": [null, "alt", "meta"], default: null },
"keyBindings": { "keyBindings": {
"type": "array",
"items": {
"type": "object", "type": "object",
"propertyNames": { "required": ["key", "command"],
"type": "string" "properties": {
"key": { "type": "string" },
"command": { "type": "string" }
}, },
"additionalProperties": { "additionalProperties": false
"type": "string"
} }
}, },
@ -71,7 +74,7 @@ const defaults = {
settings: { settings: {
keymap: "default", keymap: "default",
emacsMetaKey: isMac ? "meta" : "alt", emacsMetaKey: isMac ? "meta" : "alt",
keyBindings: {}, keyBindings: [],
showLineNumberGutter: true, showLineNumberGutter: true,
showFoldGutter: true, showFoldGutter: true,
autoUpdate: true, autoUpdate: true,

21
package-lock.json generated
View File

@ -13,7 +13,8 @@
"electron-log": "^5.0.1", "electron-log": "^5.0.1",
"fuzzysort": "^3.0.2", "fuzzysort": "^3.0.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"semver": "^7.6.3" "semver": "^7.6.3",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/autocomplete": "^6.11.1", "@codemirror/autocomplete": "^6.11.1",
@ -5565,6 +5566,12 @@
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
} }
}, },
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT"
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -6472,6 +6479,18 @@
"typescript": "*" "typescript": "*"
} }
}, },
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/w3c-keyname": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@ -84,6 +84,7 @@
"electron-log": "^5.0.1", "electron-log": "^5.0.1",
"fuzzysort": "^3.0.2", "fuzzysort": "^3.0.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"semver": "^7.6.3" "semver": "^7.6.3",
"vuedraggable": "^4.1.0"
} }
} }

View File

@ -4,7 +4,7 @@
"keys", "keys",
"command", "command",
"isDefault", "isDefault",
"isOverridden", "source",
], ],
computed: { computed: {
@ -19,9 +19,9 @@
</script> </script>
<template> <template>
<tr :class="{overridden:isOverridden}"> <tr>
<td class="source"> <td class="source">
{{ isDefault ? "Heynote" : "User" }} {{ source }}
</td> </td>
<td class="key"> <td class="key">
<template v-if="keys"> <template v-if="keys">
@ -37,6 +37,9 @@
<button class="delete">Delete</button> <button class="delete">Delete</button>
</template> </template>
</td> </td>
<td v-if="!isDefault" class="drag-handle"></td>
<td v-else></td>
</tr> </tr>
</template> </template>
@ -57,6 +60,19 @@
.unbound .unbound
font-style: italic font-style: italic
color: #999 color: #999
&.drag-handle
width: 24px
padding: 0
cursor: ns-resize
background-color: rgba(0,0,0, 0.02)
background-size: 20px
background-repeat: no-repeat
background-position: center center
background-image: url(@/assets/icons/drag-vertical-light.svg)
+dark-mode
background-color: rgba(0,0,0, 0.08)
background-image: url(@/assets/icons/drag-vertical-dark.svg)
button button
padding: 0 10px padding: 0 10px
height: 22px height: 22px

View File

@ -1,5 +1,6 @@
<script> <script>
import { mapState} from 'pinia' import { mapState} from 'pinia'
import draggable from 'vuedraggable'
import { DEFAULT_KEYMAP, EMACS_KEYMAP } from "@/src/editor/keymap" import { DEFAULT_KEYMAP, EMACS_KEYMAP } from "@/src/editor/keymap"
import { useSettingsStore } from "@/src/stores/settings-store" import { useSettingsStore } from "@/src/stores/settings-store"
@ -7,72 +8,77 @@
export default { export default {
props: [ props: [
"userKeys" "userKeys",
"modelValue",
], ],
components: { components: {
KeyBindRow, KeyBindRow,
draggable,
}, },
data() { data() {
return { return {
keymap: this.modelValue,
//fixedKeymap: [],
} }
}, },
mounted() {
/*const defaultKeymap = this.settings.keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP
this.fixedKeymap = defaultKeymap.map((km) => {
return {
key: km.key,
command: km.command,
isDefault: true,
}
})*/
/*
const keymap = this.settings.keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP
this.testKeymap = [
...this.userKeys,
{"key": "Mod-Enter", command: "yay"},
{"key": "Mod-Enter n", command: "nay"},
{"key": "Ctrl-o", command: null},
]
this.fixedKeymap = keymap.map((km) => {
return {
key: km.key,
command: km.command,
isDefault: true,
isOverridden: km.key in this.userKeys,
}
})*/
},
computed: { computed: {
...mapState(useSettingsStore, [ ...mapState(useSettingsStore, [
"settings", "settings",
]), ]),
keymapOld() { fixedKeymap() {
const userKeys = [ const defaultKeymap = (this.settings.keymap === "emacs" ? EMACS_KEYMAP : []).map((km) => ({
{key: "Mod-Enter", command: null},
]
// merge default keymap with user keymap
const defaultKeys = Object.fromEntries(DEFAULT_KEYMAP.map(km => [km.key, km.command]))
let mergedKeys = {...defaultKeys, ...Object.fromEntries(userKeys.map(km => [km.key, km.command]))}
//console.log("defaultKeys:", defaultKeys)
return Object.entries(mergedKeys).map(([key, command]) => {
return {
key: key,
command: command,
isDefault: defaultKeys[key] !== undefined && defaultKeys[key] === command,
}
})
},
keymap() {
//const userKeys = {
// "Mod-Enter": null,
// "Mod-Shift-A": "test",
//}
const keymap = this.settings.keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP
return [
...Object.entries(this.userKeys).map(([key, command]) => {
return {key, command}
}),
...keymap.map((km) => {
return {
key: km.key, key: km.key,
command: km.command, command: km.command,
isDefault: true, isDefault: true,
isOverridden: km.key in this.userKeys, source: "Emacs",
} }))
}), return defaultKeymap.concat(
] DEFAULT_KEYMAP.map((km) => ({
} key: km.key,
command: km.command,
isDefault: true,
source: "Default",
}))
)
},
}, },
methods: { methods: {
onDragEnd(event) {
console.log("onDragEnd", this.testKeymap)
this.$emit("update:modelValue", this.keymap)
},
}, },
} }
</script> </script>
@ -82,19 +88,41 @@
<h2>Keyboard Bindings</h2> <h2>Keyboard Bindings</h2>
<table> <table>
<tr> <tr>
<th></th>
<th>Source</th> <th>Source</th>
<th>Key</th> <th>Key</th>
<th>Command</th> <th>Command</th>
<th></th> <th></th>
</tr> </tr>
<draggable
v-model="keymap"
tag="tbody"
group="keymap"
ghost-class="ghost"
handle=".drag-handle"
@start="drag=true"
@end="onDragEnd"
item-key="key"
>
<template #item="{element}">
<KeyBindRow <KeyBindRow
v-for="key in keymap" :keys="element.key"
:command="element.command"
:isDefault="element.isDefault"
source="User"
/>
</template>
</draggable>
<tbody>
<KeyBindRow
v-for="key in fixedKeymap"
:key="key.key" :key="key.key"
:keys="key.key" :keys="key.key"
:command="key.command" :command="key.command"
:isOverridden="key.isOverridden"
:isDefault="key.isDefault" :isDefault="key.isDefault"
:source="key.source"
/> />
</tbody>
</table> </table>
</div> </div>
</template> </template>
@ -111,16 +139,31 @@
border: 2px solid #f1f1f1 border: 2px solid #f1f1f1
+dark-mode +dark-mode
background: #3c3c3c background: #3c3c3c
background: #333
border: 2px solid #3c3c3c border: 2px solid #3c3c3c
::v-deep(tr) ::v-deep(tr)
&:nth-child(2n)
background: #fff background: #fff
border-bottom: 2px solid #f1f1f1
+dark-mode +dark-mode
background: #333 background: #333
border-bottom: 2px solid #3c3c3c
&.ghost
background: #48b57e
color: #fff
+dark-mode
background: #1b6540
th th
text-align: left text-align: left
font-weight: 600 font-weight: 600
th, td th, td
padding: 8px padding: 8px
&.actions
padding: 6px
button
height: 20px
font-size: 11px
tbody
margin-bottom: 20px
</style> </style>

View File

@ -86,6 +86,12 @@
window.removeEventListener("keydown", this.onKeyDown); window.removeEventListener("keydown", this.onKeyDown);
}, },
watch: {
keyBindings(newKeyBindings) {
this.updateSettings()
}
},
methods: { methods: {
onKeyDown(event) { onKeyDown(event) {
if (event.key === "Escape") { if (event.key === "Escape") {
@ -98,7 +104,7 @@
showLineNumberGutter: this.showLineNumberGutter, showLineNumberGutter: this.showLineNumberGutter,
showFoldGutter: this.showFoldGutter, showFoldGutter: this.showFoldGutter,
keymap: this.keymap, keymap: this.keymap,
keyBindings: toRaw(this.keyBindings), keyBindings: this.keyBindings.map((kb) => toRaw(kb)),
emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt", emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt",
allowBetaVersions: this.allowBetaVersions, allowBetaVersions: this.allowBetaVersions,
enableGlobalHotkey: this.enableGlobalHotkey, enableGlobalHotkey: this.enableGlobalHotkey,
@ -369,6 +375,7 @@
</div> </div>
<KeyboardBindings <KeyboardBindings
:userKeys="keyBindings ? keyBindings : {}" :userKeys="keyBindings ? keyBindings : {}"
v-model="keyBindings"
/> />
</TabContent> </TabContent>

View File

@ -16,7 +16,7 @@ import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, t
import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK, SET_FONT } from "./annotation.js"; import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK, SET_FONT } from "./annotation.js";
import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.js" import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.js"
import { formatBlockContent } from "./block/format-code.js" import { formatBlockContent } from "./block/format-code.js"
import { heynoteKeymap, DEFAULT_KEYMAP, EMACS_KEYMAP } from "./keymap.js" import { getKeymapExtensions } from "./keymap.js"
import { heynoteCopyCut } from "./copy-paste" import { heynoteCopyCut } from "./copy-paste"
import { languageDetection } from "./language-detection/autodetect.js" import { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js" import { autoSaveContent } from "./save.js"
@ -29,15 +29,6 @@ import { useHeynoteStore } from "../stores/heynote-store.js";
import { useErrorStore } from "../stores/error-store.js"; import { useErrorStore } from "../stores/error-store.js";
function getKeymapExtensions(editor, keymap, keyBindings) {
return heynoteKeymap(
editor,
keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP,
keyBindings,
)
}
export class HeynoteEditor { export class HeynoteEditor {
constructor({ constructor({
element, element,

View File

@ -3,30 +3,6 @@ import { Prec } from "@codemirror/state"
import { HEYNOTE_COMMANDS } from "./commands.js" import { HEYNOTE_COMMANDS } from "./commands.js"
function keymapFromSpec(specs, editor) {
return keymap.of(specs.map((spec) => {
let key = spec.key
if (key.indexOf("EmacsMeta") != -1) {
key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta")
}
return {
key: key,
//preventDefault: true,
preventDefault: false,
run: (view) => {
//console.log("run()", spec.key, spec.command)
const command = HEYNOTE_COMMANDS[spec.command]
if (!command) {
console.error(`Command not found: ${spec.command} (${spec.key})`)
return false
}
return command(editor)(view)
},
}
}))
}
const cmd = (key, command) => ({key, command}) const cmd = (key, command) => ({key, command})
const cmdShift = (key, command, shiftCommand) => { const cmdShift = (key, command, shiftCommand) => {
return [ return [
@ -99,8 +75,6 @@ export const DEFAULT_KEYMAP = [
cmd("Shift-Tab", "indentLess"), cmd("Shift-Tab", "indentLess"),
//cmd("Alt-ArrowLeft", "cursorSubwordBackward"), //cmd("Alt-ArrowLeft", "cursorSubwordBackward"),
//cmd("Alt-ArrowRight", "cursorSubwordForward"), //cmd("Alt-ArrowRight", "cursorSubwordForward"),
cmd("Ctrl-Space", "toggleEmacsMarkMode"),
cmd("Ctrl-g", "emacsCancel"),
cmd("Mod-l", "openLanguageSelector"), cmd("Mod-l", "openLanguageSelector"),
cmd("Mod-p", "openBufferSelector"), cmd("Mod-p", "openBufferSelector"),
@ -146,27 +120,46 @@ export const EMACS_KEYMAP = [
...cmdShift("Ctrl-f", "cursorCharRight", "selectCharRight"), ...cmdShift("Ctrl-f", "cursorCharRight", "selectCharRight"),
...cmdShift("Ctrl-a", "cursorLineStart", "selectLineStart"), ...cmdShift("Ctrl-a", "cursorLineStart", "selectLineStart"),
...cmdShift("Ctrl-e", "cursorLineEnd", "selectLineEnd"), ...cmdShift("Ctrl-e", "cursorLineEnd", "selectLineEnd"),
...DEFAULT_KEYMAP,
] ]
function keymapFromSpec(specs, editor) {
return keymap.of(specs.map((spec) => {
let key = spec.key
if (key.indexOf("EmacsMeta") != -1) {
key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta")
}
return {
key: key,
//preventDefault: true,
preventDefault: false,
run: (view) => {
//console.log("run()", spec.key, spec.command)
const command = HEYNOTE_COMMANDS[spec.command]
if (!command) {
console.error(`Command not found: ${spec.command} (${spec.key})`)
return false
}
return command(editor)(view)
},
}
}))
}
export function heynoteKeymap(editor, keymap, userKeymap) { export function heynoteKeymap(editor, keymap, userKeymap) {
//return [
// keymapFromSpec([
// ...Object.entries(userKeymap).map(([key, command]) => cmd(key, command)),
// ...keymap,
// ], editor),
//]
// merge the default keymap with the custom keymap
const defaultKeys = Object.fromEntries(keymap.map(km => [km.key, km.command]))
//let mergedKeys = Object.entries({...defaultKeys, ...Object.fromEntries(userKeymap.map(km => [km.key, km.command]))}).map(([key, command]) => cmd(key, command))
let mergedKeys = Object.entries({...defaultKeys, ...userKeymap}).map(([key, command]) => cmd(key, command))
//console.log("userKeys:", userKeymap)
//console.log("mergedKeys:", mergedKeys)
return [ return [
Prec.high(keymapFromSpec(mergedKeys, editor)), keymapFromSpec([
...userKeymap,
...keymap,
], editor),
] ]
} }
export function getKeymapExtensions(editor, keymap, keyBindings) {
return heynoteKeymap(
editor,
keymap === "emacs" ? EMACS_KEYMAP.concat(DEFAULT_KEYMAP) : DEFAULT_KEYMAP,
keyBindings,
)
}