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 // HYPERTERM_DEBUG for backwards compatibility with hyperterm
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev, show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
x: startX, x: startX,
y: startY y: startY,
acceptFirstMouse: true
}; };
const browserOptions = plugins.getDecoratedBrowserOptions(browserDefaults); const browserOptions = plugins.getDecoratedBrowserOptions(browserDefaults);

View file

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

View file

@ -42,69 +42,6 @@ const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
hterm.Keyboard.prototype.onKeyDown_ = function (e) { hterm.Keyboard.prototype.onKeyDown_ = function (e) {
const modifierKeysConf = this.terminal.modifierKeys; 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 && if (e.altKey &&
e.which !== 16 && // Ignore other modifer keys e.which !== 16 && // Ignore other modifer keys
e.which !== 17 && e.which !== 17 &&
@ -132,7 +69,9 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) { if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) {
return; 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 // Test for valid keys in order to clear the terminal selection
selection.clear(this.terminal); selection.clear(this.terminal);
} }
@ -212,6 +151,102 @@ hterm.ScrollPort.prototype.setBackgroundColor = function () {
this.screen_.style.backgroundColor = 'transparent'; 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 // fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb // is not properly converted to rgb
lib.colors.hexToRGB = function (arg) { lib.colors.hexToRGB = function (arg) {