mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
615 lines
20 KiB
JavaScript
615 lines
20 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/. */
|
|
|
|
let Ci = Components.interfaces;
|
|
let Cc = Components.classes;
|
|
|
|
dump("### SelectionHandler.js loaded\n");
|
|
|
|
var SelectionHandler = {
|
|
init: function init() {
|
|
this.type = kContentSelector;
|
|
this.snap = true;
|
|
this.lastYPos = this.lastXPos = null;
|
|
addMessageListener("Browser:SelectionStart", this);
|
|
addMessageListener("Browser:SelectionAttach", this);
|
|
addMessageListener("Browser:SelectionEnd", this);
|
|
addMessageListener("Browser:SelectionMoveStart", this);
|
|
addMessageListener("Browser:SelectionMove", this);
|
|
addMessageListener("Browser:SelectionMoveEnd", this);
|
|
addMessageListener("Browser:SelectionUpdate", this);
|
|
addMessageListener("Browser:SelectionClose", this);
|
|
addMessageListener("Browser:SelectionCopy", this);
|
|
addMessageListener("Browser:SelectionDebug", this);
|
|
addMessageListener("Browser:CaretAttach", this);
|
|
addMessageListener("Browser:CaretMove", this);
|
|
addMessageListener("Browser:CaretUpdate", this);
|
|
addMessageListener("Browser:SelectionSwitchMode", this);
|
|
addMessageListener("Browser:RepositionInfoRequest", this);
|
|
addMessageListener("Browser:SelectionHandlerPing", this);
|
|
addMessageListener("Browser:ResetLastPos", this);
|
|
},
|
|
|
|
shutdown: function shutdown() {
|
|
removeMessageListener("Browser:SelectionStart", this);
|
|
removeMessageListener("Browser:SelectionAttach", this);
|
|
removeMessageListener("Browser:SelectionEnd", this);
|
|
removeMessageListener("Browser:SelectionMoveStart", this);
|
|
removeMessageListener("Browser:SelectionMove", this);
|
|
removeMessageListener("Browser:SelectionMoveEnd", this);
|
|
removeMessageListener("Browser:SelectionUpdate", this);
|
|
removeMessageListener("Browser:SelectionClose", this);
|
|
removeMessageListener("Browser:SelectionCopy", this);
|
|
removeMessageListener("Browser:SelectionDebug", this);
|
|
removeMessageListener("Browser:CaretAttach", this);
|
|
removeMessageListener("Browser:CaretMove", this);
|
|
removeMessageListener("Browser:CaretUpdate", this);
|
|
removeMessageListener("Browser:SelectionSwitchMode", this);
|
|
removeMessageListener("Browser:RepositionInfoRequest", this);
|
|
removeMessageListener("Browser:SelectionHandlerPing", this);
|
|
removeMessageListener("Browser:ResetLastPos", this);
|
|
},
|
|
|
|
sendAsync: function sendAsync(aMsg, aJson) {
|
|
sendAsyncMessage(aMsg, aJson);
|
|
},
|
|
|
|
/*************************************************
|
|
* Browser event handlers
|
|
*/
|
|
|
|
/*
|
|
* Selection start event handler
|
|
*/
|
|
_onSelectionStart: function _onSelectionStart(aJson) {
|
|
// Init content window information
|
|
if (!this._initTargetInfo(aJson.xPos, aJson.yPos)) {
|
|
this._onFail("failed to get target information");
|
|
return;
|
|
}
|
|
|
|
// for context menu select command, which doesn't trigger
|
|
// form input focus changes.
|
|
if (aJson.setFocus && this._targetIsEditable) {
|
|
this._targetElement.focus();
|
|
}
|
|
|
|
// Clear any existing selection from the document
|
|
let selection = this._contentWindow.getSelection();
|
|
selection.removeAllRanges();
|
|
|
|
// Set our initial selection, aX and aY should be in client coordinates.
|
|
let framePoint = this._clientPointToFramePoint({ xPos: aJson.xPos, yPos: aJson.yPos });
|
|
if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
|
|
Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
|
|
this._onFail("failed to set selection at point");
|
|
return;
|
|
}
|
|
|
|
// Update the position of our selection monocles
|
|
this._updateSelectionUI("start", true, true);
|
|
},
|
|
|
|
_onSelectionAttach: function _onSelectionAttach(aX, aY) {
|
|
// Init content window information
|
|
if (!this._initTargetInfo(aX, aY)) {
|
|
this._onFail("failed to get frame offset");
|
|
return;
|
|
}
|
|
|
|
// Update the position of our selection monocles
|
|
this._updateSelectionUI("start", 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;
|
|
}
|
|
|
|
// Only use selectAtPoint for editable content and avoid that for inputs,
|
|
// as we can expand caret to selection manually more precisely. We can use
|
|
// selectAtPoint for inputs too though, but only once bug 881938 is fully
|
|
// resolved.
|
|
if(Util.isEditableContent(this._targetElement)) {
|
|
// 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;
|
|
}
|
|
} else if (this._targetElement.selectionStart == 0 || aMarker == "end") {
|
|
// Expand caret forward or backward depending on direction
|
|
this._targetElement.selectionEnd++;
|
|
} else {
|
|
this._targetElement.selectionStart--;
|
|
}
|
|
|
|
// We bail if things get out of sync here implying we missed a message.
|
|
this._selectionMoveActive = true;
|
|
|
|
// Update the position of the selection marker that is *not*
|
|
// being dragged.
|
|
this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
|
|
},
|
|
|
|
/*
|
|
* Selection monocle start move event handler
|
|
*/
|
|
_onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
|
|
if (!this._contentWindow) {
|
|
this._onFail("_onSelectionMoveStart was called without proper view set up");
|
|
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._contentWindow) {
|
|
this._onFail("_onSelectionMove was called without proper view set up");
|
|
return;
|
|
}
|
|
|
|
if (!this._selectionMoveActive) {
|
|
this._onFail("mouse isn't down for drag move?");
|
|
return;
|
|
}
|
|
|
|
this._handleSelectionPoint(aMsg, false);
|
|
},
|
|
|
|
/*
|
|
* Selection monocle move finished event handler
|
|
*/
|
|
_onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
|
|
if (!this._contentWindow) {
|
|
this._onFail("_onSelectionMove was called without proper view set up");
|
|
return;
|
|
}
|
|
|
|
if (!this._selectionMoveActive) {
|
|
this._onFail("mouse isn't down for drag move?");
|
|
return;
|
|
}
|
|
|
|
this._handleSelectionPoint(aMsg, true);
|
|
this._selectionMoveActive = false;
|
|
|
|
// _handleSelectionPoint may set a scroll timer, so this must
|
|
// be reset after the last call.
|
|
this._clearTimers();
|
|
|
|
// Update the position of our selection monocles
|
|
this._updateSelectionUI("end", true, true);
|
|
},
|
|
|
|
/*
|
|
* _onCaretAttach - called by SelectionHelperUI when the user taps in a
|
|
* form input. Initializes SelectionHandler, updates the location of the
|
|
* caret, and messages back with current monocle position information.
|
|
*
|
|
* @param aX, aY tap location in client coordinates.
|
|
*/
|
|
_onCaretAttach: function _onCaretAttach(aX, aY) {
|
|
// Init content window information
|
|
if (!this._initTargetInfo(aX, aY)) {
|
|
this._onFail("failed to get target information");
|
|
return;
|
|
}
|
|
|
|
// This should never happen, but we check to make sure
|
|
if (!this._targetIsEditable) {
|
|
this._onFail("Coordiates didn't find a text input element.");
|
|
return;
|
|
}
|
|
|
|
// Locate and sanity check the caret position
|
|
let selection = this._getSelection();
|
|
if (!selection || !selection.isCollapsed) {
|
|
this._onFail("No selection or selection is not collapsed.");
|
|
return;
|
|
}
|
|
|
|
// Update the position of our selection monocles
|
|
this._updateSelectionUI("caret", false, false, true);
|
|
},
|
|
|
|
/*
|
|
* Selection copy event handler
|
|
*
|
|
* Check to see if the incoming click was on our selection rect.
|
|
* if it was, copy to the clipboard. Incoming coordinates are
|
|
* content values.
|
|
*/
|
|
_onSelectionCopy: function _onSelectionCopy(aMsg) {
|
|
let tap = {
|
|
xPos: aMsg.xPos,
|
|
yPos: aMsg.yPos,
|
|
};
|
|
|
|
let tapInSelection = (tap.xPos > this._cache.selection.left &&
|
|
tap.xPos < this._cache.selection.right) &&
|
|
(tap.yPos > this._cache.selection.top &&
|
|
tap.yPos < this._cache.selection.bottom);
|
|
// Util.dumpLn(tapInSelection,
|
|
// tap.xPos, tap.yPos, "|", this._cache.selection.left,
|
|
// this._cache.selection.right, this._cache.selection.top,
|
|
// this._cache.selection.bottom);
|
|
let success = false;
|
|
let selectedText = this._getSelectedText();
|
|
if (tapInSelection && selectedText.length) {
|
|
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
|
|
.getService(Ci.nsIClipboardHelper);
|
|
clipboard.copyString(selectedText, this._contentWindow.document);
|
|
success = true;
|
|
}
|
|
sendSyncMessage("Content:SelectionCopied", { succeeded: success });
|
|
},
|
|
|
|
/*
|
|
* Selection close event handler
|
|
*
|
|
* @param aClearSelection requests that selection be cleared.
|
|
*/
|
|
_onSelectionClose: function _onSelectionClose(aClearSelection) {
|
|
if (aClearSelection) {
|
|
this._clearSelection();
|
|
}
|
|
this.closeSelection();
|
|
},
|
|
|
|
/*
|
|
* Called any time SelectionHelperUI would like us to
|
|
* recalculate the selection bounds.
|
|
*/
|
|
_onSelectionUpdate: function _onSelectionUpdate(aMsg) {
|
|
if (!this._contentWindow) {
|
|
this._onFail("_onSelectionUpdate was called without proper view set up");
|
|
return;
|
|
}
|
|
|
|
if (aMsg && aMsg.isInitiatedByAPZC) {
|
|
let {offset: offset} = Content.getCurrentWindowAndOffset(
|
|
this._targetCoordinates.x, this._targetCoordinates.y);
|
|
this._contentOffset = offset;
|
|
}
|
|
|
|
// Update the position of our selection monocles
|
|
this._updateSelectionUI("update", true, true);
|
|
},
|
|
|
|
/*
|
|
* 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();
|
|
},
|
|
|
|
/*
|
|
* _repositionInfoRequest - fired at us by ContentAreaObserver when the
|
|
* soft keyboard is being displayed. CAO wants to make a decision about
|
|
* whether the browser deck needs repositioning.
|
|
*/
|
|
_repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) {
|
|
let result = this._calcNewContentPosition(aJsonMsg.viewHeight);
|
|
|
|
// no repositioning needed
|
|
if (result == 0) {
|
|
this.sendAsync("Content:RepositionInfoResponse", { reposition: false });
|
|
return;
|
|
}
|
|
|
|
this.sendAsync("Content:RepositionInfoResponse", {
|
|
reposition: true,
|
|
raiseContent: result,
|
|
});
|
|
},
|
|
|
|
_onPing: function _onPing(aId) {
|
|
this.sendAsync("Content:SelectionHandlerPong", { id: aId });
|
|
},
|
|
|
|
onClickCoords: function (xPos, yPos) {
|
|
this.lastXPos = xPos;
|
|
this.lastYPos = yPos;
|
|
},
|
|
|
|
/*************************************************
|
|
* Selection helpers
|
|
*/
|
|
|
|
/*
|
|
* _clearSelection
|
|
*
|
|
* Clear existing selection if it exists and reset our internla state.
|
|
*/
|
|
_clearSelection: function _clearSelection() {
|
|
this._clearTimers();
|
|
if (this._contentWindow) {
|
|
let selection = this._getSelection();
|
|
if (selection)
|
|
selection.removeAllRanges();
|
|
} else {
|
|
let selection = content.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._contentOffset = null;
|
|
this._domWinUtils = null;
|
|
this._targetIsEditable = false;
|
|
this._targetCoordinates = null;
|
|
sendSyncMessage("Content:HandlerShutdown", {});
|
|
},
|
|
|
|
/*
|
|
* Find content within frames - cache the target nsIDOMWindow,
|
|
* client coordinate offset, target element, and dom utils interface.
|
|
*/
|
|
_initTargetInfo: function _initTargetInfo(aX, aY) {
|
|
// getCurrentWindowAndOffset takes client coordinates
|
|
let { element: element,
|
|
contentWindow: contentWindow,
|
|
offset: offset,
|
|
utils: utils } =
|
|
Content.getCurrentWindowAndOffset(aX, aY);
|
|
if (!contentWindow) {
|
|
return false;
|
|
}
|
|
this._targetElement = element;
|
|
this._contentWindow = contentWindow;
|
|
this._contentOffset = offset;
|
|
this._domWinUtils = utils;
|
|
this._targetIsEditable = Util.isEditable(this._targetElement);
|
|
this._targetCoordinates = {
|
|
x: aX,
|
|
y: aY
|
|
};
|
|
|
|
return true;
|
|
},
|
|
|
|
/*
|
|
* _calcNewContentPosition - calculates the distance the browser should be
|
|
* raised to move the focused form input out of the way of the soft
|
|
* keyboard.
|
|
*
|
|
* @param aNewViewHeight the new content view height
|
|
* @return 0 if no positioning is required or a positive val equal to the
|
|
* distance content should be raised to center the target element.
|
|
*/
|
|
_calcNewContentPosition: function _calcNewContentPosition(aNewViewHeight) {
|
|
// We have no target element but the keyboard is up
|
|
// so lets not cover content that is below the keyboard
|
|
if (!this._cache || !this._cache.element) {
|
|
if (this.lastYPos != null && this.lastYPos > aNewViewHeight) {
|
|
return Services.metro.keyboardHeight;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
let position = Util.centerElementInView(aNewViewHeight, this._cache.element);
|
|
if (position !== undefined) {
|
|
return position;
|
|
}
|
|
|
|
// Special case: we are dealing with an input that is taller than the
|
|
// desired height of content. We need to center on the caret location.
|
|
let rect =
|
|
this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_CARET_RECT,
|
|
this._targetElement.selectionEnd,
|
|
0, 0, 0);
|
|
if (!rect || !rect.succeeded) {
|
|
Util.dumpLn("no caret was present, unexpected.");
|
|
return 0;
|
|
}
|
|
|
|
// Note sendQueryContentEvent with QUERY_CARET_RECT is really buggy. If it
|
|
// can't find the exact location of the caret position it will "guess".
|
|
// Sometimes this can put the result in unexpected locations.
|
|
let caretLocation = Math.max(Math.min(Math.round(rect.top + (rect.height * .5)),
|
|
viewBottom), 0);
|
|
|
|
// Caret is above the bottom of the new view bounds, no need to shift.
|
|
if (caretLocation <= aNewViewHeight) {
|
|
return 0;
|
|
}
|
|
|
|
// distance from the top of the keyboard down to the caret location
|
|
return caretLocation - aNewViewHeight;
|
|
},
|
|
|
|
/*************************************************
|
|
* Events
|
|
*/
|
|
|
|
/*
|
|
* Scroll + selection advancement timer when the monocle is
|
|
* outside the bounds of an input control.
|
|
*/
|
|
scrollTimerCallback: function scrollTimerCallback() {
|
|
let result = SelectionHandler.updateTextEditSelection();
|
|
// Update monocle position and speed if we've dragged off to one side
|
|
if (result.trigger) {
|
|
SelectionHandler._updateSelectionUI("update", result.start, result.end);
|
|
}
|
|
},
|
|
|
|
receiveMessage: function sh_receiveMessage(aMessage) {
|
|
if (this._debugEvents && aMessage.name != "Browser:SelectionMove") {
|
|
Util.dumpLn("SelectionHandler:", aMessage.name);
|
|
}
|
|
let json = aMessage.json;
|
|
switch (aMessage.name) {
|
|
case "Browser:SelectionStart":
|
|
this._onSelectionStart(json);
|
|
break;
|
|
|
|
case "Browser:SelectionAttach":
|
|
this._onSelectionAttach(json.xPos, json.yPos);
|
|
break;
|
|
|
|
case "Browser:CaretAttach":
|
|
this._onCaretAttach(json.xPos, json.yPos);
|
|
break;
|
|
|
|
case "Browser:CaretMove":
|
|
this._onCaretMove(json.caret.xPos, json.caret.yPos);
|
|
break;
|
|
|
|
case "Browser:CaretUpdate":
|
|
this._onCaretPositionUpdate(json.caret.xPos, json.caret.yPos);
|
|
break;
|
|
|
|
case "Browser:SelectionSwitchMode":
|
|
this._onSwitchMode(json.newMode, json.change, json.xPos, json.yPos);
|
|
break;
|
|
|
|
case "Browser:SelectionClose":
|
|
this._onSelectionClose(json.clearSelection);
|
|
break;
|
|
|
|
case "Browser:SelectionMoveStart":
|
|
this._onSelectionMoveStart(json);
|
|
break;
|
|
|
|
case "Browser:SelectionMove":
|
|
this._onSelectionMove(json);
|
|
break;
|
|
|
|
case "Browser:SelectionMoveEnd":
|
|
this._onSelectionMoveEnd(json);
|
|
break;
|
|
|
|
case "Browser:SelectionCopy":
|
|
this._onSelectionCopy(json);
|
|
break;
|
|
|
|
case "Browser:SelectionDebug":
|
|
this._onSelectionDebug(json);
|
|
break;
|
|
|
|
case "Browser:SelectionUpdate":
|
|
this._onSelectionUpdate(json);
|
|
break;
|
|
|
|
case "Browser:RepositionInfoRequest":
|
|
// This message is sent simultaneously with a tap event.
|
|
// Wait a bit to make sure we have the most up-to-date tap co-ordinates
|
|
// before a call to _calcNewContentPosition() which accesses them.
|
|
content.setTimeout (function () {
|
|
SelectionHandler._repositionInfoRequest(json);
|
|
}, 50);
|
|
break;
|
|
|
|
case "Browser:SelectionHandlerPing":
|
|
this._onPing(json.id);
|
|
break;
|
|
|
|
case "Browser:ResetLastPos":
|
|
this.onClickCoords(json.xPos, json.yPos);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/*************************************************
|
|
* Utilities
|
|
*/
|
|
|
|
_getDocShell: function _getDocShell(aWindow) {
|
|
if (aWindow == null)
|
|
return null;
|
|
return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell);
|
|
},
|
|
|
|
_getSelectedText: function _getSelectedText() {
|
|
let selection = this._getSelection();
|
|
if (selection)
|
|
return selection.toString();
|
|
return "";
|
|
},
|
|
|
|
_getSelection: function _getSelection() {
|
|
if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
|
|
return this._targetElement
|
|
.QueryInterface(Ci.nsIDOMNSEditableElement)
|
|
.editor.selection;
|
|
} else if (this._contentWindow)
|
|
return this._contentWindow.getSelection();
|
|
return null;
|
|
},
|
|
|
|
_getSelectController: function _getSelectController() {
|
|
if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
|
|
return this._targetElement
|
|
.QueryInterface(Ci.nsIDOMNSEditableElement)
|
|
.editor.selectionController;
|
|
} else {
|
|
let docShell = this._getDocShell(this._contentWindow);
|
|
if (docShell == null)
|
|
return null;
|
|
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsISelectionDisplay)
|
|
.QueryInterface(Ci.nsISelectionController);
|
|
}
|
|
},
|
|
};
|
|
this.SelectionHandler = SelectionHandler;
|
|
|
|
SelectionHandler.__proto__ = new SelectionPrototype();
|
|
SelectionHandler.init();
|
|
|