/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.waveprotocol.wave.client.editor.event;
import com.google.common.annotations.VisibleForTesting;
import org.waveprotocol.wave.client.editor.constants.BrowserEvents;
import org.waveprotocol.wave.client.scheduler.Scheduler;
import org.waveprotocol.wave.client.scheduler.TimerService;
import org.waveprotocol.wave.common.logging.LoggerBundle;
/**
* Logic dealing with the intricacies of composition events and
* abstracting the differences between browsers
*
* @param <V> Event type pass through
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class CompositionEventHandler<V> {
public interface CompositionListener<V> {
void compositionStart(V event);
void compositionUpdate();
void compositionEnd();
}
private final LoggerBundle logger;
private final boolean modifiesDomAndFiresTextInputAfterComposition;
private final TimerService timer;
private final CompositionListener<V> listener;
/**
* It might be worthwhile to increase this delay, because we get lots of 2nd
* composition cycles on linux, so it might be an improvement to bunch them up.
*/
final int compositionEndDelay = 0;
/**
* True if the "application" is composing (i.e. the state maintained by the
* listener). Compare with {@link #browserComposing}.
*/
private boolean appComposing = false;
/**
* True if we are in the timer delay after a textInput event (not after just a
* compositionend event)
*
* See notes inside {@link #compositionStart(Object)} for details about why we
* care.
*/
@VisibleForTesting boolean delayAfterTextInput = false;
/**
* True if we are between a compositionstart...compositionend event sequence.
* Not true if compositionend has already been received but we haven't notified
* the listener of the compositionEnd. Therefore this state does not exactly
* match state being maintained by the listener.
*
* Compare with {@link #appComposing}
*/
private boolean browserComposing = false;
private final Scheduler.Task endTask = new Scheduler.Task() {
public void execute() {
delayAfterTextInput = false;
if (browserComposing) {
return;
}
assert appComposing == true;
appComposing = false;
listener.compositionEnd();
}
};
/**
*
* @param modifiesDomAndFiresTextinputAfterComposition use
* QuirksConstants.MODIFIES_DOM_AND_FIRES_TEXTINPUT_AFTER_COMPOSITION
* @param timer
* @param listener
*/
public CompositionEventHandler(TimerService timer, CompositionListener<V> listener,
LoggerBundle logger, boolean modifiesDomAndFiresTextinputAfterComposition) {
this.timer = timer;
this.listener = listener;
this.logger = logger;
this.modifiesDomAndFiresTextInputAfterComposition =
modifiesDomAndFiresTextinputAfterComposition;
}
/**
* This method must be called when any "composition" event is received. A composition
* event is one of the following:
* - compositionstart
* - compositionupdate
* - compositionend
* - text
* - textinput
*
* @param event Event object for pass through purposes
* @param typeName Event name
* @return true if the event should have its default prevented, false otherwise.
*/
public boolean handleCompositionEvent(V event, String typeName) {
if (BrowserEvents.COMPOSITIONSTART.equals(typeName)) {
compositionStart(event);
} else if (
BrowserEvents.TEXT.equals(typeName) ||
BrowserEvents.COMPOSITIONUPDATE.equals(typeName)) {
compositionUpdate();
} else if (BrowserEvents.COMPOSITIONEND.equals(typeName)){
compositionEnd();
} else if (BrowserEvents.TEXTINPUT.equals(typeName)) {
textInput();
} else {
throw new AssertionError("unreachable");
}
return false;
}
/**
* This method is to be called for all non-composition events.
* Calling is optional, except under the following conditions:
*
* {@code modifiesDomAndFiresTextinputAfterComposition} is true AND
* we are between calls to {@link CompositionListener#compositionStart(Object)}
*
* Otherwise it will always be a no-op.
*/
public void handleOtherEvent() {
checkAppComposing();
// Flush if we are outside a composition start...end event sequence,
// but we haven't yet had our delayed callback to notify the listener
// of the end.
if (modifiesDomAndFiresTextInputAfterComposition && !browserComposing) {
flush();
} else {
// do nothing. adding branch for code coverage checking.
return;
}
}
private void compositionStart(V event) {
if (browserComposing) {
logger.error().log("CEH: State was already 'composing' during a compositionstart event!");
return;
}
if (delayAfterTextInput) {
// We don't want to hit the merge logic below - if we've had a text input, always
// flush to ensure there's a corresponding compositionEnd, because the browser might
// be moving on to the next composition phase. But if it's a composition start
// straight after a composition end, then as of this writing it's safe to have them
// merged and avoid redundant events.
assert appComposing();
flush();
}
delayAfterTextInput = false;
browserComposing = true;
if (modifiesDomAndFiresTextInputAfterComposition && timer.isScheduled(endTask)) {
// Got a composition start before our timer fired - just pretend the
// browser never left composition mode.
timer.cancel(endTask);
return;
}
assert appComposing == false;
appComposing = true;
listener.compositionStart(event);
}
private void compositionUpdate() {
checkAppComposing();
assert appComposing == true;
listener.compositionUpdate();
}
private void compositionEnd() {
delayAfterTextInput = false;
checkAppComposing();
if (!browserComposing) {
logger.error().log("CEH: State was not 'composing' during a compositionend event!");
return;
}
browserComposing = false;
if (modifiesDomAndFiresTextInputAfterComposition) {
// Browser is known to modify the dom one last time outside
// a compositionend event - not safe to notify app immediately,
// notify it later after the dom changes have finished.
logger.trace().log("ce schedule");
scheduleEndTask();
} else {
// Browser is known not to modify the dom one last time outside
// a compositionend event - safe to notify app immediately.
logger.trace().log("ce now");
assert appComposing == true;
appComposing = false;
listener.compositionEnd();
}
}
private void textInput() {
checkAppComposing();
if (modifiesDomAndFiresTextInputAfterComposition && appComposing() && !browserComposing) {
delayAfterTextInput = true;
scheduleEndTask();
}
}
/** Use this instead of the boolean, to do the assert sanity check */
private boolean appComposing() {
checkAppComposing();
return appComposing;
}
@VisibleForTesting void checkAppComposing() {
// The appComposing variable should be equivalent to the expression on the right
assert appComposing == (browserComposing || timer.isScheduled(endTask))
: "appComposing variable does not match inferred state";
}
/**
* End the composition sequence from the listener's perspective, if we have a
* delayed end scheduled.
*/
private void flush() {
assert !browserComposing :
"flush should not be called during native composition, because it is impossible to flush";
checkAppComposing();
if (appComposing()) {
timer.cancel(endTask);
endTask.execute();
} else {
// do nothing. adding branch for code coverage checking.
return;
}
}
private void scheduleEndTask() {
timer.scheduleDelayed(endTask, compositionEndDelay);
}
}