Bug 788000 - Add support for chrome selection. r=mbrubeck

This commit is contained in:
Jim Mathies 2013-06-17 07:46:52 -05:00
parent 86988330d1
commit 476455dbd7
6 changed files with 431 additions and 14 deletions

View File

@ -110,6 +110,7 @@ let ScriptContexts = {};
["SelectHelperUI", "chrome://browser/content/helperui/SelectHelperUI.js"],
["SelectionHelperUI", "chrome://browser/content/helperui/SelectionHelperUI.js"],
["SelectionPrototype", "chrome://browser/content/library/SelectionPrototype.js"],
["ChromeSelectionHandler", "chrome://browser/content/helperui/ChromeSelectionHandler.js"],
["AnimatedZoom", "chrome://browser/content/AnimatedZoom.js"],
["CommandUpdater", "chrome://browser/content/commandUtil.js"],
["ContextCommands", "chrome://browser/content/ContextCommands.js"],

View File

@ -783,21 +783,35 @@ var BrowserUI = {
onEvent: function(aEventName) {}
},
_urlbarClicked: function _urlbarClicked() {
_urlbarClicked: function _urlbarClicked(aEvent) {
let touchEvent = aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
// If the urlbar is not already focused, focus it and select the contents.
if (Elements.urlbarState.getAttribute("mode") != "edit")
this._editURI(true);
if (Elements.urlbarState.getAttribute("mode") != "edit") {
this._editURI(true, touchEvent);
if (touchEvent) {
SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
aEvent.clientX, aEvent.clientY);
}
return;
}
// tap caret handling
if (touchEvent) {
SelectionHelperUI.attachToCaret(ChromeSelectionHandler,
aEvent.clientX, aEvent.clientY);
}
},
_editURI: function _editURI(aShouldDismiss) {
this._clearURIFormatting();
_editURI: function _editURI(aEvent, aShouldDismiss) {
this._edit.focus();
this._edit.select();
Elements.urlbarState.setAttribute("mode", "edit");
StartUI.show();
if (aShouldDismiss)
if (aShouldDismiss) {
ContextUI.dismissTabs();
}
},
formatURI: function formatURI() {

View File

@ -0,0 +1,364 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Selection handler for chrome text inputs
*/
const kCaretMode = 1;
const kSelectionMode = 2;
var ChromeSelectionHandler = {
_mode: kSelectionMode,
/*************************************************
* Messaging wrapper
*/
sendAsync: function sendAsync(aMsg, aJson) {
SelectionHelperUI.receiveMessage({
name: aMsg,
json: aJson
});
},
/*************************************************
* Browser event handlers
*/
/*
* General selection start method for both caret and selection mode.
*/
_onSelectionAttach: function _onSelectionAttach(aJson) {
this._domWinUtils = Util.getWindowUtils(window);
this._contentWindow = window;
this._targetElement = this._domWinUtils.elementFromPoint(aJson.xPos, aJson.yPos, true, false);
this._targetIsEditable = this._targetElement instanceof Components.interfaces.nsIDOMXULTextBoxElement;
if (!this._targetIsEditable) {
this._onFail("not an editable?");
return;
}
let selection = this._getSelection();
if (!selection) {
this._onFail("no selection.");
return;
}
if (!selection.isCollapsed) {
this._mode = kSelectionMode;
this._updateSelectionUI("start", true, true);
} else {
this._mode = kCaretMode;
this._updateSelectionUI("caret", false, false, true);
}
},
/*
* Selection monocle start move event handler
*/
_onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
if (!this.targetIsEditable) {
this._onFail("_onSelectionMoveStart with bad targetElement.");
return;
}
if (this._selectionMoveActive) {
this._onFail("mouse is already down on drag start?");
return;
}
// We bail if things get out of sync here implying we missed a message.
this._selectionMoveActive = true;
if (this._targetIsEditable) {
// If we're coming out of an out-of-bounds scroll, the node the user is
// trying to drag may be hidden (the monocle will be pegged to the edge
// of the edit). Make sure the node the user wants to move is visible
// and has focus.
this._updateInputFocus(aMsg.change);
}
// Update the position of our selection monocles
this._updateSelectionUI("update", true, true);
},
/*
* Selection monocle move event handler
*/
_onSelectionMove: function _onSelectionMove(aMsg) {
if (!this.targetIsEditable) {
this._onFail("_onSelectionMove with bad targetElement.");
return;
}
if (!this._selectionMoveActive) {
this._onFail("mouse isn't down for drag move?");
return;
}
// Update selection in the doc
let pos = null;
if (aMsg.change == "start") {
pos = aMsg.start;
} else {
pos = aMsg.end;
}
this._handleSelectionPoint(aMsg.change, pos, false);
},
/*
* Selection monocle move finished event handler
*/
_onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
if (!this.targetIsEditable) {
this._onFail("_onSelectionMoveEnd with bad targetElement.");
return;
}
if (!this._selectionMoveActive) {
this._onFail("mouse isn't down for drag move?");
return;
}
// Update selection in the doc
let pos = null;
if (aMsg.change == "start") {
pos = aMsg.start;
} else {
pos = aMsg.end;
}
this._handleSelectionPoint(aMsg.change, pos, true);
this._selectionMoveActive = false;
// Update the position of our selection monocles
this._updateSelectionUI("end", true, true);
},
/*
* Switch selection modes. Currently we only support switching
* from "caret" to "selection".
*/
_onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) {
if (aMode != "selection") {
this._onFail("unsupported mode switch");
return;
}
// Sanity check to be sure we are initialized
if (!this._targetElement) {
this._onFail("not initialized");
return;
}
// Similar to _onSelectionStart - we need to create initial selection
// but without the initialization bits.
let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY });
if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) {
this._onFail("failed to set selection at point");
return;
}
// We bail if things get out of sync here implying we missed a message.
this._selectionMoveActive = true;
this._mode = kSelectionMode;
// Update the position of the selection marker that is *not*
// being dragged.
this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
},
/*
* Selection close event handler
*
* @param aClearSelection requests that selection be cleared.
*/
_onSelectionClose: function _onSelectionClose(aClearSelection) {
if (aClearSelection) {
this._clearSelection();
}
this._closeSelection();
},
/*
* Called if for any reason we fail during the selection
* process. Cancels the selection.
*/
_onFail: function _onFail(aDbgMessage) {
if (aDbgMessage && aDbgMessage.length > 0)
Util.dumpLn(aDbgMessage);
this.sendAsync("Content:SelectionFail");
this._clearSelection();
this._closeSelection();
},
/*
* Empty conversion routines to match those in
* browser. Called by SelectionHelperUI when
* sending and receiving coordinates in messages.
*/
ptClientToBrowser: function ptClientToBrowser(aX, aY, aIgnoreScroll, aIgnoreScale) {
return { x: aX, y: aY }
},
rectBrowserToClient: function rectBrowserToClient(aRect, aIgnoreScroll, aIgnoreScale) {
return {
left: aRect.left,
right: aRect.right,
top: aRect.top,
bottom: aRect.bottom
}
},
ptBrowserToClient: function ptBrowserToClient(aX, aY, aIgnoreScroll, aIgnoreScale) {
return { x: aX, y: aY }
},
ctobx: function ctobx(aCoord) {
return aCoord;
},
ctoby: function ctoby(aCoord) {
return aCoord;
},
btocx: function btocx(aCoord) {
return aCoord;
},
btocy: function btocy(aCoord) {
return aCoord;
},
/*************************************************
* Selection helpers
*/
/*
* _clearSelection
*
* Clear existing selection if it exists and reset our internla state.
*/
_clearSelection: function _clearSelection() {
let selection = this._getSelection();
if (selection) {
selection.removeAllRanges();
}
},
/*
* _closeSelection
*
* Shuts SelectionHandler down.
*/
_closeSelection: function _closeSelection() {
this._clearTimers();
this._cache = null;
this._contentWindow = null;
this._targetElement = null;
this._selectionMoveActive = false;
this._domWinUtils = null;
this._targetIsEditable = false;
this.sendAsync("Content:HandlerShutdown", {});
},
/*************************************************
* Events
*/
/*
* Scroll + selection advancement timer when the monocle is
* outside the bounds of an input control.
*/
scrollTimerCallback: function scrollTimerCallback() {
let result = ChromeSelectionHandler.updateTextEditSelection();
// Update monocle position and speed if we've dragged off to one side
if (result.trigger) {
ChromeSelectionHandler._updateSelectionUI("update", result.start, result.end);
}
},
msgHandler: function msgHandler(aMsg, aJson) {
if (this._debugEvents && "Browser:SelectionMove" != aMsg) {
Util.dumpLn("ChromeSelectionHandler:", aMsg);
}
switch(aMsg) {
case "Browser:SelectionDebug":
this._onSelectionDebug(aJson);
break;
case "Browser:SelectionAttach":
this._onSelectionAttach(aJson);
break;
case "Browser:CaretAttach":
this._onSelectionAttach(aJson);
break;
case "Browser:SelectionClose":
this._onSelectionClose(aJson.clearSelection);
break;
case "Browser:SelectionUpdate":
this._updateSelectionUI("update",
this._mode == kSelectionMode,
this._mode == kSelectionMode,
this._mode == kCaretMode);
break;
case "Browser:SelectionMoveStart":
this._onSelectionMoveStart(aJson);
break;
case "Browser:SelectionMove":
this._onSelectionMove(aJson);
break;
case "Browser:SelectionMoveEnd":
this._onSelectionMoveEnd(aJson);
break;
case "Browser:CaretUpdate":
this._onCaretPositionUpdate(aJson.caret.xPos, aJson.caret.yPos);
break;
case "Browser:CaretMove":
this._onCaretMove(aJson.caret.xPos, aJson.caret.yPos);
break;
case "Browser:SelectionSwitchMode":
this._onSwitchMode(aJson.newMode, aJson.change, aJson.xPos, aJson.yPos);
break;
}
},
/*************************************************
* Utilities
*/
_getSelection: function _getSelection() {
if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) {
return this._targetElement
.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
.editor.selection;
}
return null;
},
_getSelectController: function _getSelectController() {
return this._targetElement
.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
.editor.selectionController;
},
};
ChromeSelectionHandler.__proto__ = new SelectionPrototype();
ChromeSelectionHandler.type = 1; // kChromeSelector

