mirror of
https://github.com/heyman/heynote.git
synced 2026-02-06 11:27:25 +00:00
Improve how we display keybindings
Use shorter label version with ⇧, ⌘, ⌥, etc when displaying key bindings in the UI Use dynamic key bindings in tooltips (instead of hardcoded strings), so that the new key binding is displayed if a default has been overridden.
This commit is contained in:
parent
eea8aa3924
commit
28b0b006e5
@ -2,7 +2,13 @@
|
||||
|
||||
Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases).
|
||||
|
||||
## 2.5.0 (not yet released)
|
||||
## Next version (not yet released)
|
||||
|
||||
- When displaying key bindings show shorter version with ⇧, ⌘, ⌥, etc instead of Shift, Cmd, Alt, etc
|
||||
- Fixed so that tooltips displays new key binding if the default have been overridden
|
||||
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### Tabs
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { mapState } from 'pinia'
|
||||
import UpdateStatusItem from './UpdateStatusItem.vue'
|
||||
import { LANGUAGES } from '../editor/languages.js'
|
||||
import { getKeyBindingForCommand } from '../editor/keymap.js'
|
||||
import { useHeynoteStore } from "../stores/heynote-store"
|
||||
import { useSettingsStore } from "../stores/settings-store"
|
||||
|
||||
@ -40,6 +41,7 @@
|
||||
...mapState(useSettingsStore, [
|
||||
"spellcheckEnabled",
|
||||
"alwaysOnTop",
|
||||
"settings",
|
||||
]),
|
||||
|
||||
languageName() {
|
||||
@ -63,14 +65,6 @@
|
||||
return `Format Block Content (Alt + Shift + F)`
|
||||
},
|
||||
|
||||
changeNoteTitle() {
|
||||
return `Change Note (${this.cmdKey} + P)`
|
||||
},
|
||||
|
||||
changeLanguageTitle() {
|
||||
return `Change language for current block (${this.cmdKey} + L)`
|
||||
},
|
||||
|
||||
updatesEnabled() {
|
||||
return !!window.heynote.autoUpdate
|
||||
},
|
||||
@ -81,6 +75,11 @@
|
||||
event.preventDefault()
|
||||
window.heynote.mainProcess.invoke('showSpellcheckingContextMenu')
|
||||
},
|
||||
|
||||
getTooltip(text, command) {
|
||||
const binding = getKeyBindingForCommand(command, this.settings.keymap, this.settings.keyBindings, this.settings.emacsMetaKey)
|
||||
return !binding ? text : `${text} (${binding})`
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -98,14 +97,14 @@
|
||||
<div
|
||||
@click.stop="$emit('openBufferSelector')"
|
||||
class="status-block note clickable"
|
||||
:title="changeNoteTitle"
|
||||
:title="getTooltip('Change Note', 'openBufferSelector')"
|
||||
>
|
||||
{{ currentBufferName }}
|
||||
</div>
|
||||
<div
|
||||
@click.stop="$emit('openLanguageSelector')"
|
||||
class="status-block lang clickable"
|
||||
:title="changeLanguageTitle"
|
||||
:title="getTooltip('Change language for current block', 'openLanguageSelector')"
|
||||
>
|
||||
{{ languageName }}
|
||||
<span v-if="currentLanguageAuto" class="auto">(auto)</span>
|
||||
@ -114,7 +113,7 @@
|
||||
v-if="supportsFormat"
|
||||
@click.stop="$emit('formatCurrentBlock')"
|
||||
class="status-block format clickable"
|
||||
:title="formatBlockTitle"
|
||||
:title="getTooltip('Format Block Content', 'formatBlockContent')"
|
||||
>
|
||||
<span class="icon icon-format"></span>
|
||||
</div>
|
||||
@ -123,7 +122,7 @@
|
||||
@mousedown.prevent
|
||||
@contextmenu="onSpellcheckingContextMenu"
|
||||
:class="'status-block spellcheck clickable' + (this.spellcheckEnabled ? ' spellcheck-enabled' : '')"
|
||||
title="Spellchecking"
|
||||
:title="getTooltip('Spellchecking', 'toggleSpellcheck')"
|
||||
>
|
||||
<span class="icon icon-format"></span>
|
||||
</div>
|
||||
@ -133,7 +132,7 @@
|
||||
@click.stop="$emit('toggleAlwaysOnTop')"
|
||||
@mousedown.prevent
|
||||
class="status-block pin clickable"
|
||||
title="Pin"
|
||||
:title="getTooltip('Pin', 'toggleAlwaysOnTop')"
|
||||
>
|
||||
<span class="icon icon-format" :class="{'pinned': alwaysOnTop}"></span>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
|
||||
import { HEYNOTE_COMMANDS } from '@/src/editor/commands'
|
||||
import { getKeyBindingLabel } from '@/src/editor/keymap'
|
||||
import { useSettingsStore } from '@/src/stores/settings-store'
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
@ -10,11 +15,10 @@
|
||||
],
|
||||
|
||||
computed: {
|
||||
...mapState(useSettingsStore, ["settings"]),
|
||||
|
||||
formattedKeys() {
|
||||
return this.keys.replaceAll(
|
||||
"Mod",
|
||||
window.heynote.platform.isMac ? "⌘" : "Ctrl",
|
||||
)
|
||||
return getKeyBindingLabel(this.keys, this.settings.emacsMetaKey, " ")
|
||||
},
|
||||
|
||||
commandLabel() {
|
||||
@ -38,9 +42,7 @@
|
||||
{{ source }}
|
||||
</td>
|
||||
<td class="key">
|
||||
<template v-if="keys">
|
||||
{{ formattedKeys }}
|
||||
</template>
|
||||
<span v-if="keys" v-html="formattedKeys" />
|
||||
</td>
|
||||
<td class="command">
|
||||
<span class="command-name">{{ commandLabel }}</span>
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
|
||||
import { getKeyBindingLabel } from '@/src/editor/keymap'
|
||||
import { useSettingsStore } from '@/src/stores/settings-store'
|
||||
|
||||
import { keyName, base } from "w3c-keyname"
|
||||
|
||||
export default {
|
||||
@ -13,9 +18,15 @@
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useSettingsStore, ["settings"]),
|
||||
|
||||
key() {
|
||||
return this.keys.join(" ")
|
||||
},
|
||||
|
||||
keyLabel() {
|
||||
return getKeyBindingLabel(this.key, this.settings.emacsMetaKey, " ")
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -80,7 +91,7 @@
|
||||
<template>
|
||||
<input
|
||||
type="text"
|
||||
:value="key"
|
||||
:value="keyLabel"
|
||||
@keydown.prevent="onKeyDown"
|
||||
class="keys"
|
||||
readonly
|
||||
|
||||
@ -84,9 +84,11 @@ const switchToLastTab = (editor) => () => {
|
||||
}
|
||||
const nextTab = (editor) => () => {
|
||||
useHeynoteStore().nextTab()
|
||||
return true
|
||||
}
|
||||
const previousTab = (editor) => () => {
|
||||
useHeynoteStore().previousTab()
|
||||
return true
|
||||
}
|
||||
|
||||
export function toggleAlwaysOnTop(editor) {
|
||||
@ -151,6 +153,7 @@ const HEYNOTE_COMMANDS = {
|
||||
"switchToTab" + (i+1),
|
||||
cmdLessContext(() => {
|
||||
useHeynoteStore().switchToTabIndex(i)
|
||||
return true
|
||||
}, "Buffer", `Switch to tab ${i+1}`),
|
||||
])),
|
||||
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { keymap } from "@codemirror/view"
|
||||
import { Prec } from "@codemirror/state"
|
||||
|
||||
import { keyName } from "w3c-keyname"
|
||||
|
||||
|
||||
import { HEYNOTE_COMMANDS } from "./commands.js"
|
||||
|
||||
|
||||
const cmd = (key, command, scope) => ({key, command, scope})
|
||||
const cmdShift = (key, command, shiftCommand) => {
|
||||
return [
|
||||
@ -203,19 +207,108 @@ function keymapFromSpec(specs, editor) {
|
||||
}
|
||||
|
||||
|
||||
export function heynoteKeymap(editor, keymap, userKeymap) {
|
||||
function getCombinedKeymapSpec(keymapName, userKeymap) {
|
||||
return [
|
||||
keymapFromSpec([
|
||||
...userKeymap,
|
||||
...keymap,
|
||||
], editor),
|
||||
...userKeymap,
|
||||
...(keymapName === "emacs" ? [...EMACS_KEYMAP, ...DEFAULT_KEYMAP] : [...DEFAULT_NOT_EMACS_KEYMAP, ...DEFAULT_KEYMAP]),
|
||||
]
|
||||
}
|
||||
|
||||
export function getKeymapExtensions(editor, keymap, keyBindings) {
|
||||
return heynoteKeymap(
|
||||
editor,
|
||||
keymap === "emacs" ? [...EMACS_KEYMAP, ...DEFAULT_KEYMAP] : [...DEFAULT_NOT_EMACS_KEYMAP, ...DEFAULT_KEYMAP],
|
||||
keyBindings || [],
|
||||
)
|
||||
return [
|
||||
keymapFromSpec(getCombinedKeymapSpec(keymap, keyBindings), editor)
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Human readable version of a key binding
|
||||
*/
|
||||
export function getKeyBindingLabel(binding, emacsMetaKey, separator=" ") {
|
||||
emacsMetaKey = emacsMetaKey === "meta" ? "Meta" : (emacsMetaKey === "alt" ? "Alt" : emacsMetaKey)
|
||||
|
||||
const parts = binding.split(" ")
|
||||
return parts.map((part) => {
|
||||
return part.split("-").map((key) => {
|
||||
switch(key) {
|
||||
case "Mod":
|
||||
return window.heynote.platform.isMac ? "⌘" : "Ctrl"
|
||||
case "Alt":
|
||||
return window.heynote.platform.isMac ? "⌥" : "Alt"
|
||||
case "EmacsMeta":
|
||||
return emacsMetaKey === "Meta" ? "Meta" : (emacsMetaKey === "Alt" ? "Alt" : emacsMetaKey)
|
||||
case "Meta":
|
||||
return window.heynote.platform.isMac ? "⌘" : "Meta"
|
||||
case "Shift":
|
||||
return "⇧"
|
||||
case "Control":
|
||||
return "Ctrl"
|
||||
}
|
||||
if (key.match(/^[a-z]$/)) {
|
||||
return key.toUpperCase()
|
||||
}
|
||||
return key
|
||||
}).join(parts.length === 1 ? " + " : "+")
|
||||
}).join(separator)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function canonicalizeKey(keyString) {
|
||||
const strokes = keyString.trim().split(/\s+/)
|
||||
return strokes.map(stroke => _canonicalizeSingleStroke(stroke)).join(' ')
|
||||
}
|
||||
function _canonicalizeSingleStroke(strokeString) {
|
||||
const parts = strokeString.split('-')
|
||||
const key = parts.pop()
|
||||
const modifiers = parts.map(mod => mod.toLowerCase())
|
||||
|
||||
const normalizedModifiers = modifiers.map(mod => {
|
||||
switch (mod) {
|
||||
case 'mod': return isMac ? 'meta' : 'ctrl'
|
||||
case 'control': case 'ctrl': return 'ctrl'
|
||||
case 'shift': return 'shift'
|
||||
case 'alt': case 'option': return 'alt'
|
||||
case 'meta': case 'cmd': case 'command': return 'meta'
|
||||
default: return mod
|
||||
}
|
||||
})
|
||||
|
||||
const order = ['ctrl', 'alt', 'shift', 'meta']
|
||||
const sortedModifiers = normalizedModifiers.sort((a, b) => {
|
||||
return order.indexOf(a) - order.indexOf(b)
|
||||
})
|
||||
|
||||
const uniqueModifiers = [...new Set(sortedModifiers)]
|
||||
|
||||
return uniqueModifiers.length > 0
|
||||
? uniqueModifiers.join('-') + '-' + key.toLowerCase()
|
||||
: key.toLowerCase()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the first bound key for a command (in label format, i.e. Mod replaced with ⌘ etc.)
|
||||
*/
|
||||
export function getKeyBindingForCommand(command, keymapName, userKeymap, emacsMetaKey) {
|
||||
const capturingCommands = new Set([
|
||||
"nothing",
|
||||
"toggleAlwaysOnTop",
|
||||
"openLanguageSelector", "openBufferSelector", "openCreateNewBuffer", "openMoveToBuffer", "openCommandPalette",
|
||||
"closeCurrentTab", "reopenLastClosedTab", "nextTab", "previousTab",
|
||||
"switchToTab1", "switchToTab2", "switchToTab3", "switchToTab4", "switchToTab5", "switchToTab6", "switchToTab7", "switchToTab8", "switchToTab9", "switchToLastTab"
|
||||
])
|
||||
|
||||
const capturedKeys = new Set()
|
||||
|
||||
|
||||
for (const binding of getCombinedKeymapSpec(keymapName, userKeymap)) {
|
||||
const key = canonicalizeKey(binding.key)
|
||||
if (binding.command === command && !capturedKeys.has(key)) {
|
||||
return getKeyBindingLabel(binding.key, emacsMetaKey)
|
||||
}
|
||||
|
||||
if (capturingCommands.has(binding.command)) {
|
||||
capturedKeys.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,11 @@ test("add custom key binding", async ({page}) => {
|
||||
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
|
||||
await page.locator("body").press("Control+Shift+H")
|
||||
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
|
||||
await page.locator("body").press("Escape")
|
||||
await expect(page.locator("css=.status .status-block.lang")).toHaveAttribute(
|
||||
"title",
|
||||
"Change language for current block (Ctrl + ⇧ + H)"
|
||||
)
|
||||
})
|
||||
|
||||
test("delete custom key binding", async ({page}) => {
|
||||
@ -70,4 +75,8 @@ test("disable default key binding", async ({page}) => {
|
||||
await heynotePage.setSettings(settings)
|
||||
await page.locator("body").press(langKey)
|
||||
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
|
||||
await expect(page.locator("css=.status .status-block.lang")).toHaveAttribute(
|
||||
"title",
|
||||
"Change language for current block"
|
||||
)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user