mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
Bug 661388: Support selecting text in web content [r=mbrubeck]
This commit is contained in:
parent
bc1e9d8cb9
commit
c299e4e82c
@ -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;
|
||||
|
@ -71,6 +71,7 @@ XPCOMUtils.defineLazyGetter(this, "CommonUI", function() {
|
||||
["FullScreenVideo"],
|
||||
["BadgeHandlers"],
|
||||
["ContextHelper"],
|
||||
["SelectionHelper"],
|
||||
["FormHelperUI"],
|
||||
["FindHelperUI"],
|
||||
["NewTabPopup"],
|
||||
|
@ -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": {
|
||||
|
@ -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"/>
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
BIN
mobile/themes/core/gingerbread/images/handle-end.png
Normal file
BIN
mobile/themes/core/gingerbread/images/handle-end.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
mobile/themes/core/gingerbread/images/handle-start.png
Normal file
BIN
mobile/themes/core/gingerbread/images/handle-start.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
mobile/themes/core/images/handle-end.png
Normal file
BIN
mobile/themes/core/images/handle-end.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
mobile/themes/core/images/handle-start.png
Normal file
BIN
mobile/themes/core/images/handle-start.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user