View File

@ -289,6 +289,8 @@ var SelectionHelperUI = {
},
get layerMode() {
if (this._msgTarget && this._msgTarget instanceof SelectionPrototype)
return kChromeLayer;
return kContentLayer;
},
@ -522,6 +524,8 @@ var SelectionHelperUI = {
Elements.browsers.addEventListener("SizeChanged", this, true);
Elements.browsers.addEventListener("ZoomChanged", this, true);
Elements.navbar.addEventListener("transitionend", this, true);
this.overlay.enabled = true;
},
@ -546,6 +550,8 @@ var SelectionHelperUI = {
Elements.browsers.removeEventListener("SizeChanged", this, true);
Elements.browsers.removeEventListener("ZoomChanged", this, true);
Elements.navbar.removeEventListener("transitionend", this, true);
this._shutdownAllMarkers();
this._selectionMarkIds = [];
@ -625,9 +631,6 @@ var SelectionHelperUI = {
* tap that initiates the change.
*/
_transitionFromSelectionToCaret: function _transitionFromSelectionToCaret(aClientX, aClientY) {
// clear existing selection and shutdown SelectionHandler
this.closeEditSession(true);
// Reset some of our state
this._activeSelectionRect = null;
@ -694,7 +697,11 @@ var SelectionHelperUI = {
Util.dumpLn("SelectionHelperUI sendAsyncMessage could not send", aMsg);
return;
}
this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson);
if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) {
this._msgTarget.msgHandler(aMsg, aJson);
} else {
this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson);
}
},
_checkForActiveDrag: function _checkForActiveDrag() {
@ -831,8 +838,15 @@ var SelectionHelperUI = {
// since we always get a single tap before a double, and double tap
// copies selected text.
if (selectionTap) {
aEvent.stopPropagation();
aEvent.preventDefault();
if (!this._targetIsEditable) {
this.closeEditSession(false);
return;
}
// Attach to the newly placed caret position
this._sendAsyncMessage("Browser:CaretAttach", {
xPos: aEvent.clientX,
yPos: aEvent.clientY
});
return;
}
@ -877,6 +891,24 @@ var SelectionHelperUI = {
this.attachToCaret(null, this._lastPoint.xPos, this._lastPoint.yPos);
},
/*
* Detects when the nav bar hides or shows, so we can enable
* selection at the appropriate location once the transition is
* complete, or shutdown selection down when the nav bar is hidden.
*/
_onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) {
if (this.layerMode == kContentLayer) {
return;
}
if (aEvent.propertyName == "bottom" && !Elements.navbar.isShowing) {
this.closeEditSession(false);
return;
}
if (aEvent.propertyName == "bottom" && Elements.navbar.isShowing) {
this._sendAsyncMessage("Browser:SelectionUpdate", {});
}
},
/*
* Event handlers for message manager
*/
@ -1023,6 +1055,10 @@ var SelectionHelperUI = {
case "MozDeckOffsetChanged":
this._onDeckOffsetChanged(aEvent);
break;
case "transitionend":
this._onNavBarTransitionEvent(aEvent);
break;
}
},

