From 0648d48ba1eed01f137ae9ddc138197fe7b4c0d7 Mon Sep 17 00:00:00 2001 From: Cole Lashley Date: Fri, 26 Jan 2024 10:50:11 -0800 Subject: [PATCH] Intenational key binding fixes (#234) * first pass of copy file * first pass fixing up function * first pass fixing key press * fixed up key press parsing * reverted cmdrunner.go * fixed cmdrunner.go again :p * fixed cmdrunner again lol * Add job status indicators to tabs within a workspace (#232) Adds job status indicators that will show any updates to running commands while you are focused away from a tab. These will show up as status icons in the tab view. These indicators will reset for a given tab when you focus back to it. I've updated the inner formatting of the tab to use flexboxes, allowing the title to display more text when there are no icons to display. Also includes some miscellaneous for-loop pattern improvements in model.ts and removing of unused variables, etc. * first pass fixing key press * added key util check file * addressed rebase artifacts * fixed more rebase artifacts * fixed keybindings * removed log * fixed a ton of rebase artifacts * fixed a ton of rebase artifacts * added cmd maps to altKey if not on macos * fixed platform check code and fixed some small bugs --- src/app/common/common.tsx | 6 +- src/app/history/history.tsx | 73 ++++++--- src/app/workspace/cmdinput/aichat.tsx | 14 +- src/app/workspace/cmdinput/textareainput.tsx | 71 +++++---- src/electron/emain.ts | 32 ++-- src/model/model.ts | 70 +++++---- src/plugins/code/code.tsx | 8 +- src/util/keyutil.ts | 153 +++++++++++++++++++ 8 files changed, 316 insertions(+), 111 deletions(-) create mode 100644 src/util/keyutil.ts diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 0f382348..752378e4 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -13,6 +13,7 @@ import { RemoteType, StatusIndicatorLevel } from "../../types/types"; import ReactDOM from "react-dom"; import { GlobalModel } from "../../model/model"; import * as appconst from "../appconst"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil"; import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg"; import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg"; @@ -714,13 +715,14 @@ class InlineSettingsTextEdit extends React.Component< @boundMethod handleKeyDown(e: any): void { - if (e.code == "Enter") { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Enter")) { e.preventDefault(); e.stopPropagation(); this.confirmChange(); return; } - if (e.code == "Escape") { + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); e.stopPropagation(); this.cancelChange(); diff --git a/src/app/history/history.tsx b/src/app/history/history.tsx index 8b217f92..547d4191 100644 --- a/src/app/history/history.tsx +++ b/src/app/history/history.tsx @@ -15,6 +15,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat"; import customParseFormat from "dayjs/plugin/customParseFormat"; import { Line } from "../line/linecomps"; import { CmdStrCode } from "../common/common"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil"; import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg"; import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; @@ -95,14 +96,14 @@ function formatSessionName(snames: Record, sessionId: string): s } @mobxReact.observer -class HistoryCheckbox extends React.Component<{ checked: boolean, partialCheck?: boolean, onClick?: () => void }, {}> { +class HistoryCheckbox extends React.Component<{ checked: boolean; partialCheck?: boolean; onClick?: () => void }, {}> { @boundMethod clickHandler(): void { if (this.props.onClick) { this.props.onClick(); } } - + render() { if (this.props.checked) { return ; @@ -110,14 +111,18 @@ class HistoryCheckbox extends React.Component<{ checked: boolean, partialCheck?: if (this.props.partialCheck) { return ( - - - + + + - ); - } - else { - return
+ ); + } else { + return
; } } } @@ -207,7 +212,8 @@ class HistoryView extends React.Component<{}, {}> { @boundMethod searchKeyDown(e: any) { - if (e.code == "Enter") { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Enter")) { e.preventDefault(); GlobalModel.historyViewModel.submitSearch(); return; @@ -452,8 +458,8 @@ class HistoryView extends React.Component<{}, {}> {
{hvm.searchSessionId.get() == null - ? "Limit Workspace" - : formatSessionName(snames, hvm.searchSessionId.get())} + ? "Limit Workspace" + : formatSessionName(snames, hvm.searchSessionId.get())}
@@ -486,8 +492,8 @@ class HistoryView extends React.Component<{}, {}> {
{hvm.searchRemoteId.get() == null - ? "Limit Remote" - : formatRemoteName(rnames, { remoteid: hvm.searchRemoteId.get() })} + ? "Limit Remote" + : formatRemoteName(rnames, { remoteid: hvm.searchRemoteId.get() })}
@@ -513,9 +519,7 @@ class HistoryView extends React.Component<{}, {}> {
-
- From:  -
+
From: 
{
- 0 && numSelected == items.length} partialCheck={numSelected > 0}/> + 0 && numSelected == items.length} + partialCheck={numSelected > 0} + />
{ > -  Delete Items +  Delete Items
@@ -591,7 +598,7 @@ class HistoryView extends React.Component<{}, {}> { className={cn("history-item", { "is-selected": hvm.selectedItems.get(item.historyid) })} > this.handleSelect(item.historyid)}> - + { {getHistoryViewTs(nowDate, item.ts)} this.activateItem(item.historyid)}> - - + + - - + + diff --git a/src/app/workspace/cmdinput/aichat.tsx b/src/app/workspace/cmdinput/aichat.tsx index debba909..ef65ef90 100644 --- a/src/app/workspace/cmdinput/aichat.tsx +++ b/src/app/workspace/cmdinput/aichat.tsx @@ -13,6 +13,7 @@ import { TextAreaInput } from "./textareainput"; import { If, For } from "tsx-control-statements/components"; import type { OpenAICmdInfoChatMessageType } from "../../../types/types"; import { Markdown } from "../../common/common"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil"; @mobxReact.observer class AIChat extends React.Component<{}, {}> { @@ -76,8 +77,8 @@ class AIChat extends React.Component<{}, {}> { let inputModel = model.inputModel; let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift"); let resetCodeSelect = !ctrlMod; - - if (e.code == "Enter") { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Enter")) { e.preventDefault(); if (!ctrlMod) { if (inputModel.getCodeSelectSelectedIndex() == -1) { @@ -91,17 +92,18 @@ class AIChat extends React.Component<{}, {}> { e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end"); } } - if (e.code == "Escape") { + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); e.stopPropagation(); inputModel.closeAIAssistantChat(); } - if (e.code == "KeyL" && e.getModifierState("Control")) { + + if (checkKeyPressed(waveEvent, "Ctrl:l")) { e.preventDefault(); e.stopPropagation(); inputModel.clearAIAssistantChat(); } - if (e.code == "ArrowUp") { + if (checkKeyPressed(waveEvent, "ArrowUp")) { if (this.getLinePos(e.target).linePos > 1) { // normal up arrow return; @@ -110,7 +112,7 @@ class AIChat extends React.Component<{}, {}> { inputModel.codeSelectSelectNextOldestCodeBlock(); resetCodeSelect = false; } - if (e.code == "ArrowDown") { + if (checkKeyPressed(waveEvent, "ArrowDown")) { if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) { return; } diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index b6036a67..dc1a119f 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -13,6 +13,7 @@ import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model"; import { getMonoFontSize } from "../../../util/textmeasure"; import { isModKeyPress, hasNoModifiers } from "../../../util/util"; import * as appconst from "../../appconst"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil"; type OV = mobx.IObservableValue; @@ -177,12 +178,13 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift"); let curLine = inputModel.getCurLine(); + let waveEvent = adaptFromReactOrNativeKeyEvent(e); let lastTab = this.lastTab; - this.lastTab = e.code == "Tab"; + this.lastTab = checkKeyPressed(waveEvent, "Tab"); let lastHist = this.lastHistoryUpDown; this.lastHistoryUpDown = false; - if (e.code == "Tab") { + if (checkKeyPressed(waveEvent, "Tab")) { e.preventDefault(); if (lastTab) { GlobalModel.submitCommand( @@ -204,7 +206,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () return; } } - if (e.code == "Enter") { + if (checkKeyPressed(waveEvent, "Enter")) { e.preventDefault(); if (!ctrlMod) { if (GlobalModel.inputModel.isEmpty()) { @@ -224,7 +226,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () GlobalModel.inputModel.setCurLine(e.target.value); return; } - if (e.code == "Escape") { + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); e.stopPropagation(); let inputModel = GlobalModel.inputModel; @@ -235,50 +237,50 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () inputModel.closeAIAssistantChat(); return; } - if (e.code == "KeyE" && e.getModifierState("Meta")) { + if (checkKeyPressed(waveEvent, "Cmd:e")) { e.preventDefault(); e.stopPropagation(); let inputModel = GlobalModel.inputModel; inputModel.toggleExpandInput(); } - if (e.code == "KeyC" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:c")) { e.preventDefault(); inputModel.resetInput(); return; } - if (e.code == "KeyU" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:u")) { e.preventDefault(); this.controlU(); return; } - if (e.code == "KeyP" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:p")) { e.preventDefault(); this.controlP(); return; } - if (e.code == "KeyN" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:n")) { e.preventDefault(); this.controlN(); return; } - if (e.code == "KeyW" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:w")) { e.preventDefault(); this.controlW(); return; } - if (e.code == "KeyY" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:y")) { e.preventDefault(); this.controlY(); return; } - if (e.code == "KeyR" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:r")) { e.preventDefault(); inputModel.openHistory(); return; } - if ((e.code == "ArrowUp" || e.code == "ArrowDown") && hasNoModifiers(e)) { + if (checkKeyPressed(waveEvent, "ArrowUp") || checkKeyPressed(waveEvent, "ArrowDown")) { if (!inputModel.isHistoryLoaded()) { - if (e.code == "ArrowUp") { + if (checkKeyPressed(waveEvent, "ArrowUp")) { this.lastHistoryUpDown = true; inputModel.loadHistory(false, 1, "screen"); } @@ -286,7 +288,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () } // invisible history movement let linePos = this.getLinePos(e.target); - if (e.code == "ArrowUp") { + if (checkKeyPressed(waveEvent, "ArrowUp")) { if (!lastHist && linePos.linePos > 1) { // regular arrow return; @@ -296,7 +298,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () this.lastHistoryUpDown = true; return; } - if (e.code == "ArrowDown") { + if (checkKeyPressed(waveEvent, "ArrowDown")) { if (!lastHist && linePos.linePos < linePos.numLines) { // regular arrow return; @@ -307,16 +309,16 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () return; } } - if (e.code == "PageUp" || e.code == "PageDown") { + if (checkKeyPressed(waveEvent, "PageUp") || checkKeyPressed(waveEvent, "PageDown")) { e.preventDefault(); let infoScroll = inputModel.hasScrollingInfoMsg(); if (infoScroll) { let div = document.querySelector(".cmd-input-info"); let amt = pageSize(div); - scrollDiv(div, e.code == "PageUp" ? -amt : amt); + scrollDiv(div, checkKeyPressed(waveEvent, "PageUp") ? -amt : amt); } } - if (e.code == "Space" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:Space")) { e.preventDefault(); inputModel.openAIAssistantChat(); } @@ -338,32 +340,29 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () @boundMethod onHistoryKeyDown(e: any) { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); let inputModel = GlobalModel.inputModel; - if (e.code == "Escape") { + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); inputModel.resetHistory(); return; } - if (e.code == "Enter") { + if (checkKeyPressed(waveEvent, "Enter")) { e.preventDefault(); inputModel.grabSelectedHistoryItem(); return; } - if (e.code == "KeyG" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:g")) { e.preventDefault(); inputModel.resetInput(); return; } - if (e.code == "KeyC" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:c")) { e.preventDefault(); inputModel.resetInput(); return; } - if ( - e.code == "KeyR" && - (e.getModifierState("Meta") || e.getModifierState("Control")) && - !e.getModifierState("Shift") - ) { + if (checkKeyPressed(waveEvent, "Cmd:r") || checkKeyPressed(waveEvent, "Ctrl:r")) { e.preventDefault(); let opts = mobx.toJS(inputModel.historyQueryOpts.get()); if (opts.limitRemote) { @@ -376,7 +375,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () inputModel.setHistoryQueryOpts(opts); return; } - if (e.code == "KeyS" && (e.getModifierState("Meta") || e.getModifierState("Control"))) { + if (checkKeyPressed(waveEvent, "Cmd:s") || checkKeyPressed(waveEvent, "Ctrl:s")) { e.preventDefault(); let opts = mobx.toJS(inputModel.historyQueryOpts.get()); let htype = opts.queryType; @@ -390,26 +389,26 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () inputModel.setHistoryType(htype); return; } - if (e.code == "Tab") { + if (checkKeyPressed(waveEvent, "Tab")) { e.preventDefault(); return; } - if (e.code == "ArrowUp" || e.code == "ArrowDown") { + if (checkKeyPressed(waveEvent, "ArrowUp") || checkKeyPressed(waveEvent, "ArrowDown")) { e.preventDefault(); - inputModel.moveHistorySelection(e.code == "ArrowUp" ? 1 : -1); + inputModel.moveHistorySelection(checkKeyPressed(waveEvent, "ArrowUp") ? 1 : -1); return; } - if (e.code == "PageUp" || e.code == "PageDown") { + if (checkKeyPressed(waveEvent, "PageUp") || checkKeyPressed(waveEvent, "PageDown")) { e.preventDefault(); - inputModel.moveHistorySelection(e.code == "PageUp" ? 10 : -10); + inputModel.moveHistorySelection(checkKeyPressed(waveEvent, "PageUp") ? 10 : -10); return; } - if (e.code == "KeyP" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:p")) { e.preventDefault(); inputModel.moveHistorySelection(1); return; } - if (e.code == "KeyN" && e.getModifierState("Control")) { + if (checkKeyPressed(waveEvent, "Ctrl:n")) { e.preventDefault(); inputModel.moveHistorySelection(-1); return; diff --git a/src/electron/emain.ts b/src/electron/emain.ts index 0820d7ff..a10d157d 100644 --- a/src/electron/emain.ts +++ b/src/electron/emain.ts @@ -12,6 +12,8 @@ import * as winston from "winston"; import * as util from "util"; import { sprintf } from "sprintf-js"; import { v4 as uuidv4 } from "uuid"; +import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "../util/keyutil"; +import { platform } from "os"; const WaveAppPathVarName = "WAVETERM_APP_PATH"; const WaveDevVarName = "WAVETERM_DEV"; @@ -189,7 +191,7 @@ let menuTemplate = [ }, { label: "File", - submenu: [{ role: "close" },], + submenu: [{ role: "close" }], }, { role: "editMenu", @@ -244,6 +246,7 @@ function shFrameNavHandler(event: any, url: any) { function createMainWindow(clientData) { let bounds = calcBounds(clientData); + setKeyUtilPlatform(platform()); let win = new electron.BrowserWindow({ x: bounds.x, y: bounds.y, @@ -261,6 +264,7 @@ function createMainWindow(clientData) { let indexHtml = isDev ? "index-dev.html" : "index.html"; win.loadFile(path.join(getAppBasePath(), "public", indexHtml)); win.webContents.on("before-input-event", (e, input) => { + let waveEvent = adaptFromElectronKeyEvent(input); if (win.isFocused()) { wasActive = true; } @@ -268,12 +272,12 @@ function createMainWindow(clientData) { return; } let mods = getMods(input); - if (input.code == "KeyT" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:t")) { win.webContents.send("t-cmd", mods); e.preventDefault(); return; } - if (input.code == "KeyI" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:i")) { e.preventDefault(); if (!input.alt) { win.webContents.send("i-cmd", mods); @@ -283,35 +287,35 @@ function createMainWindow(clientData) { return; } - if (input.code == "KeyR" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:r")) { if (input.shift) { e.preventDefault(); win.reload(); } return; } - if (input.code == "KeyL" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:l")) { win.webContents.send("l-cmd", mods); e.preventDefault(); return; } - if (input.code == "KeyW" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:w")) { e.preventDefault(); win.webContents.send("w-cmd", mods); return; } - if (input.code == "KeyH" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:h")) { win.webContents.send("h-cmd", mods); e.preventDefault(); return; } - if (input.code == "KeyP" && input.meta) { + if (checkKeyPressed(waveEvent, "Cmd:p")) { win.webContents.send("p-cmd", mods); e.preventDefault(); return; } - if (input.meta && (input.code == "ArrowUp" || input.code == "ArrowDown")) { - if (input.code == "ArrowUp") { + if (checkKeyPressed(waveEvent, "Cmd:ArrowUp") || checkKeyPressed(waveEvent, "Cmd:ArrowDown")) { + if (checkKeyPressed(waveEvent, "Cmd:ArrowUp")) { win.webContents.send("meta-arrowup"); } else { win.webContents.send("meta-arrowdown"); @@ -319,8 +323,8 @@ function createMainWindow(clientData) { e.preventDefault(); return; } - if (input.meta && (input.code == "PageUp" || input.code == "PageDown")) { - if (input.code == "PageUp") { + if (checkKeyPressed(waveEvent, "Cmd:PageUp") || checkKeyPressed(waveEvent, "Cmd:PageDown")) { + if (checkKeyPressed(waveEvent, "Cmd:PageUp")) { win.webContents.send("meta-pageup"); } else { win.webContents.send("meta-pagedown"); @@ -336,8 +340,8 @@ function createMainWindow(clientData) { e.preventDefault(); win.webContents.send("digit-cmd", { digit: digitNum }, mods); } - if ((input.code == "BracketRight" || input.code == "BracketLeft") && input.meta) { - let rel = input.code == "BracketRight" ? 1 : -1; + if (checkKeyPressed(waveEvent, "Cmd:[") || checkKeyPressed(waveEvent, "Cmd:]")) { + let rel = checkKeyPressed(waveEvent, "Cmd:]") ? 1 : -1; win.webContents.send("bracket-cmd", { relative: rel }, mods); e.preventDefault(); return; diff --git a/src/model/model.ts b/src/model/model.ts index 089fd521..641b5dd9 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -81,6 +81,7 @@ import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil"; import { MagicLayout } from "../app/magiclayout"; import { modalsRegistry } from "../app/common/modals/registry"; import * as appconst from "../app/appconst"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } from "../util/keyutil"; dayjs.extend(customParseFormat); dayjs.extend(localizedFormat); @@ -806,41 +807,48 @@ class Screen { } termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void { - if (e.code == "ArrowUp") { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "ArrowUp")) { termWrap.terminal.scrollLines(-1); return; } - if (e.code == "ArrowDown") { + if (checkKeyPressed(waveEvent, "ArrowDown")) { termWrap.terminal.scrollLines(1); return; } - if (e.code == "PageUp") { + if (checkKeyPressed(waveEvent, "PageUp")) { termWrap.terminal.scrollPages(-1); return; } - if (e.code == "PageDown") { + if (checkKeyPressed(waveEvent, "PageDown")) { termWrap.terminal.scrollPages(1); return; } } isTermCapturedKey(e: any): boolean { - let keys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown"]; - if (keys.includes(e.code) && keyHasNoMods(e)) { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if ( + checkKeyPressed(waveEvent, "ArrowUp") || + checkKeyPressed(waveEvent, "ArrowDown") || + checkKeyPressed(waveEvent, "PageUp") || + checkKeyPressed(waveEvent, "PageDown") + ) { return true; } return false; } termCustomKeyHandler(e: any, termWrap: TermWrap): boolean { - if (e.type == "keypress" && e.code == "KeyC" && e.shiftKey && e.ctrlKey) { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:c")) { e.stopPropagation(); e.preventDefault(); let sel = termWrap.terminal.getSelection(); navigator.clipboard.writeText(sel); return false; } - if (e.type == "keypress" && e.code == "KeyV" && e.shiftKey && e.ctrlKey) { + if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:v")) { e.stopPropagation(); e.preventDefault(); let p = navigator.clipboard.readText(); @@ -2526,7 +2534,8 @@ class HistoryViewModel { } handleDocKeyDown(e: any): void { - if (e.code == "Escape") { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); this.closeView(); return; @@ -2766,7 +2775,8 @@ class BookmarksModel { } handleDocKeyDown(e: any): void { - if (e.code == "Escape") { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); if (this.editingBookmark.get() != null) { this.cancelEdit(); @@ -2778,7 +2788,7 @@ class BookmarksModel { if (this.editingBookmark.get() != null) { return; } - if (e.code == "Backspace" || e.code == "Delete") { + if (checkKeyPressed(waveEvent, "Backspace") || checkKeyPressed(waveEvent, "Delete")) { if (this.activeBookmark.get() == null) { return; } @@ -2786,7 +2796,13 @@ class BookmarksModel { this.handleDeleteBookmark(this.activeBookmark.get()); return; } - if (e.code == "ArrowUp" || e.code == "ArrowDown" || e.code == "PageUp" || e.code == "PageDown") { + + if ( + checkKeyPressed(waveEvent, "ArrowUp") || + checkKeyPressed(waveEvent, "ArrowDown") || + checkKeyPressed(waveEvent, "PageUp") || + checkKeyPressed(waveEvent, "PageDown") + ) { e.preventDefault(); if (this.bookmarks.length == 0) { return; @@ -2810,14 +2826,14 @@ class BookmarksModel { })(); return; } - if (e.code == "Enter") { + if (checkKeyPressed(waveEvent, "Enter")) { if (this.activeBookmark.get() == null) { return; } this.useBookmark(this.activeBookmark.get()); return; } - if (e.code == "KeyE") { + if (checkKeyPressed(waveEvent, "e")) { if (this.activeBookmark.get() == null) { return; } @@ -2825,7 +2841,7 @@ class BookmarksModel { this.handleEditBookmark(this.activeBookmark.get()); return; } - if (e.code == "KeyC") { + if (checkKeyPressed(waveEvent, "c")) { if (this.activeBookmark.get() == null) { return; } @@ -3366,6 +3382,7 @@ class Model { this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { name: "model-wavesrv-running", }); + this.platform = this.getPlatform(); this.termFontSize = mobx.computed(() => { let cdata = this.clientData.get(); if (cdata == null || cdata.feopts == null || cdata.feopts.termfontsize == null) { @@ -3409,9 +3426,14 @@ class Model { return this.platform; } this.platform = getApi().getPlatform(); + setKeyUtilPlatform(this.platform); return this.platform; } + testGlobalModel() { + return ""; + } + needsTos(): boolean { let cdata = this.clientData.get(); if (cdata == null) { @@ -3553,16 +3575,17 @@ class Model { } docKeyDownHandler(e: KeyboardEvent) { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); if (isModKeyPress(e)) { return; } if (this.alertMessage.get() != null) { - if (e.code == "Escape") { + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); this.cancelAlert(); return; } - if (e.code == "Enter") { + if (checkKeyPressed(waveEvent, "Enter")) { e.preventDefault(); this.confirmAlert(); return; @@ -3585,7 +3608,7 @@ class Model { this.historyViewModel.handleDocKeyDown(e); return; } - if (e.code == "Escape") { + if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); if (this.activeMainView.get() == "webshare") { this.showSessionView(); @@ -3601,16 +3624,11 @@ class Model { } return; } - if (e.code == "KeyB" && e.getModifierState("Meta")) { + if (checkKeyPressed(waveEvent, "Cmd:b")) { e.preventDefault(); GlobalCommandRunner.bookmarksView(); } - if ( - this.activeMainView.get() == "session" && - e.code == "KeyS" && - e.getModifierState("Meta") && - e.getModifierState("Control") - ) { + if (this.activeMainView.get() == "session" && checkKeyPressed(waveEvent, "Cmd:Ctrl:s")) { e.preventDefault(); let activeScreen = this.getActiveScreen(); if (activeScreen != null) { @@ -3622,7 +3640,7 @@ class Model { } } } - if (e.code == "KeyD" && e.getModifierState("Meta")) { + if (checkKeyPressed(waveEvent, "Cmd:d")) { let ranDelete = this.deleteActiveLine(); if (ranDelete) { e.preventDefault(); diff --git a/src/plugins/code/code.tsx b/src/plugins/code/code.tsx index 172bb45d..00c8ffaa 100644 --- a/src/plugins/code/code.tsx +++ b/src/plugins/code/code.tsx @@ -10,6 +10,7 @@ import { GlobalModel, GlobalCommandRunner } from "../../model/model"; import Split from "react-split-it"; import loader from "@monaco-editor/loader"; loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } }); +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil"; import "./code.less"; @@ -152,17 +153,18 @@ class SourceCodeRenderer extends React.Component< this.setInitialLanguage(editor); this.setEditorHeight(); editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => { - if (e.code === "KeyS" && e.metaKey && this.state.isSave) { + let waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent); + if (checkKeyPressed(waveEvent, "Cmd:s") && this.state.isSave) { e.preventDefault(); e.stopPropagation(); this.doSave(); } - if (e.code === "KeyD" && e.metaKey) { + if (checkKeyPressed(waveEvent, "Cmd:d")) { e.preventDefault(); e.stopPropagation(); this.doClose(); } - if (e.code === "KeyP" && e.metaKey) { + if (checkKeyPressed(waveEvent, "Cmd:p")) { e.preventDefault(); e.stopPropagation(); this.togglePreview(); diff --git a/src/util/keyutil.ts b/src/util/keyutil.ts new file mode 100644 index 00000000..242d755f --- /dev/null +++ b/src/util/keyutil.ts @@ -0,0 +1,153 @@ +import * as React from "react"; +import * as electron from "electron"; + +type KeyPressDecl = { + mods: { + Cmd?: boolean; + Option?: boolean; + Shift?: boolean; + Ctrl?: boolean; + }; + key: string; +}; + +var PLATFORM: string; +const PlatformMacOS: string = "darwin"; + +function setKeyUtilPlatform(platform: string) { + PLATFORM = platform; +} + +function parseKeyDescription(keyDescription: string): KeyPressDecl { + let rtn = { key: "", mods: {} } as KeyPressDecl; + let keys = keyDescription.replace(/[()]/g, "").split(":"); + for (let key of keys) { + if (key == "Cmd") { + rtn.mods.Cmd = true; + } else if (key == "Shift") { + rtn.mods.Shift = true; + } else if (key == "Ctrl") { + rtn.mods.Ctrl = true; + } else if (key == "Option") { + rtn.mods.Option = true; + } else { + rtn.key = key; + if (key.length == 1) { + // check for if key is upper case + if (/[A-Z]/.test(key.charAt(0))) { + // this key is an upper case A - Z - we should apply the shift key, even if it wasn't specified + rtn.mods.Shift = true; + } else if (key == " ") { + rtn.key = "Space"; + // we allow " " and "Space" to be mapped to Space key + } + } + } + } + return rtn; +} + +function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean { + let keyPress = parseKeyDescription(description); + if (keyPress.mods.Option && !event.alt) { + return false; + } + if (keyPress.mods.Cmd && !event.cmd) { + return false; + } + if (keyPress.mods.Shift && !event.shift) { + return false; + } + if (keyPress.mods.Ctrl && !event.control) { + return false; + } + let eventKey = event.key; + let descKey = keyPress.key; + if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) { + // key is upper case A-Z, this means shift is applied, we want to allow + // "Shift:e" as well as "Shift:E" or "E" + eventKey = eventKey.toLocaleLowerCase(); + descKey = descKey.toLocaleLowerCase(); + } else if (eventKey == " ") { + eventKey = "Space"; + // a space key is shown as " ", we want users to be able to set space key as "Space" or " ", whichever they prefer + } + if (descKey != eventKey) { + return false; + } + return true; +} + +type ModKeyStrs = "Cmd" | "Option" | "Shift" | "Ctrl"; + +interface WaveKeyboardEvent { + type: string; + /** + * Equivalent to KeyboardEvent.key. + */ + key: string; + /** + * Equivalent to KeyboardEvent.code. + */ + code: string; + /** + * Equivalent to KeyboardEvent.shiftKey. + */ + shift: boolean; + /** + * Equivalent to KeyboardEvent.controlKey. + */ + control: boolean; + /** + * Equivalent to KeyboardEvent.altKey. + */ + alt: boolean; + /** + * Equivalent to KeyboardEvent.metaKey. + */ + cmd: boolean; + repeat: boolean; + /** + * Equivalent to KeyboardEvent.location. + */ + location: number; +} + +function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEvent): WaveKeyboardEvent { + let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent; + rtn.control = event.ctrlKey; + rtn.shift = event.shiftKey; + if (PLATFORM == PlatformMacOS) { + rtn.cmd = event.metaKey; + rtn.alt = event.altKey; + } else { + rtn.cmd = event.altKey; + } + rtn.code = event.code; + rtn.key = event.key; + rtn.location = event.location; + rtn.type = event.type; + rtn.repeat = event.repeat; + return rtn; +} + +function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent { + let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent; + rtn.type = event.type; + rtn.control = event.control; + if (PLATFORM == PlatformMacOS) { + rtn.cmd = event.meta; + rtn.alt = event.alt; + } else { + rtn.cmd = event.alt; + } + rtn.shift = event.shift; + rtn.repeat = event.isAutoRepeat; + rtn.location = event.location; + rtn.code = event.code; + rtn.key = event.key; + return rtn; +} + +export { adaptFromElectronKeyEvent, adaptFromReactOrNativeKeyEvent, checkKeyPressed, setKeyUtilPlatform }; +export type { WaveKeyboardEvent };