blob: aa45502bcf0d7023551ce6ac9ba83fdecc0e06f0 [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package org.mozc.android.inputmethod.japanese.keyboard;
import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchAction;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchPosition;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import java.util.Set;
/**
* This class represents user's one action, e.g., the sequence of:
* press -> move -> move -> ... -> move -> release.
*
* For user's multi touch events, multiple instances will be instantiated.
* E.g. for user's two finger strokes, two instances will be instantiated.
*
*/
public class KeyEventContext {
final Key key;
final int pointerId;
private final float pressedX;
private final float pressedY;
private final float flickThresholdSquared;
private final boolean isFlickableKey;
private final Optional<KeyState> keyState;
Flick.Direction flickDirection = Flick.Direction.CENTER;
// TODO(hidehiko): Move logging code to an upper layer, e.g., MozcService or ViewManager etc.
// after refactoring the architecture.
private Optional<TouchAction> lastAction = Optional.absent();
private float lastX;
private float lastY;
private long lastTimestamp;
private final int keyboardWidth;
private final int keyboardHeight;
// This variable will be updated in the callback of long press key event (if necessary).
boolean pastLongPressSentTimeout = false;
Optional<Runnable> longPressCallback = Optional.absent();
public KeyEventContext(Key key, int pointerId, float pressedX, float pressedY,
int keyboardWidth, int keyboardHeight,
float flickThresholdSquared, Set<MetaState> metaState) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(metaState);
this.key = key;
this.pressedX = pressedX;
this.pressedY = pressedY;
this.flickThresholdSquared = flickThresholdSquared;
this.isFlickableKey = isFlickable(key, metaState);
this.keyState = key.getKeyState(metaState);
this.pointerId = pointerId;
this.keyboardWidth = keyboardWidth;
this.keyboardHeight = keyboardHeight;
}
float getFlickThresholdSquared() {
return flickThresholdSquared;
}
/**
* Returns true iff the point ({@code x}, {@code y}) is contained by the {@code key}'s region.
*/
@VisibleForTesting static boolean isContained(float x, float y, Key key) {
float relativeX = x - key.getX();
float relativeY = y - key.getY();
return 0 <= relativeX && relativeX < key.getWidth() &&
0 <= relativeY && relativeY < key.getHeight();
}
/**
* Returns true iff the key is flickable. Otherwise returns false.
*/
@VisibleForTesting
static boolean isFlickable(Key key, Set<MetaState> metaState) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(metaState);
Optional<KeyState> optionalKeyState = key.getKeyState(metaState);
if (!optionalKeyState.isPresent()) {
return false;
}
KeyState keyState = optionalKeyState.get();
return keyState.getFlick(Flick.Direction.LEFT).isPresent() ||
keyState.getFlick(Flick.Direction.UP).isPresent() ||
keyState.getFlick(Flick.Direction.RIGHT).isPresent() ||
keyState.getFlick(Flick.Direction.DOWN).isPresent();
}
/**
* Returns the key entity corresponding to {@code metaState} and {@code direction}.
*/
public static Optional<KeyEntity> getKeyEntity(Key key, Set<MetaState> metaState,
Optional<Flick.Direction> direction) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(metaState);
Preconditions.checkNotNull(direction);
if (key.isSpacer()) {
return Optional.absent();
}
// Key is not spacer for at least one KeyState is available.
return getKeyEntityInternal(key.getKeyState(metaState).get(), direction);
}
private Optional<KeyEntity> getKeyEntity(Flick.Direction direction) {
return keyState.isPresent()
? getKeyEntityInternal(keyState.get(), Optional.of(direction))
: Optional.<KeyEntity>absent();
}
private static Optional<KeyEntity> getKeyEntityInternal(KeyState keyState,
Optional<Flick.Direction> direction) {
Preconditions.checkNotNull(keyState);
Preconditions.checkNotNull(direction);
if (!direction.isPresent()) {
return Optional.absent();
}
Optional<Flick> flick = keyState.getFlick(direction.get());
return flick.isPresent()
? Optional.of(flick.get().getKeyEntity())
: Optional.<KeyEntity>absent();
}
/**
* Returns the key code to be sent via {@link KeyboardActionListener#onKey(int, java.util.List)}.
* <p>
* If {@code keyEntyty} doesn't trigger longpress by timeout (isLongPressTimeoutTrigger is false),
* the result depends on the timestamp of touch-down event.
*/
public int getKeyCode() {
Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
if (!keyEntity.isPresent()
|| (pastLongPressSentTimeout && keyEntity.get().isLongPressTimeoutTrigger())) {
// If the long-press-key event is already sent, just return INVALID_KEY_CODE.
return KeyEntity.INVALID_KEY_CODE;
}
return !keyEntity.get().isLongPressTimeoutTrigger()
&& keyEntity.get().getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE
&& pastLongPressSentTimeout
? keyEntity.get().getLongPressKeyCode() : keyEntity.get().getKeyCode();
}
Set<MetaState> getNextMetaStates(Set<MetaState> originalMetaStates) {
Preconditions.checkNotNull(originalMetaStates);
if (!key.isModifier() || key.isSpacer()) {
// Non-modifier key shouldn't change meta state.
return originalMetaStates;
}
return keyState.get().getNextMetaStates(originalMetaStates);
}
/**
* Returns the key code to be sent for long press event.
*/
int getLongPressKeyCode() {
if (pastLongPressSentTimeout) {
// If the long-press-key event is already sent, just return INVALID_KEY_CODE.
return KeyEntity.INVALID_KEY_CODE;
}
// Note that we always use CENTER flick direction for long press key events.
Optional<KeyEntity> keyEntity = getKeyEntity(Flick.Direction.CENTER);
return keyEntity.isPresent()
? keyEntity.get().getLongPressKeyCode()
: KeyEntity.INVALID_KEY_CODE;
}
boolean isLongPressTimeoutTrigger() {
Optional<KeyEntity> keyEntity = getKeyEntity(Flick.Direction.CENTER);
return !keyEntity.isPresent() || keyEntity.get().isLongPressTimeoutTrigger();
}
/**
* Returns the key code to be send via {@link KeyboardActionListener#onPress(int)} and
* {@link KeyboardActionListener#onRelease(int)}.
*/
public int getPressedKeyCode() {
Optional<KeyEntity> keyEntity = getKeyEntity(Flick.Direction.CENTER);
return keyEntity.isPresent()
? keyEntity.get().getKeyCode()
: KeyEntity.INVALID_KEY_CODE;
}
/**
* Returns true if this key event sequence represents toggling meta state.
*/
boolean isMetaStateToggleEvent() {
return !pastLongPressSentTimeout && key.isModifier()
&& flickDirection == Flick.Direction.CENTER;
}
/**
* Returns the pop up data for the current state.
*/
Optional<PopUp> getCurrentPopUp() {
if (pastLongPressSentTimeout) {
return Optional.absent();
}
Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
return keyEntity.isPresent() ? keyEntity.get().getPopUp() : Optional.<PopUp>absent();
}
/**
* Updates the internal state of this context when the touched position is moved to
* {@code (x, y)} at time {@code timestamp} in milliseconds since the press.
* @return {@code true} if the internal state is actually updated.
*/
public boolean update(float x, float y, TouchAction touchAction, long timestamp) {
lastAction = Optional.of(touchAction);
Flick.Direction originalDirection = flickDirection;
lastX = x;
lastY = y;
lastTimestamp = timestamp;
float deltaX = x - pressedX;
float deltaY = y - pressedY;
if (deltaX * deltaX + deltaY * deltaY < flickThresholdSquared ||
!isFlickableKey && isContained(x, y, key)) {
// A user touches (or returns back to) the same key, so we don't fire flick.
// If the key isn't flickable, we also look at the key's region to avoid unexpected
// cancellation.
flickDirection = Flick.Direction.CENTER;
} else {
if (Math.abs(deltaX) < Math.abs(deltaY)) {
// Vertical flick
flickDirection = deltaY < 0 ? Flick.Direction.UP : Flick.Direction.DOWN;
} else {
// Horizontal flick
flickDirection = deltaX > 0 ? Flick.Direction.RIGHT : Flick.Direction.LEFT;
}
}
if (flickDirection == originalDirection) {
return false;
} else {
// If flickDirection has been updated, reset pastLongPressSentTimeout flag
// so that long-press even can be sent again.
// This happens when
// [Hold 'q' key]
// -> [Popup '1' is shown as the result of long-press]
// -> [Flick outside to dismiss the popup]
// -> [Flick again to the center position and hold]
// -> [Popup '1' is shown again as the result of long-press]
pastLongPressSentTimeout = false;
return true;
}
}
/**
* @return {@code TouchEvent} instance which includes the stroke related to this context.
*/
public Optional<TouchEvent> getTouchEvent() {
Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
if (!keyEntity.isPresent()) {
return Optional.absent();
}
TouchEvent.Builder builder = TouchEvent.newBuilder()
.setSourceId(keyEntity.get().getSourceId());
builder.addStroke(createTouchPosition(
TouchAction.TOUCH_DOWN, pressedX, pressedY, keyboardWidth, keyboardHeight, 0));
if (lastAction.isPresent()) {
builder.addStroke(createTouchPosition(
lastAction.get(), lastX, lastY, keyboardWidth, keyboardHeight, lastTimestamp));
}
return Optional.of(builder.build());
}
public static TouchPosition createTouchPosition(
TouchAction action, float x, float y, int width, int height, long timestamp) {
return TouchPosition.newBuilder()
.setAction(action)
.setX(x / width)
.setY(y / height)
.setTimestamp(timestamp)
.build();
}
public void setLongPressCallback(Runnable longPressCallback) {
this.longPressCallback = Optional.of(longPressCallback);
}
}