/**
* Copyright 2010 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.wavepanel.impl.edit;
import com.google.common.base.Preconditions;
import com.google.gwt.user.client.ui.Widget;
import org.waveprotocol.wave.client.common.util.EventWrapper;
import org.waveprotocol.wave.client.common.util.KeyCombo;
import org.waveprotocol.wave.client.common.util.KeySignalListener;
import org.waveprotocol.wave.client.common.util.LogicalPanel;
import org.waveprotocol.wave.client.common.util.SignalEvent;
import org.waveprotocol.wave.client.debug.logger.LogLevel;
import org.waveprotocol.wave.client.doodad.selection.SelectionExtractor;
import org.waveprotocol.wave.client.doodad.title.TitleAnnotationHandler;
import org.waveprotocol.wave.client.editor.Editor;
import org.waveprotocol.wave.client.editor.EditorSettings;
import org.waveprotocol.wave.client.editor.Editors;
import org.waveprotocol.wave.client.editor.content.CMutableDocument;
import org.waveprotocol.wave.client.editor.content.ContentDocument;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.keys.KeyBindingRegistry;
import org.waveprotocol.wave.client.util.ClientFlags;
import org.waveprotocol.wave.client.wave.DocumentRegistry;
import org.waveprotocol.wave.client.wave.InteractiveDocument;
import org.waveprotocol.wave.client.wavepanel.WavePanel;
import org.waveprotocol.wave.client.wavepanel.impl.WavePanelImpl;
import org.waveprotocol.wave.client.wavepanel.impl.focus.FocusFramePresenter;
import org.waveprotocol.wave.client.wavepanel.view.BlipView;
import org.waveprotocol.wave.client.wavepanel.view.IntrinsicBlipMetaView.MenuOption;
import org.waveprotocol.wave.client.wavepanel.view.dom.ModelAsViewProvider;
import org.waveprotocol.wave.model.conversation.ConversationBlip;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
/**
* Interprets focus-frame movement as reading actions, and also provides an
* ordering for focus frame movement, based on unread content.
*
*/
public final class EditSession
implements FocusFramePresenter.Listener, WavePanel.LifecycleListener, KeySignalListener {
public interface Listener {
void onSessionStart(Editor e, BlipView blipUi);
void onSessionEnd(Editor e, BlipView blipUi);
}
private static final EditorSettings EDITOR_SETTINGS =
new EditorSettings().setHasDebugDialog(
LogLevel.showErrors() || ClientFlags.get().enableEditorDebugging()).setUndoEnabled(
ClientFlags.get().enableUndo()).setUseFancyCursorBias(
ClientFlags.get().useFancyCursorBias()).setUseSemanticCopyPaste(
ClientFlags.get().useSemanticCopyPaste()).setUseWhitelistInEditor(
ClientFlags.get().useWhitelistInEditor()).setUseWebkitCompositionEvents(
ClientFlags.get().useWebkitCompositionEvents()).setCloseSuggestionsMenuDelayMs(
ClientFlags.get().closeSuggestionsMenuDelayMs());
private static final KeyBindingRegistry KEY_BINDINGS = new KeyBindingRegistry();
private final ModelAsViewProvider views;
private final DocumentRegistry<? extends InteractiveDocument> documents;
private final LogicalPanel container;
private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();
private final SelectionExtractor selectionExtractor;
/** The UI of the document being edited. */
private BlipView editing;
/** Editor control. */
private Editor editor;
private static boolean hastTitleAnnotation(CMutableDocument mutable) {
int docSize = mutable.size();
int changeLoc = mutable.firstAnnotationChange(0, docSize, TitleAnnotationHandler.KEY, null);
return changeLoc != -1;
}
private static void computeAndSetWaveTitle(CMutableDocument mutable) {
mutable.setAnnotation(0, 1, TitleAnnotationHandler.KEY,
DocHelper.getTextForElement(mutable, LineContainers.LINE_TAGNAME));
}
EditSession(ModelAsViewProvider views, DocumentRegistry<? extends InteractiveDocument> documents,
LogicalPanel container, SelectionExtractor selectionExtractor) {
this.views = views;
this.documents = documents;
this.container = container;
this.selectionExtractor = selectionExtractor;
}
public static EditSession install(ModelAsViewProvider views,
DocumentRegistry<? extends InteractiveDocument> documents,
SelectionExtractor selectionExtractor, FocusFramePresenter focus, WavePanelImpl panel) {
EditSession edit = new EditSession(views, documents, panel.getGwtPanel(), selectionExtractor);
focus.addListener(edit);
if (panel.hasContents()) {
edit.onInit();
}
panel.addListener(focus);
// Warms up the editor code (e.g., internal statics) by creating and throwing
// away an editor, in order to reduce the latency of starting the first edit
// session.
Editors.create();
return edit;
}
@Override
public void onInit() {
}
@Override
public void onReset() {
endSession();
}
/**
* Starts an edit session on a blip. If there is already an edit session on
* another blip, that session will be moved to the new blip.
*
* @param blipUi blip to edit
*/
public void startEditing(BlipView blipUi) {
Preconditions.checkArgument(blipUi != null);
endSession();
startNewSession(blipUi);
}
/**
* Ends the current edit session, if there is one.
*/
public void stopEditing() {
endSession();
}
/**
* Starts a new document-edit session on a blip.
*
* @param blipUi blip to edit.
*/
private void startNewSession(BlipView blipUi) {
assert !isEditing() && blipUi != null;
// Find the document.
ContentDocument document = documents.get(views.getBlip(blipUi)).getDocument();
blipUi.getMeta().select(MenuOption.EDIT);
// Create or re-use and editor for it.
editor = Editors.attachTo(document);
container.doAdopt(editor.getWidget());
editor.init(null, KEY_BINDINGS, EDITOR_SETTINGS);
editor.addKeySignalListener(this);
editor.setEditing(true);
editor.focus(false);
editing = blipUi;
selectionExtractor.start(editor);
fireOnSessionStart(editor, blipUi);
}
/**
* Stops editing if there is currently an edit session.
*/
private void endSession() {
if (isEditing()) {
BlipView blipUi = getBlip();
if (blipUi != null) {
CMutableDocument mutable = getEditor().getDocument();
ConversationBlip editBlip = views.getBlip(blipUi);
if (editBlip.isRoot() || !hastTitleAnnotation(mutable)) {
computeAndSetWaveTitle(mutable);
}
}
selectionExtractor.stop(editor);
container.doOrphan(editor.getWidget());
editor.blur();
editor.setEditing(false);
// "removeContent" just means detach the editor from the document.
editor.removeContent();
editor.reset();
// TODO(user): this does not work if the view has been deleted and
// detached.
editing.getMeta().deselect(MenuOption.EDIT);
Editor oldEditor = editor;
BlipView oldEditing = editing;
editor = null;
editing = null;
fireOnSessionEnd(oldEditor, oldEditing);
}
}
/** @return true if there is an active edit session. */
public boolean isEditing() {
return editing != null;
}
/** @return the blip UI of the current edit session, or {@code null}. */
public BlipView getBlip() {
return editing;
}
/** @return the editor of the current edit session, or {@code null}. */
public Editor getEditor() {
return editor;
}
//
// Events.
//
@Override
public void onFocusMoved(BlipView oldUi, BlipView newUi) {
boolean wasEditing = isEditing();
endSession();
if (wasEditing && newUi != null) {
startEditing(newUi);
}
}
@Override
public boolean onKeySignal(Widget sender, SignalEvent signal) {
KeyCombo key = EventWrapper.getKeyCombo(signal);
switch (key) {
case SHIFT_ENTER:
endSession();
return true;
case ESC:
// TODO: undo.
endSession();
return true;
default:
return false;
}
}
//
// Listeners.
//
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
private void fireOnSessionStart(Editor editor, BlipView blipUi) {
for (Listener listener : listeners) {
listener.onSessionStart(editor, blipUi);
}
}
private void fireOnSessionEnd(Editor editor, BlipView blipUi) {
for (Listener listener : listeners) {
listener.onSessionEnd(editor, blipUi);
}
}
}