diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index af7048565..6729ca0af 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -115,7 +115,8 @@ "ctIDE": "ct.IDE", "exportDesktop": "Export for desktop", "texture": "Textures", - "launch": "Compile", + "launch": "Compile and run", + "launchHotkeys": "(F5; Alt+F5 to run in your default browser)", "license": "License", "min": "Windowed", "modules": "Catmods", diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index c3cb2eab2..9af871e01 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -116,6 +116,7 @@ "exportDesktop": "Экспортировать для ПК", "texture": "Текстуры", "launch": "Скомпилировать и запустить", + "launchHotkeys": "(F5, или Alt+F5, чтобы запустить в браузере по-умолчанию)", "license": "Лицензия", "min": "Переключить полноэкранный режим", "modules": "Котомоды", @@ -375,7 +376,7 @@ "thankAllPatrons": "Спасибо всем покровителям ComigoGames — нынешним и предыдущим —, ваша поддержка помогает Комиго двигаться вперёд, разрабатывая всё более крутые проги! :)", "becomeAPatron": "Стать покровителем", "aboutFillers": [ - "круты со всем сторон 😎", + "круты со всех сторон 😎", "— приятный собеседник 🤗", "ещё предстоит стать звездой 💫", "— просто чудо! ⭐️", diff --git a/src/node_requires/hotkeys.js b/src/node_requires/hotkeys.js new file mode 100644 index 000000000..67b3d7fe4 --- /dev/null +++ b/src/node_requires/hotkeys.js @@ -0,0 +1,151 @@ +/* From @github/hotkey + see https://github.com/github/hotkey/ */ +const isFormField = function(element) { + if (!(element instanceof HTMLElement)) { + return false; + } + var name = element.nodeName.toLowerCase(); + var type = (element.getAttribute('type') || '').toLowerCase(); + /* eslint no-mixed-operators: off*/ + return name === 'select' || + name === 'textarea' || + name === 'input' && + type !== 'submit' && + type !== 'reset' && + type !== 'checkbox' && + type !== 'radio' || + element.isContentEditable; +}; + +const getCode = e => '' + .concat(e.ctrlKey? 'Control+' : '') + .concat(e.altKey? 'Alt+' : '') + .concat(e.metaKey ? 'Meta+' : '') + .concat(e.key); + +const listenerRef = Symbol('keydownListener'); +const offDomEventsRef = Symbol('offDomEventsRef'); +const hotkeyRef = Symbol('hotkey'); + +class Hotkeys { + constructor(doc) { + this.document = doc; + this.document[hotkeyRef] = this; + this.scopeStack = []; + + this[offDomEventsRef] = new Map(); + + this[listenerRef] = e => { + const code = getCode(e); + this.trigger(code); + }; + this.document.body.addEventListener('keydown', this[listenerRef]); + } + + on(code, event) { + if (!this[offDomEventsRef].has(code)) { + this[offDomEventsRef].set(code, []); + } + if (this[offDomEventsRef].get(code).indexOf(event) === -1) { + this[offDomEventsRef].get(code).push(event); + } + } + off(code, event) { + if (event) { + const ind = this[offDomEventsRef].get(code).indexOf(event); + if (ind !== -1) { + this[offDomEventsRef].get(code).splice(ind, 1); + } + } else { + this[offDomEventsRef].set(code, []); + } + } + trigger(code) { + const offDom = this[offDomEventsRef].get(code); + if (offDom) { + for (const event of offDom) { + event(); + } + } + const elts = this.document.querySelectorAll(`[data-hotkey="${code}"]`); + if (this.scopeStack.length) { + // walk from the most recent scope to the last one + for (let i = this.scopeStack.length - 1; i >= 0; i--) { + const scope = this.scopeStack[i]; + for (const elt of elts) { + if (!elt.closest(`[data-hotkey-scope="${scope}"]`)) { + continue; + } + if (isFormField(elt)) { + elt.focus(); + } else { + elt.click(); + } + return; + } + } + } + // Look for all the elements if no scope + // is specified or no scoped elements were found + for (const elt of elts) { + if (isFormField(elt)) { + elt.focus(); + } else { + elt.click(); + } + return; + } + } + + get scope() { + return this.scopeStack[this.scopeStack.length - 1]; + } + set scope(val) { + if (Array.isArray(val)) { + this.scopeStack = val; + } else { + this.scopeStack = val.split(' '); + } + } + push(val) { + this.scopeStack.push(val); + } + pop() { + return this.scopeStack.pop(); + } + remove(val) { + const ind = this.scopeStack.indexOf(val); + if (val !== -1) { + this.scopeStack.splice(ind, 1); + } + return ind !== -1; + } + exit(val) { + const ind = this.scopeStack.indexOf(val); + if (val !== -1) { + this.scopeStack.splice(ind); + } + return ind !== -1; + } + cleanScope() { + this.scopeStack.length = 0; + } + inScope(val) { + return this.scopeStack.indexOf(val) !== -1; + } + + unmount() { + this.document.body.removeEventListener('keydown', this[listenerRef]); + } +} + +module.exports = function (doc) { + doc = doc || document; + if (!doc) { + throw new Error('Can\'t find the document object! Am I in a bare node.js context?!'); + } + if (hotkeyRef in doc) { + return doc[hotkeyRef]; + } + return new Hotkeys(doc); +}; diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index 4df6a4a7f..b996fc1ef 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -2,50 +2,50 @@ main-menu.flexcol nav.nogrow.flexrow(if="{window.currentProject}") ul#fullscreen.nav li.nbr(onclick="{toggleFullscreen}" title="{voc.min}") - i(class="icon-{fullscreen? 'minimize-2' : 'maximize-2'}") + i(class="icon-{fullscreen? 'minimize-2' : 'maximize-2'} (F11)" data-hotkey="F11") ul#app.nav.tabs li.it30#ctlogo(onclick="{ctClick}" title="{voc.ctIDE}") i.icon-menu li.it30(onclick="{changeTab('patrons')}" title="{voc.patrons}" class="{active: tab === 'patrons'}") i.icon-heart - li.it30(onclick="{saveProject}" title="{voc.save}") + li.it30(onclick="{saveProject}" title="{voc.save} (Control+S)" data-hotkey="Control+s") i.icon-save - li.nbr.it30(onclick="{runProject}" title="{voc.launch}") + li.nbr.it30(onclick="{runProject}" title="{voc.launch} {voc.launchHotkeys}" data-hotkey="F5") i.icon-play ul#mainnav.nav.tabs - li(onclick="{changeTab('settings')}" class="{active: tab === 'settings'}") + li(onclick="{changeTab('settings')}" class="{active: tab === 'settings'}" data-hotkey="Control+1" title="Control+1") i.icon-settings span {voc.settings} - li(onclick="{changeTab('modules')}" class="{active: tab === 'modules'}") + li(onclick="{changeTab('modules')}" class="{active: tab === 'modules'}" data-hotkey="Control+2" title="Control+2") i.icon-mod span {voc.modules} - li(onclick="{changeTab('texture')}" class="{active: tab === 'texture'}") + li(onclick="{changeTab('texture')}" class="{active: tab === 'texture'}" data-hotkey="Control+3" title="Control+3") i.icon-picture span {voc.texture} - li(onclick="{changeTab('ui')}" class="{active: tab === 'ui'}") + li(onclick="{changeTab('ui')}" class="{active: tab === 'ui'}" data-hotkey="Control+4" title="Control+4") i.icon-droplet span {voc.ui} - li(onclick="{changeTab('sounds')}" class="{active: tab === 'sounds'}") + li(onclick="{changeTab('sounds')}" class="{active: tab === 'sounds'}" data-hotkey="Control+5" title="Control+5") i.icon-headphones span {voc.sounds} - li(onclick="{changeTab('types')}" class="{active: tab === 'types'}") + li(onclick="{changeTab('types')}" class="{active: tab === 'types'}" data-hotkey="Control+6" title="Control+6") i.icon-user span {voc.types} - li(onclick="{changeTab('rooms')}" class="{active: tab === 'rooms'}") + li(onclick="{changeTab('rooms')}" class="{active: tab === 'rooms'}" data-hotkey="Control+7" title="Control+7") i.icon-map span {voc.rooms} div.flexitem.relative(if="{window.currentProject}") - settings-panel(show="{tab === 'settings'}") - modules-panel(show="{tab === 'modules'}") - textures-panel(show="{tab === 'texture'}") - ui-panel(show="{tab === 'ui'}") - sounds-panel(show="{tab === 'sounds'}") - types-panel(show="{tab === 'types'}") - rooms-panel(show="{tab === 'rooms'}") + settings-panel(show="{tab === 'settings'}" data-hotkey-scope="settings") + modules-panel(show="{tab === 'modules'}" data-hotkey-scope="modules") + textures-panel(show="{tab === 'texture'}" data-hotkey-scope="texture") + ui-panel(show="{tab === 'ui'}" data-hotkey-scope="ui") + sounds-panel(show="{tab === 'sounds'}" data-hotkey-scope="sounds") + types-panel(show="{tab === 'types'}" data-hotkey-scope="types") + rooms-panel(show="{tab === 'rooms'}" data-hotkey-scope="rooms") license-panel(if="{showLicense}") - patreon-screen(if="{tab === 'patrons'}") + patreon-screen(if="{tab === 'patrons'}" data-hotkey-scope="patrons") export-panel(show="{showExporter}") script. const fs = require('fs-extra'), @@ -54,12 +54,20 @@ main-menu.flexcol const runCtExport = require('./data/node_requires/exporter'); const glob = require('./data/node_requires/glob'); + // Mounts the hotkey plugins, enabling hotkeys on elements with data-hotkey attributes + const hotkey = require('./data/node_requires/hotkeys')(document); + this.on('unmount', () => { + hotkey.unmount(); + }); + this.namespace = 'menu'; this.mixin(window.riotVoc); this.tab = 'settings'; this.changeTab = tab => e => { this.tab = tab; + hotkey.cleanScope(); + hotkey.push(tab); window.signals.trigger('globalTabChanged'); window.signals.trigger(`${tab}Focus`); }; @@ -127,12 +135,6 @@ main-menu.flexcol window.signals.off('saveProject', this.saveProject); }); this.saveRecoveryDebounce(); - this.on('mount', () => { - keymage('ctrl-s', this.saveProject); - }); - this.on('unmount', () => { - keymage.unbind('ctrl-s', this.saveProject); - }); const {getWritableDir} = require('./data/node_requires/platformUtils'); let fileServer; @@ -189,6 +191,14 @@ main-menu.flexcol console.error(e); }); }; + this.runProjectAlt = e => { + runCtExport(currentProject, sessionStorage.projdir) + .then(path => { + console.log(path); + nw.Shell.openExternal(`http://localhost:${server.address().port}/`); + }); + }; + hotkey.on('Alt+F5', this.runProjectAlt); this.zipProject = async e => { const writable = await getWritableDir(); diff --git a/src/riotTags/notepad-panel.tag b/src/riotTags/notepad-panel.tag index 888ee822d..90cb3fde5 100644 --- a/src/riotTags/notepad-panel.tag +++ b/src/riotTags/notepad-panel.tag @@ -1,12 +1,12 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") ul.nav.tabs.nogrow - li(onclick="{changeTab('notepadlocal')}") + li(onclick="{changeTab('notepadlocal')}" class="{active: tab === 'notepadlocal'}") i.icon.icon-edit span {voc.local} - li(onclick="{changeTab('notepadglobal')}") + li(onclick="{changeTab('notepadglobal')}" class="{active: tab === 'notepadglobal'}") i.icon.icon-clipboard span {voc.global} - li(onclick="{changeTab('helppages')}") + li(onclick="{changeTab('helppages')}" class="{active: tab === 'helppages'}") i.icon.icon-life-buoy span {voc.helppages} div @@ -23,6 +23,7 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") i.icon(class="icon-{opened? 'chevron-right' : 'chevron-left'}") script. const glob = require('./data/node_requires/glob'); + const hotkey = require('./data/node_requires/hotkeys')(document); this.opened = false; this.namespace = 'notepad'; this.mixin(window.riotVoc); @@ -30,13 +31,19 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") this.opened = !this.opened; }; + hotkey.on('F1', () => { + this.opened = true; + this.tab = 'helppages'; + this.update(); + }); + this.tab = 'notepadlocal'; this.changeTab = tab => e => { this.tab = tab; }; this.on('update', () => { setTimeout(() => { - if (this.tab) { + if (this.tab && this.refs[this.tab].codeEditor) { this.refs[this.tab].codeEditor.layout(); this.refs[this.tab].codeEditor.focus(); } diff --git a/src/riotTags/room-events-editor.tag b/src/riotTags/room-events-editor.tag index dd7c1e621..1f54d7f02 100644 --- a/src/riotTags/room-events-editor.tag +++ b/src/riotTags/room-events-editor.tag @@ -1,16 +1,16 @@ room-events-editor.view.panel .tabwrap ul.tabs.nav.nogrow.noshrink - li(onclick="{switchTab('roomcreate')}" class="{active: tab === 'roomcreate'}") + li(onclick="{switchTab('roomcreate')}" class="{active: tab === 'roomcreate'}" title="Control-Q" data-hotkey="Control+q") i.icon.icon-sun span {voc.create} - li(onclick="{switchTab('roomstep')}" class="{active: tab === 'roomstep'}") + li(onclick="{switchTab('roomstep')}" class="{active: tab === 'roomstep'}" title="Control-W" data-hotkey="Control+w") i.icon.icon-next span {voc.step} - li(onclick="{switchTab('roomdraw')}" class="{active: tab === 'roomdraw'}") + li(onclick="{switchTab('roomdraw')}" class="{active: tab === 'roomdraw'}" title="Control-E" data-hotkey="Control+e") i.icon.icon-edit-2 span {voc.draw} - li(onclick="{switchTab('roomleave')}" class="{active: tab === 'roomleave'}") + li(onclick="{switchTab('roomleave')}" class="{active: tab === 'roomleave'}" title="Control-R" data-hotkey="Control+r") i.icon.icon-trash span {voc.leave} div(style="position: relative;") @@ -28,6 +28,7 @@ room-events-editor.view.panel script. this.namespace = 'roomview'; this.mixin(window.riotVoc); + this.tab = 'roomcreate'; const tabToEditor = tab => { tab = tab || this.tab; diff --git a/src/riotTags/rooms-panel.tag b/src/riotTags/rooms-panel.tag index 1ae140098..5905c9f79 100644 --- a/src/riotTags/rooms-panel.tag +++ b/src/riotTags/rooms-panel.tag @@ -11,7 +11,7 @@ rooms-panel.panel.view .aSearchWrap input.inline(type="text" onkeyup="{fuseSearch}") .toleft - button#roomcreate(onclick="{roomCreate}") + button#roomcreate(onclick="{roomCreate}" data-hotkey="Control+n" title="Control+N") i.icon.icon-add span {voc.create} ul.cards.rooms.flexfix-body @@ -93,7 +93,10 @@ rooms-panel.panel.view const gui = require('nw.gui'), fs = require('fs-extra'), path = require('path'); - this.roomCreate = function () { + this.roomCreate = function (e) { + if (this.editing) { + return false; + } var guid = generateGUID(), thumbnail = guid.split('-').pop(); fs.copy('./data/img/notexture.png', path.join(sessionStorage.projdir, '/img/r' + thumbnail + '.png'), () => { diff --git a/src/riotTags/script-editor.tag b/src/riotTags/script-editor.tag index a2d7bd649..34e068848 100644 --- a/src/riotTags/script-editor.tag +++ b/src/riotTags/script-editor.tag @@ -5,7 +5,7 @@ script-editor.view.panel input(type="text" value="{script.name}" onchange="{wire('this.script.name')}") .flexfix-body .aCodeEditor(ref="editor") - button.nm.flexfix-footer(onclick="{saveScript}") + button.nm.flexfix-footer(onclick="{saveScript}" title="Shift+Control+S" data-hotkey="Control+S") i.icon.icon-confirm span {voc.done} script. diff --git a/src/riotTags/sound-editor.tag b/src/riotTags/sound-editor.tag index 325b0a73f..279c70a78 100644 --- a/src/riotTags/sound-editor.tag +++ b/src/riotTags/sound-editor.tag @@ -25,7 +25,7 @@ sound-editor.panel.view span {voc.import} input(type="file" ref="inputsound" accept=".mp3,.ogg,.wav" onchange="{changeSoundFile}") p.nmb - button.wide(onclick="{soundSave}") + button.wide(onclick="{soundSave}" title="Shift+Control+S" data-hotkey="Control+S") i.icon.icon-confirm span {voc.save} script. diff --git a/src/riotTags/sounds-panel.tag b/src/riotTags/sounds-panel.tag index b59e3910b..be17ecbce 100644 --- a/src/riotTags/sounds-panel.tag +++ b/src/riotTags/sounds-panel.tag @@ -11,7 +11,7 @@ sounds-panel.panel.view .aSearchWrap input.inline(type="text" onkeyup="{fuseSearch}") .toleft - button#soundcreate(onclick="{soundNew}") + button#soundcreate(onclick="{soundNew}" title="Control+N" data-hotkey="Control+n") i.icon.icon-add span {voc.create} ul.cards.flexfix-body @@ -89,6 +89,9 @@ sounds-panel.panel.view const gui = require('nw.gui'); this.soundNew = e => { + if (this.editing) { + return false; + } const generateGUID = require('./data/node_requires/generateGUID'); var id = generateGUID(), slice = id.split('-').pop(); diff --git a/src/riotTags/style-editor.tag b/src/riotTags/style-editor.tag index dfc66bb08..dddb3af2d 100644 --- a/src/riotTags/style-editor.tag +++ b/src/riotTags/style-editor.tag @@ -117,7 +117,7 @@ style-editor.panel.view br input#shadowblur(type="number" value="{styleobj.shadow.blur}" min="0" onchange="{wire('this.styleobj.shadow.blur')}" oninput="{wire('this.styleobj.shadow.blur')}") .flexfix-footer - button.wide.nogrow.noshrink(onclick="{styleSave}") + button.wide.nogrow.noshrink(onclick="{styleSave}" title="Shift+Control+S" data-hotkey="Control+S") i.icon.icon-confirm span {voc.apply} #stylepreview.tall(ref="canvasSlot") diff --git a/src/riotTags/styles-panel.tag b/src/riotTags/styles-panel.tag index 67a040aa5..36b965aee 100644 --- a/src/riotTags/styles-panel.tag +++ b/src/riotTags/styles-panel.tag @@ -11,7 +11,7 @@ styles-panel.flexfix.tall.fifty h1.nmt {voc.styles} .clear .toleft - button#stylecreate(onclick="{styleCreate}") + button#stylecreate(onclick="{styleCreate}" title="Control+N" data-hotkey="Control+n") i.icon.icon-add span {voc.create} .clear @@ -78,7 +78,10 @@ styles-panel.flexfix.tall.fifty const gui = require('nw.gui'); - this.styleCreate = () => { + this.styleCreate = e => { + if (this.editingStyle) { + return; + } var id = generateGUID(), slice = id.split('-').pop(); window.currentProject.styletick ++; @@ -91,6 +94,10 @@ styles-panel.flexfix.tall.fifty this.editedStyle = obj; this.editingStyle = true; this.updateList(); + + if (!e) { + this.update(); + } }; this.openStyle = style => e => { this.editingStyle = true; diff --git a/src/riotTags/texture-editor.tag b/src/riotTags/texture-editor.tag index a37b3853d..d0a60b030 100644 --- a/src/riotTags/texture-editor.tag +++ b/src/riotTags/texture-editor.tag @@ -69,7 +69,7 @@ texture-editor.panel.view input(checked="{prevShowMask}" onchange="{wire('this.prevShowMask')}" type="checkbox") span {voc.showmask} .flexfix-footer - button.wide(onclick="{textureSave}") + button.wide(onclick="{textureSave}" title="Shift+Control+S" data-hotkey="Control+S") i.icon-save span {window.languageJSON.common.save} .texture-editor-anAtlas.tall( diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index b73addfc5..3cc03cbcb 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -37,22 +37,22 @@ type-editor.panel.view.flexrow br docs-shortcut(path="/ct.types.html" button="true" wide="true" title="{voc.learnAboutTypes}") .flexfix-footer - button#typedone.wide(onclick="{typeSave}") + button#typedone.wide(onclick="{typeSave}" title="Shift+Control+S" data-hotkey="Control+S") i.icon.icon-confirm span {voc.done} .c9.tall.borderleft .tabwrap.tall(style="position: relative") ul.tabs.nav.nogrow.noshrink - li(onclick="{changeTab('typeoncreate')}" class="{active: tab === 'typeoncreate'}" title="{voc.create}") + li(onclick="{changeTab('typeoncreate')}" class="{active: tab === 'typeoncreate'}" title="{voc.create} (Control+Q)" data-hotkey="Control+q") i.icon.icon-sun span {voc.create} - li(onclick="{changeTab('typeonstep')}" class="{active: tab === 'typeonstep'}" title="{voc.step}") + li(onclick="{changeTab('typeonstep')}" class="{active: tab === 'typeonstep'}" title="{voc.step} (Control+W)" data-hotkey="Control+w") i.icon.icon-next span {voc.step} - li(onclick="{changeTab('typeondraw')}" class="{active: tab === 'typeondraw'}" title="{voc.draw}") + li(onclick="{changeTab('typeondraw')}" class="{active: tab === 'typeondraw'}" title="{voc.draw} (Control+E)" data-hotkey="Control+e") i.icon.icon-edit-2 span {voc.draw} - li(onclick="{changeTab('typeondestroy')}" class="{active: tab === 'typeondestroy'}" title="{voc.destroy}") + li(onclick="{changeTab('typeondestroy')}" class="{active: tab === 'typeondestroy'}" title="{voc.destroy} (Control+R)" data-hotkey="Control+r") i.icon.icon-trash span {voc.destroy} div diff --git a/src/riotTags/types-panel.tag b/src/riotTags/types-panel.tag index 8c9021a61..88d0921ac 100644 --- a/src/riotTags/types-panel.tag +++ b/src/riotTags/types-panel.tag @@ -11,7 +11,7 @@ types-panel.panel.view .aSearchWrap input.inline(type="text" onkeyup="{fuseSearch}") .toleft - button#typecreate(onclick="{typeCreate}") + button#typecreate(onclick="{typeCreate}" title="Control+N" data-hotkey="Control+n") i.icon.icon-add span {voc.create} ul.cards.flexfix-body @@ -103,6 +103,9 @@ types-panel.panel.view } }; this.typeCreate = e => { + if (this.editingType) { + return false; + } var id = generateGUID(), slice = id.split('-').pop(); var obj = { @@ -120,6 +123,10 @@ types-panel.panel.view this.updateList(); this.openType(obj)(e); window.signals.trigger('typesChanged'); + + if (!e) { + this.update(); + } }; this.openType = type => e => { this.editingType = true;