/**
* 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;
import com.google.gwt.core.client.GWT;
import org.waveprotocol.wave.client.scheduler.Scheduler;
import org.waveprotocol.wave.client.scheduler.Scheduler.IncrementalTask;
import org.waveprotocol.wave.client.scheduler.Scheduler.Priority;
import org.waveprotocol.wave.client.scheduler.SchedulerInstance;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.ReadableStringSet;
import org.waveprotocol.wave.model.util.StringSet;
/**
* Editor update event implementation.
*
* Keeps track of listeners and scheduling.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class EditorUpdateEventImpl implements EditorUpdateEvent {
/** Schedule interval for internal throttling */
private static final int INITIAL_NOTIFY_SCHEDULE_DELAY_MS = 20;
// This is used, among other things, for updating toolbar buttons, which
// typically involve annotation queries, which are quite slow.
// In non-compiled mode, a single button update is on the order of 10ms. In
// compiled mode, it is more in the order of 1ms.
// With ~40 buttons to update, there is significant lag in non-compiled
// mode. This is addressed by rate-limiting the updates, with a very large
// delay in compiled mode. In non-compiled mode, frequent lag of ~50ms is
// noticeable, and so we still rate limit the updates, but with a more
// generous speed.
// Update at most twice per second in compiled mode, but much less
// frequently in non-compiled mode.
private static final int NOTIFY_SCHEDULE_DELAY_GAP_MS = GWT.isScript() ? 500 : 2000;
/** Set of objects listening in to our editor */
private final CopyOnWriteSet<EditorUpdateEvent.EditorUpdateListener> updateListeners =
CopyOnWriteSet.create();
/** Editor this update refers to. */
private final EditorImpl editor;
private boolean notifyAgain = false;
/**
* @see EditorUpdateEvent#selectionCoordsChanged()
*/
private boolean notedSelectionCoordsChanged = false;
/**
* @see EditorUpdateEvent#selectionLocationChanged()
*/
private boolean notedSelectionLocationChanged = false;
/**
* @see EditorUpdateEvent#contentChanged()
*/
private boolean notedContentChanged = false;
/**
* @see EditorUpdateEvent#contentChangedDirectlyByUser()
*/
private boolean notedUserDirectlyChangedContent = false;
/** Used for debugging, e.g. tracking down bad handlers */
private final StringSet suppressedEventNames = CollectionUtils.createStringSet();
/** Task to notify listeners of an update in the editor */
IncrementalTask notificationTask = new IncrementalTask() {
int delays = 0;
@Override
public boolean execute() {
if (!editor.isConsistent()) {
// reschedule
if (EditorStaticDeps.logger.trace().shouldLog()) {
EditorStaticDeps.logger.trace().log("Notification deferred for consistency reasons");
}
delays++;
if (delays == 20) {
EditorStaticDeps.logger.error().log("More than 20 notification delays encountered - " +
"possibly uncleared extraction state");
}
notifyAgain = true;
} else {
if (editor.hasDocument()) {
if (EditorStaticDeps.logger.trace().shouldLog()) {
EditorStaticDeps.logger.trace().log("EditorUpdateEvent: " +
"selCoords:" + notedSelectionCoordsChanged + ", " +
"selLoc:" + notedSelectionLocationChanged + ", " +
"content:" + notedContentChanged + ", " +
"userDirectlyChangedContent:" + notedUserDirectlyChangedContent);
}
// alert the listeners:
for (EditorUpdateEvent.EditorUpdateListener l : updateListeners) {
if (suppressedEventNames.contains(l.getClass().getName())) {
continue;
}
l.onUpdate(EditorUpdateEventImpl.this);
}
notedSelectionCoordsChanged = false;
notedSelectionLocationChanged = false;
notedContentChanged = false;
notedUserDirectlyChangedContent = false;
}
delays = 0;
if (EditorStaticDeps.logger.trace().shouldLog()) {
EditorStaticDeps.logger.trace().log("Notification sent");
}
}
boolean ret = notifyAgain;
notifyAgain = false;
return ret;
}
@Override
public String toString() {
return "EditorUpdateEventImpl.notificationTask [update listeners: " + updateListeners + "]";
}
};
EditorUpdateEventImpl(EditorImpl editor) {
this.editor = editor;
}
@Override
public boolean selectionCoordsChanged() {
return notedSelectionCoordsChanged;
}
@Override
public boolean selectionLocationChanged() {
return notedSelectionLocationChanged;
}
@Override
public boolean contentChanged() {
return notedContentChanged;
}
@Override
public boolean contentChangedDirectlyByUser() {
return notedUserDirectlyChangedContent;
}
/**
* Schedule the editor's update notification
*/
void scheduleUpdateNotification(
boolean selectionCoordsChanged,
boolean selectionLocationChanged,
boolean contentChanged,
boolean userDirectlyChangedContent) {
// Internal editor throttling
// We want special behaviour, here where we do not reset the delay every
// time this method is called by rescheduling - so we first check if the
// notification task is scheduled.
notedSelectionCoordsChanged |= selectionCoordsChanged;
notedSelectionLocationChanged |= selectionLocationChanged;
notedContentChanged |= contentChanged;
notedUserDirectlyChangedContent |= userDirectlyChangedContent;
Scheduler scheduler = SchedulerInstance.get();
if (!scheduler.isScheduled(notificationTask)) {
scheduler.scheduleRepeating(Priority.MEDIUM, notificationTask,
INITIAL_NOTIFY_SCHEDULE_DELAY_MS, NOTIFY_SCHEDULE_DELAY_GAP_MS);
} else {
notifyAgain = true;
}
}
void addUpdateListener(EditorUpdateEvent.EditorUpdateListener listener) {
updateListeners.add(listener);
}
void removeUpdateListener(EditorUpdateEvent.EditorUpdateListener listener) {
updateListeners.remove(listener);
}
void flushUpdates() {
SchedulerInstance.get().cancel(notificationTask);
notificationTask.execute();
}
void clear() {
SchedulerInstance.get().cancel(notificationTask);
updateListeners.clear();
}
@Override
public EditorContext context() {
return editor;
}
/**
* Suppresses running of update events whose class name matches {@code name}.
* For debugging only.
*
* @param name class name of object implementing
* {@link EditorUpdateEvent.EditorUpdateListener}
* @param suppress trrue to suppress, false to allow
*/
public void debugSuppressUpdateEvent(String name, boolean suppress) {
if (suppress) {
suppressedEventNames.add(name);
} else {
suppressedEventNames.remove(name);
}
}
/**
* Gets the class names of all registered event listeners. For debugging only.
*
* @return a new string set each time, containing the class names of all
* registered update event listeners. it is safe to modify this set.
*/
public StringSet debugGetAllUpdateEventNames() {
StringSet events = CollectionUtils.createStringSet();
for (EditorUpdateEvent.EditorUpdateListener l : updateListeners) {
events.add(l.getClass().getName());
}
return events;
}
/**
* Gets the class names of suppressed event listeners. For debugging only.
*
* @return a live readable view of the currently suppressed events.
*/
public ReadableStringSet debugGetSuppressedUpdateEventNames() {
return suppressedEventNames;
}
}