Bug 652168: Add thumb for input box. [r=mfinkle]

--HG--
extra : rebase_source : 9cd553fedb4e58a6dee437ed0360b6ba5df56c54
This commit is contained in:
Sriram Ramasubramanian 2012-09-21 10:56:41 -07:00
parent 3cc53fe881
commit cdf1677e14
10 changed files with 241 additions and 94 deletions

View File

@ -1679,6 +1679,7 @@ abstract public class GeckoApp
mPromptService = new PromptService();
mTextSelection = new TextSelection((TextSelectionHandle) findViewById(R.id.start_handle),
(TextSelectionHandle) findViewById(R.id.middle_handle),
(TextSelectionHandle) findViewById(R.id.end_handle),
GeckoAppShell.getEventDispatcher());

View File

@ -505,6 +505,7 @@ RES_DRAWABLE_BASE = \
res/drawable/bookmarkdefaults_favicon_support.png \
res/drawable/bookmarkdefaults_favicon_addons.png \
res/drawable/handle_end.png \
res/drawable/handle_middle.png \
res/drawable/handle_start.png \
$(addprefix res/drawable-mdpi/,$(notdir $(SYNC_RES_DRAWABLE_MDPI))) \
$(NULL)
@ -571,6 +572,7 @@ RES_DRAWABLE_HDPI = \
res/drawable-hdpi/validation_arrow_inverted.png \
res/drawable-hdpi/validation_bg.9.png \
res/drawable-hdpi/handle_end.png \
res/drawable-hdpi/handle_middle.png \
res/drawable-hdpi/handle_start.png \
$(addprefix res/drawable-hdpi/,$(notdir $(SYNC_RES_DRAWABLE_HDPI))) \
$(NULL)
@ -631,6 +633,7 @@ RES_DRAWABLE_XHDPI = \
res/drawable-xhdpi/validation_arrow_inverted.png \
res/drawable-xhdpi/validation_bg.9.png \
res/drawable-xhdpi/handle_end.png \
res/drawable-xhdpi/handle_middle.png \
res/drawable-xhdpi/handle_start.png \
$(NULL)

View File