View File

@ -49,7 +49,7 @@ dump("### SelectionPrototype.js loaded\n");
var SelectionPrototype = function() { }
SelectionPrototype.prototype = {
_debugEvents: true,
_debugEvents: false,
_cache: {},
_targetElement: null,
_targetIsEditable: true,
@ -307,7 +307,8 @@ SelectionPrototype.prototype = {
let cp =
this._contentWindow.document.caretPositionFromPoint(constrainedPoint.xPos,
constrainedPoint.yPos);
if (!cp || cp.offsetNode != this._targetElement) {
if (!cp || (cp.offsetNode != this._targetElement &&
this._contentWindow.document.getBindingParent(cp.offsetNode) != this._targetElement)) {
return;
}
if (aMarker == "start") {

View File

@ -42,6 +42,7 @@ chrome.jar:
content/helperui/OfflineApps.js (content/helperui/OfflineApps.js)
content/helperui/SelectHelperUI.js (content/helperui/SelectHelperUI.js)
content/helperui/SelectionHelperUI.js (content/helperui/SelectionHelperUI.js)
content/helperui/ChromeSelectionHandler.js (content/helperui/ChromeSelectionHandler.js)
content/helperui/FormHelperUI.js (content/helperui/FormHelperUI.js)
content/helperui/FindHelperUI.js (content/helperui/FindHelperUI.js)
content/helperui/ItemPinHelper.js (content/helperui/ItemPinHelper.js)