mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1152 lines
33 KiB
JavaScript
1152 lines
33 KiB
JavaScript
/* 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 management
|
|
*/
|
|
|
|
/*
|
|
* Current monocle image:
|
|
* dimensions: 32 x 24
|
|
* circle center: 16 x 14
|
|
* padding top: 6
|
|
*/
|
|
|
|
// Y axis scroll distance that will disable this module and cancel selection
|
|
const kDisableOnScrollDistance = 25;
|
|
|
|
// Drag hysteresis programmed into monocle drag moves
|
|
const kDragHysteresisDistance = 10;
|
|
|
|
// selection layer id returned from SelectionHandlerUI's layerMode.
|
|
const kChromeLayer = 1;
|
|
const kContentLayer = 2;
|
|
|
|
/*
|
|
* Markers
|
|
*/
|
|
|
|
function MarkerDragger(aMarker) {
|
|
this._marker = aMarker;
|
|
}
|
|
|
|
MarkerDragger.prototype = {
|
|
_selectionHelperUI: null,
|
|
_marker: null,
|
|
_shutdown: false,
|
|
_dragging: false,
|
|
|
|
get marker() {
|
|
return this._marker;
|
|
},
|
|
|
|
set shutdown(aVal) {
|
|
this._shutdown = aVal;
|
|
},
|
|
|
|
get shutdown() {
|
|
return this._shutdown;
|
|
},
|
|
|
|
get dragging() {
|
|
return this._dragging;
|
|
},
|
|
|
|
freeDrag: function freeDrag() {
|
|
return true;
|
|
},
|
|
|
|
isDraggable: function isDraggable(aTarget, aContent) {
|
|
return { x: true, y: true };
|
|
},
|
|
|
|
dragStart: function dragStart(aX, aY, aTarget, aScroller) {
|
|
if (this._shutdown)
|
|
return false;
|
|
this._dragging = true;
|
|
this.marker.dragStart(aX, aY);
|
|
return true;
|
|
},
|
|
|
|
dragStop: function dragStop(aDx, aDy, aScroller) {
|
|
if (this._shutdown)
|
|
return false;
|
|
this._dragging = false;
|
|
this.marker.dragStop(aDx, aDy);
|
|
return true;
|
|
},
|
|
|
|
dragMove: function dragMove(aDx, aDy, aScroller, aIsKenetic, aClientX, aClientY) {
|
|
// Note if aIsKenetic is true this is synthetic movement,
|
|
// we don't want that so return false.
|
|
if (this._shutdown || aIsKenetic)
|
|
return false;
|
|
this.marker.moveBy(aDx, aDy, aClientX, aClientY);
|
|
// return true if we moved, false otherwise. The result
|
|
// is used in deciding if we should repaint between drags.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function Marker(aParent, aTag, aElementId, xPos, yPos) {
|
|
this._xPos = xPos;
|
|
this._yPos = yPos;
|
|
this._selectionHelperUI = aParent;
|
|
this._element = aParent.overlay.getMarker(aElementId);
|
|
this._elementId = aElementId;
|
|
// These get picked in input.js and receives drag input
|
|
this._element.customDragger = new MarkerDragger(this);
|
|
this.tag = aTag;
|
|
}
|
|
|
|
Marker.prototype = {
|
|
_element: null,
|
|
_elementId: "",
|
|
_selectionHelperUI: null,
|
|
_xPos: 0,
|
|
_yPos: 0,
|
|
_xDrag: 0,
|
|
_yDrag: 0,
|
|
_tag: "",
|
|
_hPlane: 0,
|
|
_vPlane: 0,
|
|
|
|
// Tweak me if the monocle graphics change in any way
|
|
_monocleRadius: 8,
|
|
_monocleXHitTextAdjust: -2,
|
|
_monocleYHitTextAdjust: -10,
|
|
|
|
get xPos() {
|
|
return this._xPos;
|
|
},
|
|
|
|
get yPos() {
|
|
return this._yPos;
|
|
},
|
|
|
|
get tag() {
|
|
return this._tag;
|
|
},
|
|
|
|
set tag(aVal) {
|
|
this._tag = aVal;
|
|
},
|
|
|
|
get dragging() {
|
|
return this._element.customDragger.dragging;
|
|
},
|
|
|
|
shutdown: function shutdown() {
|
|
this._element.hidden = true;
|
|
this._element.customDragger.shutdown = true;
|
|
delete this._element.customDragger;
|
|
this._selectionHelperUI = null;
|
|
this._element = null;
|
|
},
|
|
|
|
setTrackBounds: function setTrackBounds(aVerticalPlane, aHorizontalPlane) {
|
|
// monocle boundaries
|
|
this._hPlane = aHorizontalPlane;
|
|
this._vPlane = aVerticalPlane;
|
|
},
|
|
|
|
show: function show() {
|
|
this._element.hidden = false;
|
|
},
|
|
|
|
hide: function hide() {
|
|
this._element.hidden = true;
|
|
},
|
|
|
|
get visible() {
|
|
return this._element.hidden == false;
|
|
},
|
|
|
|
position: function position(aX, aY) {
|
|
if (aX < 0) {
|
|
Util.dumpLn("Marker: aX is negative");
|
|
aX = 0;
|
|
}
|
|
if (aY < 0) {
|
|
Util.dumpLn("Marker: aY is negative");
|
|
aY = 0;
|
|
}
|
|
this._xPos = aX;
|
|
this._yPos = aY;
|
|
this._setPosition();
|
|
},
|
|
|
|
_setPosition: function _setPosition() {
|
|
this._element.left = this._xPos + "px";
|
|
this._element.top = this._yPos + "px";
|
|
},
|
|
|
|
dragStart: function dragStart(aX, aY) {
|
|
this._xDrag = 0;
|
|
this._yDrag = 0;
|
|
this._selectionHelperUI.markerDragStart(this);
|
|
},
|
|
|
|
dragStop: function dragStop(aDx, aDy) {
|
|
this._selectionHelperUI.markerDragStop(this);
|
|
},
|
|
|
|
moveBy: function moveBy(aDx, aDy, aClientX, aClientY) {
|
|
this._xPos -= aDx;
|
|
this._yPos -= aDy;
|
|
this._xDrag -= aDx;
|
|
this._yDrag -= aDy;
|
|
// Add a bit of hysteresis to our directional detection so "big fingers"
|
|
// are detected accurately.
|
|
let direction = "tbd";
|
|
if (Math.abs(this._xDrag) > kDragHysteresisDistance ||
|
|
Math.abs(this._yDrag) > kDragHysteresisDistance) {
|
|
direction = (this._xDrag <= 0 && this._yDrag <= 0 ? "start" : "end");
|
|
}
|
|
// We may swap markers in markerDragMove. If markerDragMove
|
|
// returns true keep processing, otherwise get out of here.
|
|
if (this._selectionHelperUI.markerDragMove(this, direction)) {
|
|
this._setPosition();
|
|
}
|
|
},
|
|
|
|
hitTest: function hitTest(aX, aY) {
|
|
// Gets the pointer of the arrow right in the middle of the
|
|
// monocle.
|
|
aY += this._monocleYHitTextAdjust;
|
|
aX += this._monocleXHitTextAdjust;
|
|
if (aX >= (this._xPos - this._monocleRadius) &&
|
|
aX <= (this._xPos + this._monocleRadius) &&
|
|
aY >= (this._yPos - this._monocleRadius) &&
|
|
aY <= (this._yPos + this._monocleRadius))
|
|
return true;
|
|
return false;
|
|
},
|
|
|
|
swapMonocle: function swapMonocle(aCaret) {
|
|
let targetElement = aCaret._element;
|
|
let targetElementId = aCaret._elementId;
|
|
|
|
aCaret._element = this._element;
|
|
aCaret._element.customDragger._marker = aCaret;
|
|
aCaret._elementId = this._elementId;
|
|
|
|
this._xPos = aCaret._xPos;
|
|
this._yPos = aCaret._yPos;
|
|
this._element = targetElement;
|
|
this._element.customDragger._marker = this;
|
|
this._elementId = targetElementId;
|
|
this._element.visible = true;
|
|
},
|
|
|
|
};
|
|
|
|
/*
|
|
* SelectionHelperUI
|
|
*/
|
|
|
|
var SelectionHelperUI = {
|
|
_debugEvents: false,
|
|
_msgTarget: null,
|
|
_startMark: null,
|
|
_endMark: null,
|
|
_caretMark: null,
|
|
_target: null,
|
|
_movement: { active: false, x:0, y: 0 },
|
|
_activeSelectionRect: null,
|
|
_selectionMarkIds: [],
|
|
_targetIsEditable: false,
|
|
|
|
/*
|
|
* Properties
|
|
*/
|
|
|
|
get startMark() {
|
|
if (this._startMark == null) {
|
|
this._startMark = new Marker(this, "start", this._selectionMarkIds.pop(), 0, 0);
|
|
}
|
|
return this._startMark;
|
|
},
|
|
|
|
get endMark() {
|
|
if (this._endMark == null) {
|
|
this._endMark = new Marker(this, "end", this._selectionMarkIds.pop(), 0, 0);
|
|
}
|
|
return this._endMark;
|
|
},
|
|
|
|
get caretMark() {
|
|
if (this._caretMark == null) {
|
|
this._caretMark = new Marker(this, "caret", this._selectionMarkIds.pop(), 0, 0);
|
|
}
|
|
return this._caretMark;
|
|
},
|
|
|
|
get overlay() {
|
|
return document.getElementById(this.layerMode == kChromeLayer ?
|
|
"chrome-selection-overlay" : "content-selection-overlay");
|
|
},
|
|
|
|
get layerMode() {
|
|
if (this._msgTarget && this._msgTarget instanceof SelectionPrototype)
|
|
return kChromeLayer;
|
|
return kContentLayer;
|
|
},
|
|
|
|
/*
|
|
* isActive (prop)
|
|
*
|
|
* Determines if a selection edit session is currently active.
|
|
*/
|
|
get isActive() {
|
|
return this._msgTarget ? true : false;
|
|
},
|
|
|
|
/*
|
|
* isSelectionUIVisible (prop)
|
|
*
|
|
* Determines if edit session monocles are visible. Useful
|
|
* in checking if selection handler is setup for tests.
|
|
*/
|
|
get isSelectionUIVisible() {
|
|
if (!this._msgTarget || !this._startMark)
|
|
return false;
|
|
return this._startMark.visible;
|
|
},
|
|
|
|
/*
|
|
* isCaretUIVisible (prop)
|
|
*
|
|
* Determines if caret browsing monocle is visible. Useful
|
|
* in checking if selection handler is setup for tests.
|
|
*/
|
|
get isCaretUIVisible() {
|
|
if (!this._msgTarget || !this._caretMark)
|
|
return false;
|
|
return this._caretMark.visible;
|
|
},
|
|
|
|
/*
|
|
* hasActiveDrag (prop)
|
|
*
|
|
* Determines if a marker is actively being dragged (missing call
|
|
* to markerDragStop). Useful in checking if selection handler is
|
|
* setup for tests.
|
|
*/
|
|
get hasActiveDrag() {
|
|
if (!this._msgTarget)
|
|
return false;
|
|
if ((this._caretMark && this._caretMark.dragging) ||
|
|
(this._startMark && this._startMark.dragging) ||
|
|
(this._endMark && this._endMark.dragging))
|
|
return true;
|
|
return false;
|
|
},
|
|
|
|
/*
|
|
* Public apis
|
|
*/
|
|
|
|
/*
|
|
* pingSelectionHandler
|
|
*
|
|
* Ping the SelectionHandler and wait for the right response. Insures
|
|
* all previous messages have been received. Useful in checking if
|
|
* selection handler is setup for tests.
|
|
*
|
|
* @return a promise
|
|
*/
|
|
pingSelectionHandler: function pingSelectionHandler() {
|
|
if (!this.isActive)
|
|
return null;
|
|
|
|
if (this._pingCount == undefined) {
|
|
this._pingCount = 0;
|
|
this._pingArray = [];
|
|
}
|
|
|
|
this._pingCount++;
|
|
|
|
let deferred = Promise.defer();
|
|
this._pingArray.push({
|
|
id: this._pingCount,
|
|
deferred: deferred
|
|
});
|
|
|
|
this._sendAsyncMessage("Browser:SelectionHandlerPing", { id: this._pingCount });
|
|
return deferred.promise;
|
|
},
|
|
|
|
/*
|
|
* openEditSession
|
|
*
|
|
* Attempts to select underlying text at a point and begins editing
|
|
* the section.
|
|
*
|
|
* @param aMsgTarget - Browser or chrome message target
|
|
* @param aX, aY - Browser relative client coordinates.
|
|
*/
|
|
openEditSession: function openEditSession(aMsgTarget, aX, aY) {
|
|
if (!aMsgTarget || this.isActive)
|
|
return;
|
|
this._init(aMsgTarget);
|
|
this._setupDebugOptions();
|
|
|
|
// Send this over to SelectionHandler in content, they'll message us
|
|
// back with information on the current selection. SelectionStart
|
|
// takes client coordinates.
|
|
this._sendAsyncMessage("Browser:SelectionStart", {
|
|
xPos: aX,
|
|
yPos: aY
|
|
});
|
|
},
|
|
|
|
/*
|
|
* attachEditSession
|
|
*
|
|
* Attaches to existing selection and begins editing.
|
|
*
|
|
* @param aMsgTarget - Browser or chrome message target
|
|
* @param aX, aY - Browser relative client coordinates.
|
|
*/
|
|
attachEditSession: function attachEditSession(aMsgTarget, aX, aY) {
|
|
if (!aMsgTarget || this.isActive)
|
|
return;
|
|
this._init(aMsgTarget);
|
|
this._setupDebugOptions();
|
|
|
|
// Send this over to SelectionHandler in content, they'll message us
|
|
// back with information on the current selection. SelectionAttach
|
|
// takes client coordinates.
|
|
this._sendAsyncMessage("Browser:SelectionAttach", {
|
|
xPos: aX,
|
|
yPos: aY
|
|
});
|
|
},
|
|
|
|
/*
|
|
* attachToCaret
|
|
*
|
|
* Initiates a touch caret selection session for a text input.
|
|
* Can be called multiple times to move the caret marker around.
|
|
*
|
|
* Note the caret marker is pretty limited in functionality. The
|
|
* only thing is can do is be displayed at the caret position.
|
|
* Once the user starts a drag, the caret marker is hidden, and
|
|
* the start and end markers take over.
|
|
*
|
|
* @param aMsgTarget - Browser or chrome message target
|
|
* @param aX, aY - Browser relative client coordinates of the tap
|
|
* that initiated the session.
|
|
*/
|
|
attachToCaret: function attachToCaret(aMsgTarget, aX, aY) {
|
|
if (!this.isActive) {
|
|
this._init(aMsgTarget);
|
|
this._setupDebugOptions();
|
|
} else {
|
|
this._hideMonocles();
|
|
}
|
|
|
|
this._lastPoint = { xPos: aX, yPos: aY };
|
|
|
|
this._sendAsyncMessage("Browser:CaretAttach", {
|
|
xPos: aX,
|
|
yPos: aY
|
|
});
|
|
},
|
|
|
|
/*
|
|
* canHandleContextMenuMsg
|
|
*
|
|
* Determines if we can handle a ContextMenuHandler message.
|
|
*/
|
|
canHandleContextMenuMsg: function canHandleContextMenuMsg(aMessage) {
|
|
if (aMessage.json.types.indexOf("content-text") != -1)
|
|
return true;
|
|
return false;
|
|
},
|
|
|
|
/*
|
|
* closeEditSession(aClearSelection)
|
|
*
|
|
* Closes an active edit session and shuts down. Does not clear existing
|
|
* selection regions if they exist.
|
|
*
|
|
* @param aClearSelection bool indicating if the selection handler should also
|
|
* clear any selection. optional, the default is false.
|
|
*/
|
|
closeEditSession: function closeEditSession(aClearSelection) {
|
|
if (!this.isActive) {
|
|
return;
|
|
}
|
|
// This will callback in _selectionHandlerShutdown in
|
|
// which we will call _shutdown().
|
|
let clearSelection = aClearSelection || false;
|
|
this._sendAsyncMessage("Browser:SelectionClose", {
|
|
clearSelection: clearSelection
|
|
});
|
|
},
|
|
|
|
/*
|
|
* Init and shutdown
|
|
*/
|
|
|
|
_init: function _init(aMsgTarget) {
|
|
// store the target message manager
|
|
this._msgTarget = aMsgTarget;
|
|
|
|
// Init our list of available monocle ids
|
|
this._setupMonocleIdArray();
|
|
|
|
// Init selection rect info
|
|
this._activeSelectionRect = Util.getCleanRect();
|
|
this._targetElementRect = Util.getCleanRect();
|
|
|
|
// SelectionHandler messages
|
|
messageManager.addMessageListener("Content:SelectionRange", this);
|
|
messageManager.addMessageListener("Content:SelectionCopied", this);
|
|
messageManager.addMessageListener("Content:SelectionFail", this);
|
|
messageManager.addMessageListener("Content:SelectionDebugRect", this);
|
|
messageManager.addMessageListener("Content:HandlerShutdown", this);
|
|
messageManager.addMessageListener("Content:SelectionHandlerPong", this);
|
|
|
|
window.addEventListener("keypress", this, true);
|
|
window.addEventListener("click", this, false);
|
|
window.addEventListener("touchstart", this, true);
|
|
window.addEventListener("touchend", this, true);
|
|
window.addEventListener("touchmove", this, true);
|
|
window.addEventListener("MozPrecisePointer", this, true);
|
|
window.addEventListener("MozDeckOffsetChanging", this, true);
|
|
window.addEventListener("MozDeckOffsetChanged", this, true);
|
|
|
|
Elements.browsers.addEventListener("URLChanged", this, true);
|
|
Elements.browsers.addEventListener("SizeChanged", this, true);
|
|
Elements.browsers.addEventListener("ZoomChanged", this, true);
|
|
|
|
Elements.navbar.addEventListener("transitionend", this, true);
|
|
|
|
this.overlay.enabled = true;
|
|
},
|
|
|
|
_shutdown: function _shutdown() {
|
|
messageManager.removeMessageListener("Content:SelectionRange", this);
|
|
messageManager.removeMessageListener("Content:SelectionCopied", this);
|
|
messageManager.removeMessageListener("Content:SelectionFail", this);
|
|
messageManager.removeMessageListener("Content:SelectionDebugRect", this);
|
|
messageManager.removeMessageListener("Content:HandlerShutdown", this);
|
|
messageManager.removeMessageListener("Content:SelectionHandlerPong", this);
|
|
|
|
window.removeEventListener("keypress", this, true);
|
|
window.removeEventListener("click", this, false);
|
|
window.removeEventListener("touchstart", this, true);
|
|
window.removeEventListener("touchend", this, true);
|
|
window.removeEventListener("touchmove", this, true);
|
|
window.removeEventListener("MozPrecisePointer", this, true);
|
|
window.removeEventListener("MozDeckOffsetChanging", this, true);
|
|
window.removeEventListener("MozDeckOffsetChanged", this, true);
|
|
|
|
Elements.browsers.removeEventListener("URLChanged", this, true);
|
|
Elements.browsers.removeEventListener("SizeChanged", this, true);
|
|
Elements.browsers.removeEventListener("ZoomChanged", this, true);
|
|
|
|
Elements.navbar.removeEventListener("transitionend", this, true);
|
|
|
|
this._shutdownAllMarkers();
|
|
|
|
this._selectionMarkIds = [];
|
|
this._msgTarget = null;
|
|
this._activeSelectionRect = null;
|
|
|
|
this.overlay.displayDebugLayer = false;
|
|
this.overlay.enabled = false;
|
|
},
|
|
|
|
/*
|
|
* Utilities
|
|
*/
|
|
|
|
/*
|
|
* _swapCaretMarker
|
|
*
|
|
* Swap two drag markers - used when transitioning from caret mode
|
|
* to selection mode. We take the current caret marker (which is in a
|
|
* drag state) and swap it out with one of the selection markers.
|
|
*/
|
|
_swapCaretMarker: function _swapCaretMarker(aDirection) {
|
|
let targetMark = null;
|
|
if (aDirection == "start")
|
|
targetMark = this.startMark;
|
|
else
|
|
targetMark = this.endMark;
|
|
let caret = this.caretMark;
|
|
targetMark.swapMonocle(caret);
|
|
let id = caret._elementId;
|
|
caret.shutdown();
|
|
this._caretMark = null;
|
|
this._selectionMarkIds.push(id);
|
|
},
|
|
|
|
/*
|
|
* _transitionFromCaretToSelection
|
|
*
|
|
* Transitions from caret mode to text selection mode.
|
|
*/
|
|
_transitionFromCaretToSelection: function _transitionFromCaretToSelection(aDirection) {
|
|
// Get selection markers initialized if they aren't already
|
|
{ let mark = this.startMark; mark = this.endMark; }
|
|
|
|
// Swap the caret marker out for the start or end marker depending
|
|
// on direction.
|
|
this._swapCaretMarker(aDirection);
|
|
|
|
let targetMark = null;
|
|
if (aDirection == "start")
|
|
targetMark = this.startMark;
|
|
else
|
|
targetMark = this.endMark;
|
|
|
|
// Position both in the same starting location.
|
|
this.startMark.position(targetMark.xPos, targetMark.yPos);
|
|
this.endMark.position(targetMark.xPos, targetMark.yPos);
|
|
|
|
// Start the selection monocle drag. SelectionHandler relies on this
|
|
// for getting initialized. This will also trigger a message back for
|
|
// monocle positioning. Note, markerDragMove is still on the stack in
|
|
// this call!
|
|
this._sendAsyncMessage("Browser:SelectionSwitchMode", {
|
|
newMode: "selection",
|
|
change: targetMark.tag,
|
|
xPos: this._msgTarget.ctobx(targetMark.xPos, true),
|
|
yPos: this._msgTarget.ctoby(targetMark.yPos, true),
|
|
});
|
|
},
|
|
|
|
/*
|
|
* _transitionFromSelectionToCaret
|
|
*
|
|
* Transitions from text selection mode to caret mode.
|
|
*
|
|
* @param aClientX, aClientY - client coordinates of the
|
|
* tap that initiates the change.
|
|
*/
|
|
_transitionFromSelectionToCaret: function _transitionFromSelectionToCaret(aClientX, aClientY) {
|
|
// Reset some of our state
|
|
this._activeSelectionRect = null;
|
|
|
|
// Reset the monocles
|
|
this._shutdownAllMarkers();
|
|
this._setupMonocleIdArray();
|
|
|
|
// Translate to browser relative client coordinates
|
|
let coords =
|
|
this._msgTarget.ptClientToBrowser(aClientX, aClientY, true);
|
|
|
|
// Init SelectionHandler and turn on caret selection. Note the focus caret
|
|
// will have been removed from the target element due to the shutdown call.
|
|
// This won't set the caret position on its own.
|
|
this._sendAsyncMessage("Browser:CaretAttach", {
|
|
xPos: coords.x,
|
|
yPos: coords.y
|
|
});
|
|
|
|
// Set the caret position
|
|
this._setCaretPositionAtPoint(coords.x, coords.y);
|
|
},
|
|
|
|
/*
|
|
* _setupDebugOptions
|
|
*
|
|
* Sends a message over to content instructing it to
|
|
* turn on various debug features.
|
|
*/
|
|
_setupDebugOptions: function _setupDebugOptions() {
|
|
// Debug options for selection
|
|
let debugOpts = { dumpRanges: false, displayRanges: false, dumpEvents: false };
|
|
try {
|
|
if (Services.prefs.getBoolPref(kDebugSelectionDumpPref))
|
|
debugOpts.displayRanges = true;
|
|
} catch (ex) {}
|
|
try {
|
|
if (Services.prefs.getBoolPref(kDebugSelectionDisplayPref))
|
|
debugOpts.displayRanges = true;
|
|
} catch (ex) {}
|
|
try {
|
|
if (Services.prefs.getBoolPref(kDebugSelectionDumpEvents)) {
|
|
debugOpts.dumpEvents = true;
|
|
this._debugEvents = true;
|
|
}
|
|
} catch (ex) {}
|
|
|
|
if (debugOpts.displayRanges || debugOpts.dumpRanges || debugOpts.dumpEvents) {
|
|
// Turn on the debug layer
|
|
this.overlay.displayDebugLayer = true;
|
|
// Tell SelectionHandler what to do
|
|
this._sendAsyncMessage("Browser:SelectionDebug", debugOpts);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* _sendAsyncMessage
|
|
*
|
|
* Helper for sending a message to SelectionHandler.
|
|
*/
|
|
_sendAsyncMessage: function _sendAsyncMessage(aMsg, aJson) {
|
|
if (!this._msgTarget) {
|
|
if (this._debugEvents)
|
|
Util.dumpLn("SelectionHelperUI sendAsyncMessage could not send", aMsg);
|
|
return;
|
|
}
|
|
if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) {
|
|
this._msgTarget.msgHandler(aMsg, aJson);
|
|
} else {
|
|
this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson);
|
|
}
|
|
},
|
|
|
|
_checkForActiveDrag: function _checkForActiveDrag() {
|
|
return (this.startMark.dragging || this.endMark.dragging ||
|
|
this.caretMark.dragging);
|
|
},
|
|
|
|
_hitTestSelection: function _hitTestSelection(aEvent) {
|
|
// Ignore if the double tap isn't on our active selection rect.
|
|
if (this._activeSelectionRect &&
|
|
Util.pointWithinRect(aEvent.clientX, aEvent.clientY, this._activeSelectionRect)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/*
|
|
* _setCaretPositionAtPoint - sets the current caret position.
|
|
*
|
|
* @param aX, aY - browser relative client coordinates
|
|
*/
|
|
_setCaretPositionAtPoint: function _setCaretPositionAtPoint(aX, aY) {
|
|
let json = this._getMarkerBaseMessage("caret");
|
|
json.caret.xPos = aX;
|
|
json.caret.yPos = aY;
|
|
this._sendAsyncMessage("Browser:CaretUpdate", json);
|
|
},
|
|
|
|
/*
|
|
* _shutdownAllMarkers
|
|
*
|
|
* Helper for shutting down all markers and
|
|
* freeing the objects associated with them.
|
|
*/
|
|
_shutdownAllMarkers: function _shutdownAllMarkers() {
|
|
if (this._startMark)
|
|
this._startMark.shutdown();
|
|
if (this._endMark)
|
|
this._endMark.shutdown();
|
|
if (this._caretMark)
|
|
this._caretMark.shutdown();
|
|
|
|
this._startMark = null;
|
|
this._endMark = null;
|
|
this._caretMark = null;
|
|
},
|
|
|
|
/*
|
|
* _setupMonocleIdArray
|
|
*
|
|
* Helper for initing the array of monocle anon ids.
|
|
*/
|
|
_setupMonocleIdArray: function _setupMonocleIdArray() {
|
|
this._selectionMarkIds = ["selectionhandle-mark1",
|
|
"selectionhandle-mark2",
|
|
"selectionhandle-mark3"];
|
|
},
|
|
|
|
_hideMonocles: function _hideMonocles() {
|
|
if (this._startMark) {
|
|
this.startMark.hide();
|
|
}
|
|
if (this._endMark) {
|
|
this.endMark.hide();
|
|
}
|
|
if (this._caretMark) {
|
|
this.caretMark.hide();
|
|
}
|
|
},
|
|
|
|
_showMonocles: function _showMonocles(aSelection) {
|
|
if (!aSelection) {
|
|
this.caretMark.show();
|
|
} else {
|
|
this.endMark.show();
|
|
this.startMark.show();
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Event handlers for document events
|
|
*/
|
|
|
|
/*
|
|
* _onTap
|
|
*
|
|
* Handles taps that move the current caret around in text edits,
|
|
* clear active selection and focus when neccessary, or change
|
|
* modes.
|
|
* Future: changing selection modes by tapping on a monocle.
|
|
*/
|
|
_onTap: function _onTap(aEvent) {
|
|
let clientCoords =
|
|
this._msgTarget.ptBrowserToClient(aEvent.clientX, aEvent.clientY, true);
|
|
|
|
// Check for a tap on a monocle
|
|
if (this.startMark.hitTest(clientCoords.x, clientCoords.y) ||
|
|
this.endMark.hitTest(clientCoords.x, clientCoords.y)) {
|
|
aEvent.stopPropagation();
|
|
aEvent.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Is the tap point within the bound of the target element? This
|
|
// is useful if we are dealing with some sort of input control.
|
|
// Not so much if the target is a page or div.
|
|
let pointInTargetElement =
|
|
Util.pointWithinRect(clientCoords.x, clientCoords.y,
|
|
this._targetElementRect);
|
|
|
|
// If the tap is within an editable element and the caret monocle is
|
|
// active, update the caret.
|
|
if (this.caretMark.visible && pointInTargetElement) {
|
|
// setCaretPositionAtPoint takes browser relative coords.
|
|
this._setCaretPositionAtPoint(aEvent.clientX, aEvent.clientY);
|
|
return;
|
|
}
|
|
|
|
// if the target is editable, we have selection or a caret, and the
|
|
// user clicks off the target clear selection and remove focus from
|
|
// the input.
|
|
if (this._targetIsEditable && !pointInTargetElement) {
|
|
// shutdown but leave focus alone. the event will fall through
|
|
// and the dom will update focus for us. If the user tapped on
|
|
// another input, we'll get a attachToCaret call soonish on the
|
|
// new input.
|
|
this.closeEditSession(false);
|
|
return;
|
|
}
|
|
|
|
let selectionTap = this._hitTestSelection(aEvent);
|
|
|
|
// If the tap is in the selection, just ignore it. We disallow this
|
|
// since we always get a single tap before a double, and double tap
|
|
// copies selected text.
|
|
if (selectionTap) {
|
|
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;
|
|
}
|
|
|
|
// A tap within an editable but outside active selection, clear the
|
|
// selection and flip back to caret mode.
|
|
if (this.startMark.visible && pointInTargetElement &&
|
|
this._targetIsEditable) {
|
|
this._transitionFromSelectionToCaret(clientCoords.x, clientCoords.y);
|
|
}
|
|
|
|
// If we have active selection in anything else don't let the event get
|
|
// to content. Prevents random taps from killing active selection.
|
|
aEvent.stopPropagation();
|
|
aEvent.preventDefault();
|
|
},
|
|
|
|
_onKeypress: function _onKeypress() {
|
|
this.closeEditSession();
|
|
},
|
|
|
|
_onResize: function _onResize() {
|
|
this._sendAsyncMessage("Browser:SelectionUpdate", {});
|
|
},
|
|
|
|
/*
|
|
* _onDeckOffsetChanging - fired by ContentAreaObserver before the browser
|
|
* deck is shifted for form input access in response to a soft keyboard
|
|
* display.
|
|
*/
|
|
_onDeckOffsetChanging: function _onDeckOffsetChanging(aEvent) {
|
|
// Hide the monocles temporarily
|
|
this._hideMonocles();
|
|
},
|
|
|
|
/*
|
|
* _onDeckOffsetChanged - fired by ContentAreaObserver after the browser
|
|
* deck is shifted for form input access in response to a soft keyboard
|
|
* display.
|
|
*/
|
|
_onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) {
|
|
// Update the monocle position and display
|
|
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
|
|
*/
|
|
|
|
_onDebugRectRequest: function _onDebugRectRequest(aMsg) {
|
|
this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom,
|
|
aMsg.color, aMsg.fill, aMsg.id);
|
|
},
|
|
|
|
_selectionHandlerShutdown: function _selectionHandlerShutdown() {
|
|
this._shutdown();
|
|
},
|
|
|
|
/*
|
|
* Message handlers
|
|
*/
|
|
|
|
_onSelectionCopied: function _onSelectionCopied(json) {
|
|
this.closeEditSession(true);
|
|
},
|
|
|
|
_onSelectionRangeChange: function _onSelectionRangeChange(json) {
|
|
let haveSelectionRect = true;
|
|
|
|
if (json.updateStart) {
|
|
this.startMark.position(this._msgTarget.btocx(json.start.xPos, true),
|
|
this._msgTarget.btocy(json.start.yPos, true));
|
|
}
|
|
if (json.updateEnd) {
|
|
this.endMark.position(this._msgTarget.btocx(json.end.xPos, true),
|
|
this._msgTarget.btocy(json.end.yPos, true));
|
|
}
|
|
|
|
if (json.updateCaret) {
|
|
// If selectionRangeFound is set SelectionHelper found a range we can
|
|
// attach to. If not, there's no text in the control, and hence no caret
|
|
// position information we can use.
|
|
haveSelectionRect = json.selectionRangeFound;
|
|
if (json.selectionRangeFound) {
|
|
this.caretMark.position(this._msgTarget.btocx(json.caret.xPos, true),
|
|
this._msgTarget.btocy(json.caret.yPos, true));
|
|
this.caretMark.show();
|
|
}
|
|
}
|
|
|
|
this._targetIsEditable = json.targetIsEditable;
|
|
this._activeSelectionRect = haveSelectionRect ?
|
|
this._msgTarget.rectBrowserToClient(json.selection, true) :
|
|
this._activeSelectionRect = Util.getCleanRect();
|
|
this._targetElementRect =
|
|
this._msgTarget.rectBrowserToClient(json.element, true);
|
|
|
|
// Ifd this is the end of a selection move show the appropriate
|
|
// monocle images. src=(start, update, end, caret)
|
|
if (json.src == "start" || json.src == "end") {
|
|
this._showMonocles(true);
|
|
}
|
|
},
|
|
|
|
_onSelectionFail: function _onSelectionFail() {
|
|
Util.dumpLn("failed to get a selection.");
|
|
this.closeEditSession();
|
|
},
|
|
|
|
/*
|
|
* _onPong
|
|
*
|
|
* Handles the closure of promise we return when we send a ping
|
|
* to SelectionHandler in pingSelectionHandler. Testing use.
|
|
*/
|
|
_onPong: function _onPong(aId) {
|
|
let ping = this._pingArray.pop();
|
|
if (ping.id != aId) {
|
|
ping.deferred.reject(
|
|
new Error("Selection module's pong doesn't match our last ping."));
|
|
}
|
|
ping.deferred.resolve();
|
|
},
|
|
|
|
/*
|
|
* Events
|
|
*/
|
|
|
|
handleEvent: function handleEvent(aEvent) {
|
|
if (this._debugEvents && aEvent.type != "touchmove") {
|
|
Util.dumpLn("SelectionHelperUI:", aEvent.type);
|
|
}
|
|
switch (aEvent.type) {
|
|
case "click":
|
|
this._onTap(aEvent);
|
|
break;
|
|
|
|
case "touchstart": {
|
|
if (aEvent.touches.length != 1)
|
|
break;
|
|
let touch = aEvent.touches[0];
|
|
this._movement.x = this._movement.y = 0;
|
|
this._movement.x = touch.clientX;
|
|
this._movement.y = touch.clientY;
|
|
this._movement.active = true;
|
|
break;
|
|
}
|
|
|
|
case "touchend":
|
|
if (aEvent.touches.length == 0)
|
|
this._movement.active = false;
|
|
break;
|
|
|
|
case "touchmove": {
|
|
if (aEvent.touches.length != 1)
|
|
break;
|
|
let touch = aEvent.touches[0];
|
|
// Clear our selection overlay when the user starts to pan the page
|
|
if (!this._checkForActiveDrag() && this._movement.active) {
|
|
let distanceY = touch.clientY - this._movement.y;
|
|
if (Math.abs(distanceY) > kDisableOnScrollDistance) {
|
|
this.closeEditSession(true);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "keypress":
|
|
this._onKeypress(aEvent);
|
|
break;
|
|
|
|
case "SizeChanged":
|
|
this._onResize(aEvent);
|
|
break;
|
|
|
|
case "URLChanged":
|
|
this._shutdown();
|
|
break;
|
|
|
|
case "ZoomChanged":
|
|
case "MozPrecisePointer":
|
|
this.closeEditSession(true);
|
|
break;
|
|
|
|
case "MozDeckOffsetChanging":
|
|
this._onDeckOffsetChanging(aEvent);
|
|
break;
|
|
|
|
case "MozDeckOffsetChanged":
|
|
this._onDeckOffsetChanged(aEvent);
|
|
break;
|
|
|
|
case "transitionend":
|
|
this._onNavBarTransitionEvent(aEvent);
|
|
break;
|
|
}
|
|
},
|
|
|
|
receiveMessage: function sh_receiveMessage(aMessage) {
|
|
if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name);
|
|
let json = aMessage.json;
|
|
switch (aMessage.name) {
|
|
case "Content:SelectionFail":
|
|
this._onSelectionFail();
|
|
break;
|
|
case "Content:SelectionRange":
|
|
this._onSelectionRangeChange(json);
|
|
break;
|
|
case "Content:SelectionCopied":
|
|
this._onSelectionCopied(json);
|
|
break;
|
|
case "Content:SelectionDebugRect":
|
|
this._onDebugRectRequest(json);
|
|
break;
|
|
case "Content:HandlerShutdown":
|
|
this._selectionHandlerShutdown();
|
|
break;
|
|
case "Content:SelectionHandlerPong":
|
|
this._onPong(json.id);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Callbacks from markers
|
|
*/
|
|
|
|
_getMarkerBaseMessage: function _getMarkerBaseMessage(aMarkerTag) {
|
|
return {
|
|
change: aMarkerTag,
|
|
start: {
|
|
xPos: this._msgTarget.ctobx(this.startMark.xPos, true),
|
|
yPos: this._msgTarget.ctoby(this.startMark.yPos, true)
|
|
},
|
|
end: {
|
|
xPos: this._msgTarget.ctobx(this.endMark.xPos, true),
|
|
yPos: this._msgTarget.ctoby(this.endMark.yPos, true)
|
|
},
|
|
caret: {
|
|
xPos: this._msgTarget.ctobx(this.caretMark.xPos, true),
|
|
yPos: this._msgTarget.ctoby(this.caretMark.yPos, true)
|
|
},
|
|
};
|
|
},
|
|
|
|
markerDragStart: function markerDragStart(aMarker) {
|
|
let json = this._getMarkerBaseMessage(aMarker.tag);
|
|
if (aMarker.tag == "caret") {
|
|
this._sendAsyncMessage("Browser:CaretMove", json);
|
|
return;
|
|
}
|
|
this._sendAsyncMessage("Browser:SelectionMoveStart", json);
|
|
},
|
|
|
|
markerDragStop: function markerDragStop(aMarker) {
|
|
let json = this._getMarkerBaseMessage(aMarker.tag);
|
|
if (aMarker.tag == "caret") {
|
|
this._sendAsyncMessage("Browser:CaretUpdate", json);
|
|
return;
|
|
}
|
|
this._sendAsyncMessage("Browser:SelectionMoveEnd", json);
|
|
},
|
|
|
|
markerDragMove: function markerDragMove(aMarker, aDirection) {
|
|
if (aMarker.tag == "caret") {
|
|
// If direction is "tbd" the drag monocle hasn't determined which
|
|
// direction the user is dragging.
|
|
if (aDirection != "tbd") {
|
|
// We are going to transition from caret browsing mode to selection
|
|
// mode on drag. So swap the caret monocle for a start or end monocle
|
|
// depending on the direction of the drag, and start selecting text.
|
|
this._transitionFromCaretToSelection(aDirection);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// We'll re-display these after the drag is complete.
|
|
this._hideMonocles();
|
|
|
|
let json = this._getMarkerBaseMessage(aMarker.tag);
|
|
this._sendAsyncMessage("Browser:SelectionMove", json);
|
|
return true;
|
|
},
|
|
};
|