author | Jan Jongboom <janjongboom@gmail.com> |
Wed, 13 Aug 2014 02:12:00 -0400 | |
changeset 199861 | b615a6a84a9c3d2a84d6c9b7d1b10e2cdb185634 |
parent 199860 | c19e057a25323a1d28838b2be8890cde96071424 |
child 199862 | 0a071f841a357860655ca80d1496cecccdc44e36 |
push id | 47750 |
push user | ryanvm@gmail.com |
push date | Fri, 15 Aug 2014 21:04:12 +0000 |
treeherder | mozilla-inbound@baea646f5a80 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | yxl |
bugs | 1026997 |
milestone | 34.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/dom/inputmethod/MozKeyboard.js +++ b/dom/inputmethod/MozKeyboard.js @@ -447,16 +447,21 @@ MozInputContext.prototype = { let json = msg.json; let resolver = this.takePromiseResolver(json.requestId); if (!resolver) { return; } + // Update context first before resolving promise to avoid race condition + if (json.selectioninfo) { + this.updateSelectionContext(json.selectioninfo); + } + switch (msg.name) { case "Keyboard:SendKey:Result:OK": resolver.resolve(); break; case "Keyboard:SendKey:Result:Error": resolver.reject(json.error); break; case "Keyboard:GetText:Result:OK":
--- a/dom/inputmethod/forms.js +++ b/dom/inputmethod/forms.js @@ -219,16 +219,17 @@ let FormAssistant = { textAfterCursor: "", scrollIntoViewTimeout: null, _focusedElement: null, _focusCounter: 0, // up one for every time we focus a new element _observer: null, _documentEncoder: null, _editor: null, _editing: false, + _selectionPrivate: null, get focusedElement() { if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement)) this._focusedElement = null; return this._focusedElement; }, @@ -239,52 +240,58 @@ let FormAssistant = { setFocusedElement: function fa_setFocusedElement(element) { let self = this; if (element === this.focusedElement) return; if (this.focusedElement) { - this.focusedElement.removeEventListener('mousedown', this); - this.focusedElement.removeEventListener('mouseup', this); this.focusedElement.removeEventListener('compositionend', this); if (this._observer) { this._observer.disconnect(); this._observer = null; } if (!element) { this.focusedElement.blur(); } + if (this._selectionPrivate) { + this._selectionPrivate.removeSelectionListener(this); + this._selectionPrivate = null; + } } this._documentEncoder = null; if (this._editor) { // When the nsIFrame of the input element is reconstructed by // CSS restyling, the editor observers are removed. Catch // [nsIEditor.removeEditorObserver] failure exception if that // happens. try { this._editor.removeEditorObserver(this); } catch (e) {} this._editor = null; } if (element) { - element.addEventListener('mousedown', this); - element.addEventListener('mouseup', this); element.addEventListener('compositionend', this); if (isContentEditable(element)) { this._documentEncoder = getDocumentEncoder(element); } this._editor = getPlaintextEditor(element); if (this._editor) { // Add a nsIEditorObserver to monitor the text content of the focused // element. this._editor.addEditorObserver(this); + + let selection = this._editor.selection; + if (selection) { + this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate); + this._selectionPrivate.addSelectionListener(this); + } } // If our focusedElement is removed from DOM we want to handle it properly let MutationObserver = element.ownerDocument.defaultView.MutationObserver; this._observer = new MutationObserver(function(mutations) { var del = [].some.call(mutations, function(m) { return [].some.call(m.removedNodes, function(n) { return n.contains(element); @@ -300,16 +307,20 @@ let FormAssistant = { childList: true, subtree: true }); } this.focusedElement = element; }, + notifySelectionChanged: function(aDocument, aSelection, aReason) { + this.updateSelection(); + }, + get documentEncoder() { return this._documentEncoder; }, // Get the nsIPlaintextEditor object of current input field. get editor() { return this._editor; }, @@ -371,42 +382,16 @@ let FormAssistant = { case "submit": if (this.focusedElement) { this.hideKeyboard(); this.selectionStart = -1; this.selectionEnd = -1; } break; - case 'mousedown': - if (!this.focusedElement) { - break; - } - - // We only listen for this event on the currently focused element. - // When the mouse goes down, note the cursor/selection position - this.updateSelection(); - break; - - case 'mouseup': - if (!this.focusedElement) { - break; - } - - // We only listen for this event on the currently focused element. - // When the mouse goes up, see if the cursor has moved (or the - // selection changed) since the mouse went down. If it has, we - // need to tell the keyboard about it - range = getSelectionRange(this.focusedElement); - if (range[0] !== this.selectionStart || - range[1] !== this.selectionEnd) { - this.updateSelection(); - } - break; - case "resize": if (!this.isKeyboardOpened) return; if (this.scrollIntoViewTimeout) { content.clearTimeout(this.scrollIntoViewTimeout); this.scrollIntoViewTimeout = null; } @@ -418,44 +403,30 @@ let FormAssistant = { this.scrollIntoViewTimeout = null; if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) { scrollSelectionOrElementIntoView(this.focusedElement); } }.bind(this), RESIZE_SCROLL_DELAY); } break; - case "input": - if (this.focusedElement) { - // When the text content changes, notify the keyboard - this.updateSelection(); - } - break; - case "keydown": if (!this.focusedElement) { break; } CompositionManager.endComposition(''); - - // We use 'setTimeout' to wait until the input element accomplishes the - // change in selection range. - content.setTimeout(function() { - this.updateSelection(); - }.bind(this), 0); break; case "keyup": if (!this.focusedElement) { break; } CompositionManager.endComposition(''); - break; case "compositionend": if (!this.focusedElement) { break; } CompositionManager.onCompositionEnd(); @@ -521,17 +492,18 @@ let FormAssistant = { domWindowUtils.sendKeyEvent('keyup', json.keyCode, json.charCode, json.modifiers, flags); } this._editing = false; if (json.requestId && doKeypress) { sendAsyncMessage("Forms:SendKey:Result:OK", { - requestId: json.requestId + requestId: json.requestId, + selectioninfo: this.getSelectionInfo() }); } else if (json.requestId && !doKeypress) { sendAsyncMessage("Forms:SendKey:Result:Error", { requestId: json.requestId, error: "Keydown event got canceled" }); } @@ -579,18 +551,16 @@ let FormAssistant = { sendAsyncMessage("Forms:SetSelectionRange:Result:Error", { requestId: json.requestId, error: "failed" }); } break; } - this.updateSelection(); - if (json.requestId) { sendAsyncMessage("Forms:SetSelectionRange:Result:OK", { requestId: json.requestId, selectioninfo: this.getSelectionInfo() }); } break; } @@ -647,24 +617,26 @@ let FormAssistant = { break; } case "Forms:SetComposition": { CompositionManager.setComposition(target, json.text, json.cursor, json.clauses); sendAsyncMessage("Forms:SetComposition:Result:OK", { requestId: json.requestId, + selectioninfo: this.getSelectionInfo() }); break; } case "Forms:EndComposition": { CompositionManager.endComposition(json.text); sendAsyncMessage("Forms:EndComposition:Result:OK", { requestId: json.requestId, + selectioninfo: this.getSelectionInfo() }); break; } } this._editing = false; }, @@ -753,25 +725,39 @@ let FormAssistant = { selectionStart: range[0], selectionEnd: range[1], textBeforeCursor: textAround.before, textAfterCursor: textAround.after, changed: changed }; }, + _selectionTimeout: null, + // Notify when the selection range changes updateSelection: function fa_updateSelection() { - if (!this.focusedElement) { - return; + // A call to setSelectionRange on input field causes 2 selection changes + // one to [0,0] and one to actual value. Both are sent in same tick. + // Prevent firing two events in that scenario, always only use the last 1. + // + // It is also a workaround for Bug 1053048, which prevents + // getSelectionInfo() accessing selectionStart or selectionEnd in the + // callback function of nsISelectionListener::NotifySelectionChanged(). + if (this._selectionTimeout) { + content.clearTimeout(this._selectionTimeout); } - let selectionInfo = this.getSelectionInfo(); - if (selectionInfo.changed) { - sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo()); - } + this._selectionTimeout = content.setTimeout(function() { + if (!this.focusedElement) { + return; + } + let selectionInfo = this.getSelectionInfo(); + if (selectionInfo.changed) { + sendAsyncMessage("Forms:SelectionChange", selectionInfo); + } + }.bind(this), 0); } }; FormAssistant.init(); function isContentEditable(element) { if (!element) { return false;
--- a/dom/inputmethod/mochitest/mochitest.ini +++ b/dom/inputmethod/mochitest/mochitest.ini @@ -10,11 +10,12 @@ support-files = file_test_sms_app.html [test_basic.html] [test_bug944397.html] [test_bug949059.html] [test_bug953044.html] [test_bug960946.html] [test_bug978918.html] +[test_bug1026997.html] [test_bug1043828.html] [test_delete_focused_element.html] [test_sendkey_cancel.html]
new file mode 100644 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1026997.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1026997 +--> +<head> + <title>SelectionChange on InputMethod API.</title> + <script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026997">Mozilla Bug 1026997</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +// The frame script running in file_test_app.html. +function appFrameScript() { + let input = content.document.getElementById('test-input'); + + input.focus(); + + function next(start, end) { + input.setSelectionRange(start, end); + } + + addMessageListener("test:KeyBoard:nextSelection", function(event) { + let json = event.json; + next(json[0], json[1]); + }); +} + +function runTest() { + let actions = [ + [0, 4], + [1, 1], + [3, 3], + [2, 3] + ]; + + let counter = 0; + let mm = null; + let ic = null; + + let im = navigator.mozInputMethod; + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + ic = im.inputcontext; + if (!ic) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + ic.onselectionchange = function() { + is(ic.selectionStart, actions[counter][0], "start"); + is(ic.selectionEnd, actions[counter][1], "end"); + + if (++counter === actions.length) { + inputmethod_cleanup(); + return; + } + + next(); + }; + + next(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + // Create an app frame to recieve keyboard inputs. + let app = document.createElement('iframe'); + app.src = 'file_test_app.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + app.addEventListener('mozbrowserloadend', function() { + mm = SpecialPowers.getBrowserFrameMessageManager(app); + mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false); + next(); + }); + + function next() { + if (ic && mm) { + mm.sendAsyncMessage('test:KeyBoard:nextSelection', actions[counter]); + } + } +} +</script> +</pre> +</body> +</html> +