@ -11,6 +11,7 @@ import org.mozilla.gecko.util.EventDispatcher;
import org.mozilla.gecko.util.FloatUtils;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONArray;
import org.json.JSONObject;
import android.util.Log;
@ -20,6 +21,7 @@ class TextSelection extends Layer implements GeckoEventListener {
private static final String LOGTAG = "GeckoTextSelection";
private final TextSelectionHandle mStartHandle;
private final TextSelectionHandle mMiddleHandle;
private final TextSelectionHandle mEndHandle;
private final EventDispatcher mEventDispatcher;
@ -27,14 +29,17 @@ class TextSelection extends Layer implements GeckoEventListener {
private float mViewTop;
private float mViewZoom;
TextSelection(TextSelectionHandle startHandle, TextSelectionHandle endHandle,
TextSelection(TextSelectionHandle startHandle,
TextSelectionHandle middleHandle,
TextSelectionHandle endHandle,
EventDispatcher eventDispatcher) {
mStartHandle = startHandle;
mMiddleHandle = middleHandle;
mEndHandle = endHandle;
mEventDispatcher = eventDispatcher;
// Only register listeners if we have valid start/end handles
if (mStartHandle == null || mEndHandle == null) {
// Only register listeners if we have valid start/middle/end handles
if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) {
Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
} else {
registerEventListener("TextSelection:ShowHandles");
@ -52,42 +57,73 @@ class TextSelection extends Layer implements GeckoEventListener {
public void handleMessage(String event, JSONObject message) {
try {
if (event.equals("TextSelection:ShowHandles")) {
final JSONArray handles = message.getJSONArray("handles");
GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
public void run() {
mStartHandle.setVisibility(View.VISIBLE);
mEndHandle.setVisibility(View.VISIBLE);
try {
for (int i=0; i < handles.length(); i++) {
String handle = handles.getString(i);
mViewLeft = 0.0f;
mViewTop = 0.0f;
mViewZoom = 0.0f;
LayerView layerView = GeckoApp.mAppContext.getLayerView();
if (layerView != null) {
layerView.addLayer(TextSelection.this);
}
if (handle.equals("START"))
mStartHandle.setVisibility(View.VISIBLE);
else if (handle.equals("MIDDLE"))
mMiddleHandle.setVisibility(View.VISIBLE);
else
mEndHandle.setVisibility(View.VISIBLE);
}
mViewLeft = 0.0f;
mViewTop = 0.0f;
mViewZoom = 0.0f;
LayerView layerView = GeckoApp.mAppContext.getLayerView();
if (layerView != null) {
layerView.addLayer(TextSelection.this);
}
} catch(Exception e) {}
}
});
} else if (event.equals("TextSelection:HideHandles")) {
final JSONArray handles = message.getJSONArray("handles");
GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
public void run() {
LayerView layerView = GeckoApp.mAppContext.getLayerView();
if (layerView != null) {
layerView.removeLayer(TextSelection.this);
}
try {
LayerView layerView = GeckoApp.mAppContext.getLayerView();
if (layerView != null) {
layerView.removeLayer(TextSelection.this);
}
mStartHandle.setVisibility(View.GONE);
mEndHandle.setVisibility(View.GONE);
for (int i=0; i < handles.length(); i++) {
String handle = handles.getString(i);
if (handle.equals("START"))
mStartHandle.setVisibility(View.GONE);
else if (handle.equals("MIDDLE"))
mMiddleHandle.setVisibility(View.GONE);
else
mEndHandle.setVisibility(View.GONE);
}
} catch(Exception e) {}
}
});
} else if (event.equals("TextSelection:PositionHandles")) {
final int startLeft = message.getInt("startLeft");
final int startTop = message.getInt("startTop");
final int endLeft = message.getInt("endLeft");
final int endTop = message.getInt("endTop");
final JSONArray positions = message.getJSONArray("positions");
GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
public void run() {
mStartHandle.positionFromGecko(startLeft, startTop);
mEndHandle.positionFromGecko(endLeft, endTop);
try {
for (int i=0; i < positions.length(); i++) {
JSONObject position = positions.getJSONObject(i);
String handle = position.getString("handle");
int left = position.getInt("left");
int top = position.getInt("top");
if (handle.equals("START"))
mStartHandle.positionFromGecko(left, top);
else if (handle.equals("MIDDLE"))
mMiddleHandle.positionFromGecko(left, top);
else
mEndHandle.positionFromGecko(left, top);
}
} catch (Exception e) { }
}
});
}
@ -113,6 +149,7 @@ class TextSelection extends Layer implements GeckoEventListener {
GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
public void run() {
mStartHandle.repositionWithViewport(context.viewport.left, context.viewport.top, context.zoomFactor);
mMiddleHandle.repositionWithViewport(context.viewport.left, context.viewport.top, context.zoomFactor);
mEndHandle.repositionWithViewport(context.viewport.left, context.viewport.top, context.zoomFactor);
}
});

View File

@ -22,7 +22,7 @@ import android.widget.RelativeLayout;
class TextSelectionHandle extends ImageView implements View.OnTouchListener {
private static final String LOGTAG = "GeckoTextSelectionHandle";
private enum HandleType { START, END };
private enum HandleType { START, MIDDLE, END };
private final HandleType mHandleType;
private final int mWidth;
@ -42,8 +42,16 @@ class TextSelectionHandle extends ImageView implements View.OnTouchListener {
setOnTouchListener(this);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextSelectionHandle);
String handleType = a.getString(R.styleable.TextSelectionHandle_handleType);
mHandleType = handleType.equals("START") ? HandleType.START : HandleType.END;
int handleType = a.getInt(R.styleable.TextSelectionHandle_handleType, 0x01);
if (handleType == 0x01)
mHandleType = HandleType.START;
else if (handleType == 0x02)
mHandleType = HandleType.MIDDLE;
else
mHandleType = HandleType.END;
mGeckoPoint = new PointF(0.0f, 0.0f);
mWidth = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_width);
mHeight = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_height);
@ -89,7 +97,14 @@ class TextSelectionHandle extends ImageView implements View.OnTouchListener {
return;
}
// Send x coordinate on the right side of the start handle, left side of the end handle.
float left = (float) mLeft + (mHandleType.equals(HandleType.START) ? mWidth - mShadow : mShadow);
float left = (float) mLeft;
if (mHandleType.equals(HandleType.START))
left += mWidth - mShadow;
else if (mHandleType.equals(HandleType.MIDDLE))
left += (float) ((mWidth - mShadow) / 2);
else
left += mShadow;
PointF geckoPoint = new PointF(left, (float) mTop);
geckoPoint = layerView.convertViewPointToLayerPoint(geckoPoint);
@ -122,7 +137,14 @@ class TextSelectionHandle extends ImageView implements View.OnTouchListener {
PointF viewPoint = new PointF((mGeckoPoint.x * zoom) - x,
(mGeckoPoint.y * zoom) - y);
mLeft = Math.round(viewPoint.x) - (mHandleType.equals(HandleType.START) ? mWidth - mShadow : mShadow);
mLeft = Math.round(viewPoint.x);
if (mHandleType.equals(HandleType.START))
mLeft -= mWidth - mShadow;
else if (mHandleType.equals(HandleType.MIDDLE))
mLeft -= (float) ((mWidth - mShadow) / 2);
else
mLeft -= mShadow;
mTop = Math.round(viewPoint.y);
setLayoutPosition();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -12,12 +12,19 @@
android:layout_height="@dimen/text_selection_handle_height"
android:src="@drawable/handle_start"
android:visibility="gone"
gecko:handleType="START"/>
gecko:handleType="start"/>
<org.mozilla.gecko.TextSelectionHandle android:id="@+id/middle_handle"
android:layout_width="@dimen/text_selection_handle_width"
android:layout_height="@dimen/text_selection_handle_height"
android:src="@drawable/handle_middle"
android:visibility="gone"
gecko:handleType="middle"/>
<org.mozilla.gecko.TextSelectionHandle android:id="@+id/end_handle"
android:layout_width="@dimen/text_selection_handle_width"
android:layout_height="@dimen/text_selection_handle_height"
android:src="@drawable/handle_end"
android:visibility="gone"
gecko:handleType="END"/>
gecko:handleType="end"/>
</merge>

View File

@ -50,7 +50,11 @@
</declare-styleable>
<declare-styleable name="TextSelectionHandle">
<attr name="handleType" format="string"/>
<attr name="handleType">
<flag name="start" value="0x01" />
<flag name="middle" value="0x02" />
<flag name="end" value="0x03" />
</attr>
</declare-styleable>
</resources>

View File

@ -1629,12 +1629,17 @@ var NativeWindow = {
var SelectionHandler = {
HANDLE_TYPE_START: "START",
HANDLE_TYPE_MIDDLE: "MIDDLE",
HANDLE_TYPE_END: "END",
TYPE_NONE: 0,
TYPE_CURSOR: 1,
TYPE_SELECTION: 2,
// Keeps track of data about the dimensions of the selection. Coordinates
// stored here are relative to the _view window.
cache: null,
_active: false,
_activeType: this.TYPE_NONE,
// The window that holds the selection (can be a sub-frame)
get _view() {
@ -1684,62 +1689,78 @@ var SelectionHandler = {
},
observe: function sh_observe(aSubject, aTopic, aData) {
if (!this._active)
return;
switch (aTopic) {
case "Gesture:SingleTap": {
let data = JSON.parse(aData);
this.endSelection(data.x, data.y);
if (this._activeType == this.TYPE_SELECTION) {
let data = JSON.parse(aData);
this.endSelection(data.x, data.y);
}
break;
}
case "Tab:Selected":
case "Window:Resize": {
// Knowing when the page is done drawing is hard, so let's just cancel
// the selection when the window changes. We should fix this later.
this.endSelection();
if (this._activeType == this.TYPE_SELECTION) {
// Knowing when the page is done drawing is hard, so let's just cancel
// the selection when the window changes. We should fix this later.
this.endSelection();
}
break;
}
case "after-viewport-change": {
// Update the cache after the viewport changes (e.g. panning, zooming).
this.updateCacheForSelection();
if (this._activeType == this.TYPE_SELECTION) {
// Update the cache after the viewport changes (e.g. panning, zooming).
this.updateCacheForSelection();
}
break;
}
case "TextSelection:Move": {
let data = JSON.parse(aData);
this.moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
if (this._activeType == this.TYPE_SELECTION)
this.moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
else if (this._activeType == this.TYPE_CURSOR)
this._sendMouseEvents(data.x, data.y);
break;
}
case "TextSelection:Position": {
let data = JSON.parse(aData);
if (this._activeType == this.TYPE_SELECTION) {
let data = JSON.parse(aData);
// Reverse the handles if necessary.
let selectionReversed = this.updateCacheForSelection(data.handleType == this.HANDLE_TYPE_START);
if (selectionReversed) {
// Re-send mouse events to update the selection corresponding to the new handles.
if (this._isRTL) {
this._sendMouseEvents(this.cache.end.x, this.cache.end.y, false);
this._sendMouseEvents(this.cache.start.x, this.cache.start.y, true);
} else {
this._sendMouseEvents(this.cache.start.x, this.cache.start.y, false);
this._sendMouseEvents(this.cache.end.x, this.cache.end.y, true);
// Reverse the handles if necessary.
let selectionReversed = this.updateCacheForSelection(data.handleType == this.HANDLE_TYPE_START);
if (selectionReversed) {
// Re-send mouse events to update the selection corresponding to the new handles.
if (this._isRTL) {
this._sendMouseEvents(this.cache.end.x, this.cache.end.y, false);
this._sendMouseEvents(this.cache.start.x, this.cache.start.y, true);
} else {
this._sendMouseEvents(this.cache.start.x, this.cache.start.y, false);
this._sendMouseEvents(this.cache.end.x, this.cache.end.y, true);
}
}
}
// Position the handles to align with the edges of the selection.
this.positionHandles();
// Position the handles to align with the edges of the selection.
this.positionHandles();
} else if (this._activeType == this.TYPE_CURSOR) {
this.positionHandles();
}
break;
}
}
},
handleEvent: function sh_handleEvent(aEvent) {
if (!this._active)
return;
switch (aEvent.type) {
case "pagehide":
this.endSelection();
if (this._activeType == this.TYPE_SELECTION)
this.endSelection();
else
this.hideThumb();
break;
case "keydown":
case "blur":
if (this._activeType == this.TYPE_CURSOR)
this.hideThumb();
break;
}
},
@ -1777,8 +1798,12 @@ var SelectionHandler = {
// aX/aY are in top-level window browser coordinates
startSelection: function sh_startSelection(aElement, aX, aY) {
// Clear out any existing selection
if (this._active)
if (this._activeType == this.TYPE_SELECTION) {
this.endSelection();
} else if (this._activeType == this.TYPE_CURSOR) {
// Hide the cursor handles.
this.hideThumb();
}
// Get the element's view
this._view = aElement.ownerDocument.defaultView;
@ -1827,8 +1852,15 @@ var SelectionHandler = {
this.cache = { start: {}, end: {}};
this.updateCacheForSelection();
this.showHandles();
this._active = true;
this._activeType = this.TYPE_SELECTION;
this.positionHandles();
sendMessageToJava({
gecko: {
type: "TextSelection:ShowHandles",
handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
}
});
if (aElement instanceof Ci.nsIDOMNSEditableElement)
aElement.focus();
@ -1854,11 +1886,11 @@ var SelectionHandler = {
// Used by the contextmenu "matches" functions in ClipboardHelper
shouldShowContextMenu: function sh_shouldShowContextMenu(aX, aY) {
return this._active && this._pointInSelection(aX, aY);
return (this._activeType == this.TYPE_SELECTION) && this._pointInSelection(aX, aY);
},
selectAll: function sh_selectAll(aElement, aX, aY) {
if (!this._active)
if (this._activeType != this.TYPE_SELECTION)
this.startSelection(aElement, aX, aY);
let selectionController = this.getSelectionController();
@ -1906,11 +1938,17 @@ var SelectionHandler = {
// aX/aY are in top-level window browser coordinates
endSelection: function sh_endSelection(aX, aY) {
if (!this._active)
if (this._activeType != this.TYPE_SELECTION)
return "";
this._activeType = this.TYPE_NONE;
sendMessageToJava({
gecko: {
type: "TextSelection:HideHandles",
handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
}
});
this._active = false;
this.hideHandles();
let selectedText = "";
let pointInSelection = false;
@ -1947,6 +1985,9 @@ var SelectionHandler = {
_cleanUp: function sh_cleanUp() {
this._view.removeEventListener("pagehide", this, false);
this._view.removeEventListener("keydown", this, false);
this._view.removeEventListener("blur", this, true);
this._activeType = this.TYPE_NONE;
this._view = null;
this._target = null;
this._isRTL = false;
@ -2000,6 +2041,41 @@ var SelectionHandler = {
return selectionReversed;
},
showThumb: function sh_showThumb(aElement) {
if (!aElement)
return;
// Get the element's view
this._view = aElement.ownerDocument.defaultView;
this._target = aElement;
this._view.addEventListener("pagehide", this, false);
this._view.addEventListener("keydown", this, false);
this._view.addEventListener("blur", this, true);
this._activeType = this.TYPE_CURSOR;
this.positionHandles();
sendMessageToJava({
gecko: {
type: "TextSelection:ShowHandles",
handles: [this.HANDLE_TYPE_MIDDLE]
}
});
},
hideThumb: function sh_hideThumb() {
this._activeType = this.TYPE_NONE;
this._cleanUp();
sendMessageToJava({
gecko: {
type: "TextSelection:HideHandles",
handles: [this.HANDLE_TYPE_MIDDLE]
}
});
},
positionHandles: function sh_positionHandles() {
// Translate coordinates to account for selections in sub-frames. We can't cache
// this because the top-level page may have scrolled since selection started.
@ -2007,37 +2083,30 @@ var SelectionHandler = {
let scrollX = {}, scrollY = {};
this._view.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).getScrollXY(false, scrollX, scrollY);
let positions = null;
if (this._activeType == this.TYPE_CURSOR) {
let cursor = this._cwu.sendQueryContentEvent(this._cwu.QUERY_CARET_RECT, this._target.selectionEnd, 0, 0, 0);
positions = [ { handle: this.HANDLE_TYPE_MIDDLE,
left: cursor.left + offset.x + scrollX.value,
top: cursor.top + cursor.height + offset.y + scrollY.value } ];
} else {
positions = [ { handle: this.HANDLE_TYPE_START,
left: this.cache.start.x + offset.x + scrollX.value,
top: this.cache.start.y + offset.y + scrollY.value },
{ handle: this.HANDLE_TYPE_END,
left: this.cache.end.x + offset.x + scrollX.value,
top: this.cache.end.y + offset.y + scrollY.value } ];
}
sendMessageToJava({
gecko: {
type: "TextSelection:PositionHandles",
startLeft: this.cache.start.x + offset.x + scrollX.value,
startTop: this.cache.start.y + offset.y + scrollY.value,
endLeft: this.cache.end.x + offset.x + scrollX.value,
endTop: this.cache.end.y + offset.y + scrollY.value
}
});
},
showHandles: function sh_showHandles() {
this.positionHandles();
sendMessageToJava({
gecko: {
type: "TextSelection:ShowHandles"
}
});
},
hideHandles: function sh_hideHandles() {
sendMessageToJava({
gecko: {
type: "TextSelection:HideHandles"
positions: positions
}
});
}
};
var UserAgent = {
DESKTOP_UA: null,
@ -3607,6 +3676,10 @@ var BrowserEventHandler = {
this._sendMouseEvent("mousemove", element, data.x, data.y);
this._sendMouseEvent("mousedown", element, data.x, data.y);
this._sendMouseEvent("mouseup", element, data.x, data.y);
// See if its a input element
if ((element instanceof HTMLInputElement && element.mozIsTextField(false)) || (element instanceof HTMLTextAreaElement))
SelectionHandler.showThumb(element);
if (isClickable)
Haptic.performSimpleAction(Haptic.LongPress);