diff --git a/core/input/domkeytable.js b/core/input/domkeytable.js index b84ad45d..1b6baa67 100644 --- a/core/input/domkeytable.js +++ b/core/input/domkeytable.js @@ -119,7 +119,7 @@ addStandard("WakeUp", KeyTable.XF86XK_WakeUp); // 2.8. IME and Composition Keys addStandard("AllCandidates", KeyTable.XK_MultipleCandidate); -addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle +addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle); addStandard("CodeInput", KeyTable.XK_Codeinput); addStandard("Compose", KeyTable.XK_Multi_key); addStandard("Convert", KeyTable.XK_Henkan); @@ -147,7 +147,7 @@ addStandard("KanjiMode", KeyTable.XK_Kanji); addStandard("Katakana", KeyTable.XK_Katakana); addStandard("Romaji", KeyTable.XK_Romaji); addStandard("Zenkaku", KeyTable.XK_Zenkaku); -addStandard("ZenkakuHanaku", KeyTable.XK_Zenkaku_Hankaku); +addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku); // 2.9. General-Purpose Function Keys diff --git a/core/input/keyboard.js b/core/input/keyboard.js index ed3b1bf1..48f65cf6 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -164,6 +164,20 @@ export default class Keyboard { return; } + // Windows doesn't send proper key releases for a bunch of + // Japanese IM keys so we have to fake the release right away + const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku, + KeyTable.XK_Eisu_toggle, + KeyTable.XK_Katakana, + KeyTable.XK_Hiragana, + KeyTable.XK_Romaji ]; + if (browser.isWindows() && jpBadKeys.includes(keysym)) { + this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + stopEvent(e); + return; + } + stopEvent(e); // Possible start of AltGr sequence? (see above) diff --git a/core/input/util.js b/core/input/util.js index 182be7f7..58f84e55 100644 --- a/core/input/util.js +++ b/core/input/util.js @@ -157,6 +157,21 @@ export function getKeysym(evt) { } } + // Windows sends alternating symbols for some keys when using a + // Japanese layout. We have no way of synchronising with the IM + // running on the remote system, so we send some combined keysym + // instead and hope for the best. + if (browser.isWindows()) { + switch (key) { + case 'Zenkaku': + case 'Hankaku': + return KeyTable.XK_Zenkaku_Hankaku; + case 'Romaji': + case 'KanaMode': + return KeyTable.XK_Romaji; + } + } + return DOMKeyTable[key][location]; } diff --git a/tests/test.helper.js b/tests/test.helper.js index 5552ec48..ed65770e 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -186,5 +186,38 @@ describe('Helpers', function () { expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC); }); }); + + describe('Japanese IM keys on Windows', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "Windows"; + }); + + afterEach(function () { + if (origNavigator !== undefined) { + Object.defineProperty(window, "navigator", origNavigator); + } + }); + + const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a, + 'Romaji': 0xff24, 'KanaMode': 0xff24 }; + for (let [key, keysym] of Object.entries(keys)) { + it(`should fake combined key for ${key} on Windows`, function () { + expect(KeyboardUtil.getKeysym({code: 'FakeIM', key: key})).to.be.equal(keysym); + }); + } + }); }); }); diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 940769e6..381cd308 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -219,7 +219,7 @@ describe('Key Event Handling', function () { } }); - it('should toggle caps lock on key press on iOS', function (done) { + it('should toggle caps lock on key press on iOS', function () { window.navigator.platform = "iPad"; const kbd = new Keyboard(document); kbd.onkeyevent = sinon.spy(); @@ -228,10 +228,9 @@ describe('Key Event Handling', function () { expect(kbd.onkeyevent).to.have.been.calledTwice; expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); - done(); }); - it('should toggle caps lock on key press on mac', function (done) { + it('should toggle caps lock on key press on mac', function () { window.navigator.platform = "Mac"; const kbd = new Keyboard(document); kbd.onkeyevent = sinon.spy(); @@ -240,10 +239,9 @@ describe('Key Event Handling', function () { expect(kbd.onkeyevent).to.have.been.calledTwice; expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); - done(); }); - it('should toggle caps lock on key release on iOS', function (done) { + it('should toggle caps lock on key release on iOS', function () { window.navigator.platform = "iPad"; const kbd = new Keyboard(document); kbd.onkeyevent = sinon.spy(); @@ -252,10 +250,9 @@ describe('Key Event Handling', function () { expect(kbd.onkeyevent).to.have.been.calledTwice; expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); - done(); }); - it('should toggle caps lock on key release on mac', function (done) { + it('should toggle caps lock on key release on mac', function () { window.navigator.platform = "Mac"; const kbd = new Keyboard(document); kbd.onkeyevent = sinon.spy(); @@ -264,10 +261,50 @@ describe('Key Event Handling', function () { expect(kbd.onkeyevent).to.have.been.calledTwice; expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); - done(); }); }); + describe('Japanese IM keys on Windows', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "Windows"; + }); + + afterEach(function () { + if (origNavigator !== undefined) { + Object.defineProperty(window, "navigator", origNavigator); + } + }); + + const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a, + 'Alphanumeric': 0xff30, 'Katakana': 0xff26, + 'Hiragana': 0xff25, 'Romaji': 0xff24, + 'KanaMode': 0xff24 }; + for (let [key, keysym] of Object.entries(keys)) { + it(`should fake key release for ${key} on Windows`, function () { + let kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'FakeIM', key: key})); + + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(keysym, "FakeIM", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(keysym, "FakeIM", false); + }); + } + }); + describe('Escape AltGraph on Windows', function () { let origNavigator; beforeEach(function () {