Skip to content
This repository was archived by the owner on Jun 27, 2024. It is now read-only.

Commit 5ab5f30

Browse files
authored
feat(keybindings): add a way to view & set keybindings (#50)
closes #49
1 parent 2b93d94 commit 5ab5f30

File tree

10 files changed

+303
-44
lines changed

10 files changed

+303
-44
lines changed

index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,26 @@
219219
#appUpdates a{
220220
text-decoration:none;
221221
}
222+
223+
.shortcutCommand.error{
224+
color:red;
225+
}
226+
227+
input.error::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
228+
color: red;
229+
opacity: 1; /* Firefox */
230+
}
231+
232+
#shortcuts .error input:-ms-input-placeholder { /* Internet Explorer 10-11 */
233+
color: red;
234+
}
235+
236+
#shortcuts .error input::-ms-input-placeholder { /* Microsoft Edge */
237+
color: red;
238+
}
239+
240+
241+
222242
</style>
223243
</head>
224244
<body>

locales/de.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,18 @@
2424
"STL (ASCII)":"STL (ASCII)",
2525
"AMF (experimental)":"AMF (experimentell)",
2626
"update":"aktualisieren",
27-
"instant update":"automatisch aktualisieren"
27+
"instant update":"automatisch aktualisieren",
28+
"keyboard shortcuts":"Tastenkombinationen",
29+
"command":"befehl",
30+
"keybinding":"Tastenkombination",
31+
"when":"wann",
32+
"always":"immer",
33+
"front":"forne",
34+
"back":"hinten",
35+
"top":"oben",
36+
"bottom":"unten",
37+
"left":"links",
38+
"right":"rechts",
39+
"perspective":"perspektive",
40+
"orthographic":"orthographic"
2841
}

locales/en.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,18 @@
2424
"STL (ASCII)":"",
2525
"AMF (experimental)":"",
2626
"update":"",
27-
"instant update":""
27+
"instant update":"",
28+
"keyboard shortcuts":"",
29+
"command":"",
30+
"keybinding":"",
31+
"when":"",
32+
"always":"",
33+
"front":"",
34+
"back":"",
35+
"top":"",
36+
"bottom":"",
37+
"left":"",
38+
"right":"",
39+
"perspective":"",
40+
"orthographic":""
2841
}

locales/fr.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,18 @@
2424
"STL (ASCII)":"STL (ASCII)",
2525
"AMF (experimental)":"AMF (experimental)",
2626
"update":"actualiser",
27-
"instant update":"actualisation instantanée"
27+
"instant update":"actualisation instantanée",
28+
"keyboard shortcuts":"racourcis clavier",
29+
"command":"commande",
30+
"keybinding":"racourci",
31+
"when":"quand",
32+
"always":"toujours",
33+
"front":"devant",
34+
"back":"derrière",
35+
"top":"dessus",
36+
"bottom":"dessous",
37+
"left":"gauche",
38+
"right":"droite",
39+
"perspective":"perspective",
40+
"orthographic":"orthographique"
2841
}

