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 };