* 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" },
"emacsMetaKey": { "enum": [null, "alt", "meta"], default: null },
"keyBindings": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
"type": "array",
"items": {
"type": "object",
"required": ["key", "command"],
"properties": {
"key": { "type": "string" },
"command": { "type": "string" }
},
"additionalProperties": false
}
},
@ -71,7 +74,7 @@ const defaults = {
settings: {
keymap: "default",
emacsMetaKey: isMac ? "meta" : "alt",
keyBindings: {},
keyBindings: [],
showLineNumberGutter: true,
showFoldGutter: true,
autoUpdate: true,

21
package-lock.json generated
View File

@ -13,7 +13,8 @@
"electron-log": "^5.0.1",
"fuzzysort": "^3.0.2",
"pinia": "^2.1.7",
"semver": "^7.6.3"
"semver": "^7.6.3",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.11.1",
@ -5565,6 +5566,12 @@
"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": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -6472,6 +6479,18 @@
"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": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

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

View File

@ -4,7 +4,7 @@
"keys",
"command",
"isDefault",
"isOverridden",
"source",
],
computed: {
@ -19,9 +19,9 @@
</script>
<template>
<tr :class="{overridden:isOverridden}">
<tr>
<td class="source">
{{ isDefault ? "Heynote" : "User" }}
{{ source }}
</td>
<td class="key">
<template v-if="keys">
@ -37,6 +37,9 @@
<button class="delete">Delete</button>
</template>
</td>
<td v-if="!isDefault" class="drag-handle"></td>
<td v-else></td>
</tr>
</template>
@ -57,6 +60,19 @@
.unbound
font-style: italic
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
padding: 0 10px
height: 22px

View File

@ -1,5 +1,6 @@
<script>
import { mapState} from 'pinia'
import draggable from 'vuedraggable'
import { DEFAULT_KEYMAP, EMACS_KEYMAP } from "@/src/editor/keymap"
import { useSettingsStore } from "@/src/stores/settings-store"
@ -7,72 +8,77 @@
export default {
props: [
"userKeys"
"userKeys",
"modelValue",
],
components: {
KeyBindRow,
draggable,
},
data() {
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: {
...mapState(useSettingsStore, [
"settings",
]),
keymapOld() {
const userKeys = [
{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,
}
})
fixedKeymap() {
const defaultKeymap = (this.settings.keymap === "emacs" ? EMACS_KEYMAP : []).map((km) => ({
key: km.key,
command: km.command,
isDefault: true,
source: "Emacs",
}))
return defaultKeymap.concat(
DEFAULT_KEYMAP.map((km) => ({
key: km.key,
command: km.command,
isDefault: true,
source: "Default",
}))
)
},
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,
command: km.command,
isDefault: true,
isOverridden: km.key in this.userKeys,
}
}),
]
}
},
methods: {
onDragEnd(event) {
console.log("onDragEnd", this.testKeymap)
this.$emit("update:modelValue", this.keymap)
},
},
}
</script>
@ -82,19 +88,41 @@
<h2>Keyboard Bindings</h2>
<table>
<tr>
<th></th>
<th>Source</th>
<th>Key</th>
<th>Command</th>
<th></th>
</tr>
<KeyBindRow
v-for="key in keymap"
:key="key.key"
:keys="key.key"
:command="key.command"
:isOverridden="key.isOverridden"
:isDefault="key.isDefault"
/>
<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
:keys="element.key"
:command="element.command"
:isDefault="element.isDefault"
source="User"
/>
</template>
</draggable>
<tbody>
<KeyBindRow
v-for="key in fixedKeymap"
:key="key.key"
:keys="key.key"
:command="key.command"
:isDefault="key.isDefault"
:source="key.source"
/>
</tbody>
</table>
</div>
</template>
@ -111,16 +139,31 @@
border: 2px solid #f1f1f1
+dark-mode
background: #3c3c3c
background: #333
border: 2px solid #3c3c3c
::v-deep(tr)
&:nth-child(2n)
background: #fff
background: #fff
border-bottom: 2px solid #f1f1f1
+dark-mode
background: #333
border-bottom: 2px solid #3c3c3c
&.ghost
background: #48b57e
color: #fff
+dark-mode
background: #333
background: #1b6540
th
text-align: left
font-weight: 600
th, td
padding: 8px
&.actions
padding: 6px
button
height: 20px
font-size: 11px
tbody
margin-bottom: 20px
</style>

View File

@ -86,6 +86,12 @@
window.removeEventListener("keydown", this.onKeyDown);
},
watch: {
keyBindings(newKeyBindings) {
this.updateSettings()
}
},
methods: {
onKeyDown(event) {
if (event.key === "Escape") {
@ -98,7 +104,7 @@
showLineNumberGutter: this.showLineNumberGutter,
showFoldGutter: this.showFoldGutter,
keymap: this.keymap,
keyBindings: toRaw(this.keyBindings),
keyBindings: this.keyBindings.map((kb) => toRaw(kb)),
emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt",
allowBetaVersions: this.allowBetaVersions,
enableGlobalHotkey: this.enableGlobalHotkey,
@ -369,6 +375,7 @@
</div>
<KeyboardBindings
:userKeys="keyBindings ? keyBindings : {}"
v-model="keyBindings"
/>
</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 { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.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 { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js"
@ -29,15 +29,6 @@ import { useHeynoteStore } from "../stores/heynote-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 {
constructor({
element,

View File

@ -3,30 +3,6 @@ import { Prec } from "@codemirror/state"
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 cmdShift = (key, command, shiftCommand) => {
return [
@ -99,8 +75,6 @@ export const DEFAULT_KEYMAP = [
cmd("Shift-Tab", "indentLess"),
//cmd("Alt-ArrowLeft", "cursorSubwordBackward"),
//cmd("Alt-ArrowRight", "cursorSubwordForward"),
cmd("Ctrl-Space", "toggleEmacsMarkMode"),
cmd("Ctrl-g", "emacsCancel"),
cmd("Mod-l", "openLanguageSelector"),
cmd("Mod-p", "openBufferSelector"),
@ -146,27 +120,46 @@ export const EMACS_KEYMAP = [
...cmdShift("Ctrl-f", "cursorCharRight", "selectCharRight"),
...cmdShift("Ctrl-a", "cursorLineStart", "selectLineStart"),
...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) {
//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 [
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,
)
}