Bug 661388: Support selecting text in web content [r=mbrubeck]

This commit is contained in:
Mark Finkle 2011-06-23 17:12:38 -04:00
parent bc1e9d8cb9
commit c299e4e82c
15 changed files with 329 additions and 9 deletions

View File

@ -530,7 +530,6 @@
<body>
<![CDATA[
let bcr = this.getBoundingClientRect();
let view = this.getRootView();
let scroll = this.getRootView().getPosition();
return { x: (clientX + scroll.x - bcr.left) / this.scale,
y: (clientY + scroll.y - bcr.top) / this.scale };
@ -538,6 +537,19 @@
</body>
</method>
<method name="transformBrowserToClient">
<parameter name="browserX"/>
<parameter name="browserY"/>
<body>
<![CDATA[
let bcr = this.getBoundingClientRect();
let scroll = this.getRootView().getPosition();
return { x: (browserX * this.scale - scroll.x + bcr.left) ,
y: (browserY * this.scale - scroll.y + bcr.top)};
]]>
</body>
</method>
<constructor>
<![CDATA[
this._frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;

View File

@ -71,6 +71,7 @@ XPCOMUtils.defineLazyGetter(this, "CommonUI", function() {
["FullScreenVideo"],
["BadgeHandlers"],
["ContextHelper"],
["SelectionHelper"],
["FormHelperUI"],
["FindHelperUI"],
["NewTabPopup"],

View File

@ -1754,11 +1754,13 @@ const ContentTouchHandler = {
case "Browser:ContextMenu":
// Long tap
let contextMenu = { name: aMessage.name, json: json, target: aMessage.target };
if (ContextHelper.showPopup(contextMenu)) {
// Stop all input sequences
let event = document.createEvent("Events");
event.initEvent("CancelTouchSequence", true, false);
document.dispatchEvent(event);
if (!SelectionHelper.showPopup(contextMenu)) {
if (ContextHelper.showPopup(contextMenu)) {
// Stop all input sequences
let event = document.createEvent("Events");
event.initEvent("CancelTouchSequence", true, false);
document.dispatchEvent(event);
}
}
break;
case "Browser:CaptureEvents": {

View File

@ -663,6 +663,9 @@
</vbox>
</hbox>
<toolbarbutton id="selectionhandle-start" label="^" left="0" top="0" hidden="true"/>
<toolbarbutton id="selectionhandle-end" label="^" left="0" top="0" hidden="true"/>
<hbox id="menulist-container" class="window-width window-height context-block" top="0" left="0" hidden="true" flex="1">
<vbox id="menulist-popup" class="dialog-dark">
<label id="menulist-title" class="options-title" crop="center" flex="1"/>

View File

@ -1235,6 +1235,167 @@ var ContextHelper = {
}
};
var SelectionHelper = {
enabled: true,
popupState: null,
target: null,
deltaX: -1,
deltaY: -1,
get _start() {
delete this._start;
return this._start = document.getElementById("selectionhandle-start");
},
get _end() {
delete this._end;
return this._end = document.getElementById("selectionhandle-end");
},
showPopup: function ch_showPopup(aMessage) {
if (!this.enabled || aMessage.json.types.indexOf("content-text") == -1)
return false;
this.popupState = aMessage.json;
this.popupState.target = aMessage.target;
this._start.customDragger = {
isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
dragStart: function dragStart(cx, cy, target, scroller) {},
dragStop: function dragStop(dx, dy, scroller) { return false; },
dragMove: function dragMove(dx, dy, scroller) { return false; }
};
this._end.customDragger = {
isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
dragStart: function dragStart(cx, cy, target, scroller) {},
dragStop: function dragStop(dx, dy, scroller) { return false; },
dragMove: function dragMove(dx, dy, scroller) { return false; }
};
this._start.addEventListener("TapDown", this, true);
this._start.addEventListener("TapUp", this, true);
this._end.addEventListener("TapDown", this, true);
this._end.addEventListener("TapUp", this, true);
messageManager.addMessageListener("Browser:SelectionRange", this);
messageManager.addMessageListener("Browser:SelectionCopied", this);
Services.prefs.setBoolPref("accessibility.browsewithcaret", true);
this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionStart", { x: this.popupState.x, y: this.popupState.y });
BrowserUI.pushPopup(this, [this._start, this._end]);
// Hide the selection handles
window.addEventListener("resize", this, true);
window.addEventListener("keypress", this, true);
Elements.browsers.addEventListener("URLChanged", this, true);
Elements.browsers.addEventListener("SizeChanged", this, true);
Elements.browsers.addEventListener("ZoomChanged", this, true);
let event = document.createEvent("Events");
event.initEvent("CancelTouchSequence", true, false);
this.popupState.target.dispatchEvent(event);
return true;
},
hide: function ch_hide() {
if (this._start.hidden)
return;
this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionEnd", {});
this.popupState = null;
Services.prefs.setBoolPref("accessibility.browsewithcaret", false);
this._start.hidden = true;
this._end.hidden = true;
this._start.removeEventListener("TapDown", this, true);
this._start.removeEventListener("TapUp", this, true);
this._end.removeEventListener("TapDown", this, true);
this._end.removeEventListener("TapUp", this, true);
messageManager.removeMessageListener("Browser:SelectionRange", this);
window.removeEventListener("resize", this, true);
window.removeEventListener("keypress", this, true);
Elements.browsers.removeEventListener("URLChanged", this, true);
Elements.browsers.removeEventListener("SizeChanged", this, true);
Elements.browsers.removeEventListener("ZoomChanged", this, true);
BrowserUI.popPopup(this);
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "TapDown":
this.target = aEvent.target;
this.deltaX = (aEvent.clientX - this.target.left);
this.deltaY = (aEvent.clientY - this.target.top);
window.addEventListener("TapMove", this, true);
break;
case "TapUp":
window.removeEventListener("TapMove", this, true);
this.target = null;
this.deltaX = -1;
this.deltaY = -1;
break;
case "TapMove":
if (this.target) {
this.target.left = aEvent.clientX - this.deltaX;
this.target.top = aEvent.clientY - this.deltaY;
let rect = this.target.getBoundingClientRect();
let data = this.target == this._start ? { x: rect.right, y: rect.top, type: "start" } : { x: rect.left, y: rect.top, type: "end" };
let pos = this.popupState.target.transformClientToBrowser(data.x || 0, data.y || 0);
let json = {
type: data.type,
x: pos.x,
y: pos.y
};
this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionMove", json);
}
break;
case "resize":
case "keypress":
case "URLChanged":
case "SizeChanged":
case "ZoomChanged":
this.hide();
break;
}
},
receiveMessage: function sh_receiveMessage(aMessage) {
let json = aMessage.json;
switch (aMessage.name) {
case "Browser:SelectionRange": {
let pos = this.popupState.target.transformBrowserToClient(json.start.x || 0, json.start.y || 0);
this._start.left = pos.x - 32;
this._start.top = pos.y + this.deltaY;
this._start.hidden = false;
pos = this.popupState.target.transformBrowserToClient(json.end.x || 0, json.end.y || 0);
this._end.left = pos.x;
this._end.top = pos.y;
this._end.hidden = false;
break;
}
case "Browser:SelectionCopied": {
messageManager.removeMessageListener("Browser:SelectionCopied", this);
if (json.succeeded) {
let toaster = Cc["@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService);
toaster.showAlertNotification(null, Strings.browser.GetStringFromName("selectionHelper.textCopied"), "", false, "", null);
}
break;
}
}
}
};
var BadgeHandlers = {
_handlers: [
{

View File

@ -918,6 +918,14 @@ var ContextHandler = {
if (hasData && !elem.readOnly)
state.types.push("paste");
break;
} else if (elem instanceof Ci.nsIDOMHTMLParagraphElement ||
elem instanceof Ci.nsIDOMHTMLDivElement ||
elem instanceof Ci.nsIDOMHTMLLIElement ||
elem instanceof Ci.nsIDOMHTMLPreElement ||
elem instanceof Ci.nsIDOMHTMLHeadingElement ||
elem instanceof Ci.nsIDOMHTMLTableCellElement) {
state.types.push("content-text");
break;
}
}
@ -1281,6 +1289,7 @@ var TouchEventHandler = {
if (!this.element)
return;
let cancelled = !this.sendEvent(type, json, this.element);
if (type == "touchend")
this.element = null;
@ -1316,6 +1325,93 @@ var TouchEventHandler = {
}
return aElement.dispatchEvent(evt);
}
}
};
TouchEventHandler.init();
var SelectionHandler = {
cache: {},
init: function() {
addMessageListener("Browser:SelectionStart", this);
addMessageListener("Browser:SelectionEnd", this);
addMessageListener("Browser:SelectionMove", this);
},
receiveMessage: function(aMessage) {
let scrollOffset = ContentScroll.getScrollOffset(content);
let utils = Util.getWindowUtils(content);
let json = aMessage.json;
switch (aMessage.name) {
case "Browser:SelectionStart": {
// Position the caret using a fake mouse click
utils.sendMouseEventToWindow("mousedown", json.x - scrollOffset.x, json.y - scrollOffset.y, 0, 1, 0, true);
utils.sendMouseEventToWindow("mouseup", json.x - scrollOffset.x, json.y - scrollOffset.y, 0, 1, 0, true);
// Select the word nearest the caret
try {
let selcon = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).QueryInterface(Ci.nsISelectionController);
selcon.wordMove(false, false);
selcon.wordMove(true, true);
} catch(e) {
// If we couldn't select the word at the given point, bail
return;
}
// Find the selected text rect and send it back so the handles can position correctly
let selection = content.getSelection();
if (selection.rangeCount == 0)
return;
let range = selection.getRangeAt(0).QueryInterface(Ci.nsIDOMNSRange);
this.cache = { start: {}, end: {} };
let rects = range.getClientRects();
for (let i=0; i<rects.length; i++) {
if (i == 0) {
this.cache.start.x = rects[i].left + scrollOffset.x;
this.cache.start.y = rects[i].bottom + scrollOffset.y;
}
this.cache.end.x = rects[i].right + scrollOffset.x;
this.cache.end.y = rects[i].bottom + scrollOffset.y;
}
sendAsyncMessage("Browser:SelectionRange", this.cache);
break;
}
case "Browser:SelectionEnd": {
let selection = content.getSelection();
let str = selection.toString();
selection.collapseToStart();
if (str.length) {
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
clipboard.copyString(str);
sendAsyncMessage("Browser:SelectionCopied", { succeeded: true });
} else {
sendAsyncMessage("Browser:SelectionCopied", { succeeded: false });
}
break;
}
case "Browser:SelectionMove":
if (json.type == "end") {
this.cache.end.x = json.x - scrollOffset.x;
this.cache.end.y = json.y - scrollOffset.y;
utils.sendMouseEventToWindow("mousedown", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
utils.sendMouseEventToWindow("mouseup", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
} else {
this.cache.start.x = json.x - scrollOffset.x;
this.cache.start.y = json.y - scrollOffset.y;
utils.sendMouseEventToWindow("mousedown", this.cache.start.x, this.cache.start.y, 0, 1, 0, true);
// Don't cause a click. A mousedown is enough to move the caret
//utils.sendMouseEventToWindow("mouseup", this.cache.start.x, this.cache.start.y, 0, 1, 0, true);
utils.sendMouseEventToWindow("mousedown", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
utils.sendMouseEventToWindow("mouseup", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
}
break;
}
}
};
SelectionHandler.init();

View File

@ -90,6 +90,8 @@ function test() {
gCurrentTab = Browser.addTab(testURL, true);
ok(gCurrentTab, "Tab Opened");
SelectionHelper.enabled = false;
window.addEventListener("TapSingle", dumpEvents, true);
window.addEventListener("TapDouble", dumpEvents, true);
window.addEventListener("TapLong", dumpEvents, true);
@ -125,6 +127,7 @@ function runNextTest() {
window.removeEventListener("TapDouble", dumpEvents, true);
window.removeEventListener("TapLong", dumpEvents, true);
SelectionHelper.enabled = true;
Browser.closeTab(gCurrentTab);
finish();
@ -277,7 +280,7 @@ gTests.push({
contextPlainImageTest: function() {
waitForContextMenu(function() {
ok(checkContextTypes(["image","image-shareable","image-loaded"]), "Plain image context types");
ok(checkContextTypes(["image","image-shareable","image-loaded", "content-text"]), "Plain image context types");
}, gCurrentTest.contextNestedImageTest);
let browser = gCurrentTab.browser;

View File

@ -235,3 +235,6 @@ intl.charsetmenu.browser.static=iso-8859-1,utf-8,x-gbk,big5,iso-2022-jp,shift_ji
#Application Menu
appMenu.more=More
#Text Selection
selectionHelper.textCopied=Text copied to clipboard

View File

@ -1531,3 +1531,20 @@ setting {
90% { -moz-transform: translateX(@sidebar_width_minimum@); }
to { -moz-transform: translateX(0); }
}
#selectionhandle-start,
#selectionhandle-end {
min-width: 35px !important;
width: 35px !important;
padding: 0 !important;
margin: 0 !important;
}
#selectionhandle-start {
list-style-image: url("chrome://browser/skin/images/handle-start.png");
}
#selectionhandle-end {
list-style-image: url("chrome://browser/skin/images/handle-end.png");
}

View File

@ -1497,3 +1497,20 @@ setting {
90% { -moz-transform: translateX(-@sidebar_width_minimum@); }
to { -moz-transform: translateX(0); }
}
#selectionhandle-start,
#selectionhandle-end {
min-width: 35px !important;
width: 35px !important;
padding: 0 !important;
margin: 0 !important;
}
#selectionhandle-start {
list-style-image: url("chrome://browser/skin/images/handle-start.png");
}
#selectionhandle-end {
list-style-image: url("chrome://browser/skin/images/handle-end.png");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -122,6 +122,8 @@ chrome.jar:
skin/images/mute-hdpi.png (images/mute-hdpi.png)
skin/images/unmute-hdpi.png (images/unmute-hdpi.png)
skin/images/scrubber-hdpi.png (images/scrubber-hdpi.png)
skin/images/handle-start.png (images/handle-start.png)
skin/images/handle-end.png (images/handle-end.png)
chrome.jar:
% skin browser classic/1.0 %skin/gingerbread/ os=Android osversion=2.3 osversion=2.3.3 osversion=2.3.4
@ -240,6 +242,8 @@ chrome.jar:
skin/gingerbread/images/mute-hdpi.png (gingerbread/images/mute-hdpi.png)
skin/gingerbread/images/unmute-hdpi.png (gingerbread/images/unmute-hdpi.png)
skin/gingerbread/images/scrubber-hdpi.png (gingerbread/images/scrubber-hdpi.png)
skin/gingerbread/images/handle-start.png (gingerbread/images/handle-start.png)
skin/gingerbread/images/handle-end.png (gingerbread/images/handle-end.png)
chrome.jar:
% skin browser classic/1.0 %skin/honeycomb/ os=Android osversion>=3.0
@ -360,4 +364,5 @@ chrome.jar:
skin/honeycomb/images/mute-hdpi.png (honeycomb/images/mute-hdpi.png)
skin/honeycomb/images/unmute-hdpi.png (honeycomb/images/unmute-hdpi.png)
skin/honeycomb/images/scrubber-hdpi.png (honeycomb/images/scrubber-hdpi.png)
skin/honeycomb/images/handle-start.png (images/handle-start.png)
skin/honeycomb/images/handle-end.png (images/handle-end.png)