src/app.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@ titleBar.sink(
5858

5959
//
6060
const settingsStorage = state => {
61-
const {themeName, design, locale} = state
61+
const {themeName, design, locale, shortcuts} = state
6262
const {name, mainPath, vtreeMode, paramDefinitions, paramDefaults, paramValues} = design
6363
return {
6464
themeName,
6565
locale,
66+
shortcuts,
6667
design: {
6768
name,
6869
mainPath,
@@ -211,9 +212,11 @@ const outToDom$ = state$
211212
const sameLocale = state.locale === previousState.locale
212213
const sameAvailableLanguages = state.availableLanguages === previousState.availableLanguages
213214

215+
const sameShortcuts = state.shortcuts === previousState.shortcuts
216+
214217
return sameParamDefinitions && sameParamValues && sameExportFormats && sameStatus && sameStyling &&
215218
sameAutoreload && sameInstantUpdate && sameError && sameShowOptions && samevtreeMode && sameAppUpdates &&
216-
sameLocale && sameAvailableLanguages
219+
sameLocale && sameAvailableLanguages && sameShortcuts
217220
})
218221
.combine(function (state, i18n) {
219222
return require('./ui/views/main')(state, paramsCallbacktoStream, i18n)

src/sideEffects/dom.js

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ const most = require('most')
22
const morph = require('morphdom')// require('nanomorph')
33
const {proxy} = require('most-proxy')
44
const { attach, stream } = proxy()
5-
// const {holdSubject} = require('../../node_modules/csg-viewer/')
6-
// require('../observable-utils/most-subject/index')
75

86
const out$ = stream
97
function domSink (outToDom$) {
@@ -36,40 +34,44 @@ let storedListeners = {
3634

3735
}
3836
function domSource () {
39-
function getElement (query) {
40-
let item = document.querySelectorAll(query)
41-
return item
37+
function getElements (query) {
38+
return Array.from(document.querySelectorAll(query))
4239
}
4340

4441
const select = function (query) {
4542
// console.log('selecting', query)
46-
const item = getElement(query)
43+
const items = getElements(query)
4744

4845
let outputStream
49-
if (!item || (item && item.length === 0)) {
50-
const eventProxy = proxy()
51-
outputStream = eventProxy.stream
52-
storedListeners[query] = {observable: eventProxy, live: false}
53-
}
5446

5547
return {events: function events (eventName) {
48+
if (!items || (items && items.length === 0)) {
49+
const eventProxy = proxy()
50+
outputStream = eventProxy.stream
51+
storedListeners[query + '@@' + eventName] = {observable: eventProxy, live: false}
52+
}
5653
// eventsForListners[query] = eventName
57-
storedListeners[query].events = eventName
58-
return outputStream
54+
storedListeners[query + '@@' + eventName].events = eventName
55+
return outputStream.multicast()
5956
}}
6057
}
6158

6259
out$.forEach(function () {
6360
// console.log('dom source watching dom change')
64-
Object.keys(storedListeners).forEach(function (query) {
65-
const item = getElement(query)
66-
if (item) {
67-
const storedListener = storedListeners[query]
68-
if (item.length === 1 && storedListener.live === false) {
69-
// console.log('HURRAY NOW I HAVE SOMETHING !!')
70-
const realObservable = most.fromEvent(storedListener.events, item[0])
71-
storedListener.observable.attach(realObservable)
61+
Object.keys(storedListeners).forEach(function (queryAndEventName) {
62+
const [query, eventName] = queryAndEventName.split('@@')
63+
const items = getElements(query)
64+
if (items && items.length > 0) {
65+
const storedListener = storedListeners[queryAndEventName]
66+
if (storedListener.live === false) {
7267
storedListener.live = true
68+
69+
const itemObs = items.map(item => {
70+
// console.log('HURRAY NOW I HAVE SOMETHING !!')
71+
return most.fromEvent(storedListener.events, item)
72+
})
73+
const realObservable = most.mergeArray(itemObs)
74+
storedListener.observable.attach(realObservable)
7375
}
7476
}
7577
})

src/ui/actions.js

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,138 @@ function compositeKeyFromKeyEvent (event) {
1818
const compositeKey = `${ctrl}${shift}${meta}${key}`
1919
return compositeKey
2020
}
21+
const simpleKey = (event) => {
22+
return event.key ? event.key.toLowerCase() : undefined
23+
}
2124

22-
const makeActions = (sources) => {
23-
/* sources.watcher.forEach(function (data) {
24-
console.log('watchedFile', data)
25-
})
26-
sources.drops.forEach(function (data) {
27-
console.log('drop', data)
28-
})
29-
sources.fs.forEach(function (data) {
30-
console.log('fs operations', data)
31-
})
32-
sources.paramChanges.forEach(function (data) {
33-
console.log('param changes', data)
34-
}) */
25+
const getKeyCombos = (options, keyUps$, keyDown$) => {
26+
const defaults = {
27+
dropRepeats: false
28+
}
29+
const {dropRepeats} = Object.assign({}, defaults, options)
30+
31+
keyDown$ = keyDown$.multicast().debounce(10)
32+
if (dropRepeats) {
33+
keyDown$ = keyDown$
34+
.skipRepeatsWith((event, previousEvent) => {
35+
return simpleKey(event) === simpleKey(previousEvent)
36+
})
37+
}
3538

39+
const keyStuffEnd$ = keyDown$.throttle(1000).delay(2000)
40+
const keyCombos$ = keyDown$
41+
.merge(keyUps$.map(x => 'end'))
42+
.merge(keyStuffEnd$.map(x => 'end'))
43+
.loop((values, event) => {
44+
if (event === 'end' || simpleKey(event) === 'enter' || simpleKey(event) === 'escape') {
45+
const value = {
46+
event: values.length > 0 ? values[0].event : undefined,
47+
compositeKey: values.map(x => x.compositeKey).join('+')
48+
}
49+
return {seed: [], value}
50+
} else {
51+
const compositeKey = simpleKey(event)
52+
values.push({event, compositeKey})
53+
}
54+
return {seed: values}
55+
}, [])
56+
.filter(x => x !== undefined)
57+
.filter(x => x.event !== undefined)
58+
// .tap(x => console.log('key stuff', x))
59+
.multicast()
60+
61+
return keyCombos$
62+
}
63+
64+
const makeActions = (sources) => {
3665
// keyboard shortcut handling
37-
const keyDowns$ = most.fromEvent('keyup', document)
38-
const actionsFromKey$ = most.sample(function (event, state) {
39-
const compositeKey = compositeKeyFromKeyEvent(event)
66+
const keyUps$ = most.fromEvent('keyup', document).multicast()
67+
const keyDown$ = most.fromEvent('keydown', document).multicast()
68+
// we get all key combos, accepting repeated key strokes
69+
const keyCombos$ = getKeyCombos({dropRepeats: false}, keyUps$, keyDown$)
70+
71+
// we match key stroke combos to actions
72+
const actionsFromKey$ = most.sample(function ({event, compositeKey}, state) {
4073
const matchingAction = head(state.shortcuts.filter(shortcut => shortcut.key === compositeKey))
4174
if (matchingAction) {
4275
const {command, args} = matchingAction
4376
return {type: command, data: args}
4477
}
4578
return undefined
46-
}, keyDowns$, keyDowns$, sources.state$)
79+
}, keyCombos$, keyCombos$, sources.state$)
80+
.filter(x => x !== undefined)
81+
82+
// set shortcuts
83+
const setShortcuts$ = most.mergeArray([
84+
sources.store
85+
.filter(data => data && data.shortcuts)
86+
.map(data => data.shortcuts)
87+
])
88+
.map(data => ({type: 'setShortcuts', data}))
89+
90+
// set a specific shortcut
91+
const shortcutCommandUp$ = sources.dom.select('.shortcutCommand').events('keyup').multicast()
92+
const shortcutCommandDown$ = sources.dom.select('.shortcutCommand').events('keydown').multicast()
93+
const shortcutCommandKey$ = getKeyCombos(
94+
{dropRepeats: true},
95+
shortcutCommandUp$,
96+
shortcutCommandDown$
97+
)
98+
shortcutCommandUp$
99+
.forEach((event) => {
100+
event.preventDefault()
101+
event.stopPropagation()
102+
return false
103+
})
104+
shortcutCommandDown$
105+
.forEach((event) => {
106+
event.preventDefault()
107+
event.stopPropagation()
108+
return false
109+
})
110+
111+
const setShortcut$ = most.mergeArray([
112+
shortcutCommandKey$
113+
.map(({event, compositeKey}) => ({event, compositeKey, inProgress: true}))
114+
115+
.merge(
116+
sources.dom.select('.shortcutCommand').events('focus')
117+
.map(event => ({event, compositeKey: '', inProgress: true}))
118+
)
119+
.merge(
120+
sources.dom.select('.shortcutCommand').events('blur')
121+
.map(event => ({event, compositeKey: '', inProgress: false}))
122+
)
123+
.merge(
124+
shortcutCommandUp$
125+
.filter(event => simpleKey(event) === 'escape')
126+
.map(event => ({event, compositeKey: '', inProgress: false}))
127+
)
128+
.merge(
129+
shortcutCommandUp$
130+
.filter(event => simpleKey(event) === 'enter')
131+
.map(event => ({event, done: true}))
132+
)
133+
.scan(function (acc, current) {
134+
const {event, compositeKey, inProgress, done} = current
135+
const command = event.target.dataset.command
136+
const args = event.target.dataset.args
137+
138+
let updated = Object.assign({}, acc, {command, args, inProgress, done})
139+
if (compositeKey !== undefined && compositeKey !== '') {
140+
updated.key = compositeKey
141+
updated.tmpKey = compositeKey
142+
}
143+
if ('done' in updated && updated.done) {
144+
delete updated.inProgress
145+
delete updated.tmpKey
146+
}
147+
return updated
148+
}, {})
47149
.filter(x => x !== undefined)
150+
])
151+
.map(data => ({type: 'setShortcut', data}))
152+
.multicast()
48153

49154
const changeTheme$ = most.mergeArray([
50155
sources.dom.select('#themeSwitcher').events('change')
@@ -105,6 +210,9 @@ const makeActions = (sources) => {
105210
return {
106211
// generic key shortuct handler
107212
actionsFromKey$,
213+
// set shortcut(s)
214+
setShortcuts$,
215+
setShortcut$,
108216
// generic clear error action
109217
clearErrors$,
110218
setErrors$,

0 commit comments

Comments
 (0)