Add support for composed chars and fix issues with "foreign" keyboard layouts (#1006)

* ime

* Fix code style

* Add visual feedback for composition events

* Temporarily disable `hterm`'s `onKeyDown` hacks

* Replicate the focus/blur state of our caret on the `hterm` caret

* Fix: focus our caret when there's a tab change

* `caret_` => `hyperCaret`

* Reorg: move the caret hacks to the `hterm.js` extensions

* Remove `console.log`

* Remove the `Dead key` hack and reenable keyboard commands

* Add a (temporary?) fix to re-enable text selection

* Check for a selection `onMouseUp` instead `onFocus`

* Fix wrong buggy hterm's cursor styling on term focus/blur

* Fix the cursor style after the bell rings – closes #674

* Enable `acceptFirstMouse` to focus the correct term – closes #861

* Fix code style

* Fix: clear the `hyper-caret` when a char is inserted via the IME dialog

* Remove useless function

* Add coments regarding text selection

* Fix code style
This commit is contained in:
Matheus Fernandes 2016-11-16 16:44:04 -02:00 committed by Guillermo Rauch
parent 0585607da4
commit 27a20e0cfc
3 changed files with 142 additions and 72 deletions

View file

@ -121,7 +121,8 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
// HYPERTERM_DEBUG for backwards compatibility with hyperterm
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
x: startX,
y: startY
y: startY,
acceptFirstMouse: true
};
const browserOptions = plugins.getDecoratedBrowserOptions(browserDefaults);

View file

@ -13,15 +13,18 @@ export default class Term extends Component {
super(props);
this.handleWheel = this.handleWheel.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleScrollEnter = this.handleScrollEnter.bind(this);
this.handleScrollLeave = this.handleScrollLeave.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.onHyperCaret = this.onHyperCaret.bind(this);
props.ref_(this);
}
componentDidMount() {
const {props} = this;
this.term = props.term || new hterm.Terminal(uuid.v4());
this.term.onHyperCaret(this.hyperCaret);
// the first term that's created has unknown size
// subsequent new tabs have size
@ -77,6 +80,7 @@ export default class Term extends Component {
// emit onTitle event when hterm instance
// wants to set the title of its tab
this.term.setWindowTitle = props.onTitle;
this.term.focusHyperCaret();
};
this.term.decorate(this.termRef);
this.term.installKeyboard();
@ -87,7 +91,7 @@ export default class Term extends Component {
const iframeWindow = this.getTermDocument().defaultView;
iframeWindow.addEventListener('wheel', this.handleWheel);
this.getScreenNode().addEventListener('focus', this.handleFocus);
this.getScreenNode().addEventListener('mouseup', this.handleMouseUp);
}
handleWheel(e) {
@ -122,6 +126,22 @@ export default class Term extends Component {
// called, which is unecessary.
// Should investigate if it matters.
this.props.onActive();
this.term.focusHyperCaret();
}
handleMouseUp() {
this.props.onActive();
// this makes sure that we focus the hyper caret only
// if a click on the term does not result in a selection
// otherwise, if we focus without such check, it'd be
// impossible to select a piece of text
if (this.term.document_.getSelection().type !== 'Range') {
this.term.focusHyperCaret();
}
}
onHyperCaret(caret) {
this.hyperCaret = caret;
}
write(data) {
@ -129,7 +149,7 @@ export default class Term extends Component {
}
focus() {
this.term.focus();
this.term.focusHyperCaret();
}
clear() {
@ -185,6 +205,14 @@ export default class Term extends Component {
getStylesheet(css) {
const blob = new Blob([`
.hyper-caret {
outline: none;
display: inline-block;
color: transparent;
text-shadow: 0 0 0 black;
font-family: ${this.props.fontFamily};
font-size: ${this.props.fontSize}px;
}
.cursor-node[focus="false"] {
border-width: 1px !important;
}
@ -237,6 +265,7 @@ export default class Term extends Component {
if (this.props.fontSize !== nextProps.fontSize) {
prefs.set('font-size', nextProps.fontSize);
this.hyperCaret.style.fontSize = nextProps.fontSize + 'px';
}
if (this.props.foregroundColor !== nextProps.foregroundColor) {
@ -245,6 +274,7 @@ export default class Term extends Component {
if (this.props.fontFamily !== nextProps.fontFamily) {
prefs.set('font-family', nextProps.fontFamily);
this.hyperCaret.style.fontFamily = nextProps.fontFamily;
}
if (this.props.fontSmoothing !== nextProps.fontSmoothing) {
@ -315,11 +345,15 @@ export default class Term extends Component {
height: '100%'
}}
/> :
<div
className={css('scrollbarShim')}
onMouseEnter={this.handleScrollEnter}
onMouseLeave={this.handleScrollLeave}
/>
[
<div key="hyper-caret" contentEditable className="hyper-caret" ref={this.onHyperCaret}/>,
<div // eslint-disable-line react/jsx-indent
key="scrollbar"
className={css('scrollbarShim')}
onMouseEnter={this.handleScrollEnter}
onMouseLeave={this.handleScrollLeave}
/>
]
}
{ this.props.customChildren }
</div>);

View file

@ -42,69 +42,6 @@ const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
const modifierKeysConf = this.terminal.modifierKeys;
/**
* Add fixes for U.S. International PC Keyboard layout
* These keys are sent through as 'Dead' keys, as they're used as modifiers.
* Ignore that and insert the correct character.
*/
if (e.key === 'Dead') {
if (e.code === 'Quote' && e.shiftKey === false) {
this.terminal.onVTKeystroke('\'');
return;
}
if (e.code === 'Quote' && e.shiftKey === true) {
this.terminal.onVTKeystroke('"');
return;
}
if ((e.code === 'IntlBackslash' || e.code === 'Backquote') && e.shiftKey === true) {
this.terminal.onVTKeystroke('~');
return;
}
// This key is also a tilde on all tested keyboards
if (e.code === 'KeyN' && e.altKey === true && modifierKeysConf.altIsMeta === false) {
this.terminal.onVTKeystroke('~');
return;
}
if ((e.code === 'IntlBackslash' || e.code === 'Backquote') && e.shiftKey === false) {
this.terminal.onVTKeystroke('`');
return;
}
if (e.code === 'Digit6') {
this.terminal.onVTKeystroke('^');
return;
}
// German keyboard layout
if (e.code === 'Equal' && e.shiftKey === false) {
this.terminal.onVTKeystroke('´');
return;
}
if (e.code === 'Equal' && e.shiftKey === true) {
this.terminal.onVTKeystroke('`');
return;
}
// Italian keyboard layout
if (e.code === 'Digit9' && e.altKey === true && modifierKeysConf.altIsMeta === false) {
this.terminal.onVTKeystroke('`');
return;
}
if (e.code === 'Digit8' && e.altKey === true && modifierKeysConf.altIsMeta === false) {
this.terminal.onVTKeystroke('´');
// To fix issue with changing the terminal prompt
e.preventDefault();
return;
}
// French keyboard layout
if (e.code === 'BracketLeft') {
this.terminal.onVTKeystroke('^');
return;
}
if (e.code === 'Backslash') {
this.terminal.onVTKeystroke('`');
return;
}
console.warn('Uncaught dead key on international keyboard', e);
}
if (e.altKey &&
e.which !== 16 && // Ignore other modifer keys
e.which !== 17 &&
@ -132,7 +69,9 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) {
return;
}
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') {
if ((!e.ctrlKey || e.code !== 'ControlLeft') &&
!e.shiftKey && e.code !== 'CapsLock' &&
e.key !== 'Dead') {
// Test for valid keys in order to clear the terminal selection
selection.clear(this.terminal);
}
@ -212,6 +151,102 @@ hterm.ScrollPort.prototype.setBackgroundColor = function () {
this.screen_.style.backgroundColor = 'transparent';
};
// will be called by the <Term/> right after the `hterm.Terminal` is instantiated
hterm.Terminal.prototype.onHyperCaret = function (caret) {
this.hyperCaret = caret;
let ongoingComposition = false;
caret.addEventListener('compositionstart', () => {
ongoingComposition = true;
});
// we can ignore `compositionstart` since chromium always fire it with ''
caret.addEventListener('compositionupdate', () => {
this.cursorNode_.style.backgroundColor = 'yellow';
this.cursorNode_.style.borderColor = 'yellow';
});
// at this point the char(s) is ready
caret.addEventListener('compositionend', () => {
ongoingComposition = false;
this.cursorNode_.style.backgroundColor = '';
this.setCursorShape(this.getCursorShape());
this.cursorNode_.style.borderColor = this.getCursorColor();
caret.innerText = '';
});
// if you open the `Emoji & Symbols` (ctrl+cmd+space)
// and select an emoji, it'll be inserted into our caret
// and stay there until you star a compositon event.
// to avoid that, we'll just check if there's an ongoing
// compostion event. if there's one, we do nothing.
// otherwise, we just remove the emoji and stop the event
// propagation.
// PS: this event will *not* be fired when a standard char
// (a, b, c, 1, 2, 3, etc) is typed only for composed
// ones and `Emoji & Symbols`
caret.addEventListener('input', e => {
if (!ongoingComposition) {
caret.innerText = '';
e.stopPropagation();
e.preventDefault();
}
});
// we need to capture pastes, prevent them and send its contents to the terminal
caret.addEventListener('paste', e => {
e.stopPropagation();
e.preventDefault();
const text = e.clipboardData.getData('text');
this.onVTKeystroke(text);
});
// here we replicate the focus/blur state of our caret on the `hterm` caret
caret.addEventListener('focus', () => {
this.cursorNode_.setAttribute('focus', true);
this.restyleCursor_();
});
caret.addEventListener('blur', () => {
this.cursorNode_.setAttribute('focus', false);
this.restyleCursor_();
});
// this is necessary because we need to access the `document_` and the hyperCaret
// on `hterm.Screen.prototype.syncSelectionCaret`
this.primaryScreen_.terminal = this;
this.alternateScreen_.terminal = this;
};
// ensure that our contenteditable caret is injected
// inside the term's cursor node and that it's focused
hterm.Terminal.prototype.focusHyperCaret = function () {
if (!this.hyperCaret.parentNode !== this.cursorNode_) {
this.cursorNode_.appendChild(this.hyperCaret);
}
this.hyperCaret.focus();
};
hterm.Screen.prototype.syncSelectionCaret = function () {
const p = this.terminal.hyperCaret;
const doc = this.terminal.document_;
const win = doc.defaultView;
const s = win.getSelection();
const r = doc.createRange();
r.selectNodeContents(p);
s.removeAllRanges();
s.addRange(r);
};
// fixes a bug in hterm, where the cursor goes back to `BLOCK`
// after the bell rings
const oldRingBell = hterm.Terminal.prototype.ringBell;
hterm.Terminal.prototype.ringBell = function () {
oldRingBell.call(this);
setTimeout(() => {
this.restyleCursor_();
}, 200);
};
// fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb
lib.colors.hexToRGB = function (arg) {