/**
* 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.toolbar;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.Window;
import org.waveprotocol.wave.client.common.util.WaveRefConstants;
import org.waveprotocol.wave.client.doodad.link.Link;
import org.waveprotocol.wave.client.doodad.link.Link.InvalidLinkException;
import org.waveprotocol.wave.client.editor.Editor;
import org.waveprotocol.wave.client.editor.EditorContext;
import org.waveprotocol.wave.client.editor.EditorContextAdapter;
import org.waveprotocol.wave.client.editor.content.CMutableDocument;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.ContentNode;
import org.waveprotocol.wave.client.editor.content.misc.StyleAnnotationHandler;
import org.waveprotocol.wave.client.editor.content.paragraph.Paragraph;
import org.waveprotocol.wave.client.editor.content.paragraph.Paragraph.LineStyle;
import org.waveprotocol.wave.client.editor.toolbar.ButtonUpdater;
import org.waveprotocol.wave.client.editor.toolbar.ParagraphApplicationController;
import org.waveprotocol.wave.client.editor.toolbar.ParagraphTraversalController;
import org.waveprotocol.wave.client.editor.toolbar.TextSelectionController;
import org.waveprotocol.wave.client.editor.util.EditorAnnotationUtil;
import org.waveprotocol.wave.client.gadget.GadgetXmlUtil;
import org.waveprotocol.wave.client.wavepanel.impl.toolbar.attachment.AttachmentPopupWidget;
import org.waveprotocol.wave.client.wavepanel.impl.toolbar.gadget.popup.GadgetPopupView;
import org.waveprotocol.wave.client.wavepanel.impl.toolbar.gadget.popup.GadgetPopupWidget;
import org.waveprotocol.wave.client.wavepanel.view.AttachmentPopupView;
import org.waveprotocol.wave.client.wavepanel.view.AttachmentPopupView.Listener;
import org.waveprotocol.wave.client.widget.toolbar.SubmenuToolbarView;
import org.waveprotocol.wave.client.widget.toolbar.ToolbarButtonViewBuilder;
import org.waveprotocol.wave.client.widget.toolbar.ToolbarView;
import org.waveprotocol.wave.client.widget.toolbar.ToplevelToolbarWidget;
import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarClickButton;
import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarToggleButton;
import org.waveprotocol.wave.media.model.AttachmentIdGenerator;
import org.waveprotocol.wave.media.model.AttachmentIdGeneratorImpl;
import org.waveprotocol.wave.model.document.util.FocusedRange;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
import org.waveprotocol.wave.model.id.IdGenerator;
import org.waveprotocol.wave.model.wave.ParticipantId;
/**
* Attaches actions that can be performed in a Wave's "edit mode" to a toolbar.
* <p>
* Also constructs an initial set of such actions.
*
* @author kalman@google.com (Benjamin Kalman)
*/
public class EditToolbar {
/**
* Handler for click buttons added with {@link EditToolbar#addClickButton}.
*/
public interface ClickHandler {
void onClicked(EditorContext context);
}
/**
* Container for a font family.
*/
private static final class FontFamily {
public final String description;
public final String style;
public FontFamily(String description, String style) {
this.description = description;
this.style = style;
}
}
/**
* Container for an alignment.
*/
private static final class Alignment {
public final String description;
public final String iconCss;
public final LineStyle style;
public Alignment(String description, String iconCss, LineStyle style) {
this.description = description;
this.iconCss = iconCss;
this.style = style;
}
}
private final EditorToolbarResources.Css css;
private final ToplevelToolbarWidget toolbarUi;
private final ParticipantId user;
private final AttachmentIdGenerator attachmentIdGenerator;
private final EditorContextAdapter editor = new EditorContextAdapter(null);
private final ButtonUpdater updater = new ButtonUpdater(editor);
private EditToolbar(EditorToolbarResources.Css css, ToplevelToolbarWidget toolbarUi,
ParticipantId user, IdGenerator idGenerator) {
this.css = css;
this.toolbarUi = toolbarUi;
this.user = user;
attachmentIdGenerator = new AttachmentIdGeneratorImpl(idGenerator);
}
/**
* Attaches editor behaviour to a toolbar, adding all the edit buttons.
*/
public static EditToolbar create(ParticipantId user, IdGenerator idGenerator) {
ToplevelToolbarWidget toolbarUi = new ToplevelToolbarWidget();
EditorToolbarResources.Css css = EditorToolbarResources.Loader.res.css();
return new EditToolbar(css, toolbarUi, user, idGenerator);
}
/** Constructs the initial set of actions in the toolbar. */
public void init() {
ToolbarView group = toolbarUi.addGroup();
createBoldButton(group);
createItalicButton(group);
createUnderlineButton(group);
createStrikethroughButton(group);
group = toolbarUi.addGroup();
createSuperscriptButton(group);
createSubscriptButton(group);
group = toolbarUi.addGroup();
createFontSizeButton(group);
createFontFamilyButton(group);
createHeadingButton(group);
group = toolbarUi.addGroup();
createIndentButton(group);
createOutdentButton(group);
group = toolbarUi.addGroup();
createUnorderedListButton(group);
createOrderedListButton(group);
group = toolbarUi.addGroup();
createAlignButtons(group);
createClearFormattingButton(group);
group = toolbarUi.addGroup();
createInsertLinkButton(group);
createRemoveLinkButton(group);
group = toolbarUi.addGroup();
createInsertGadgetButton(group, user);
group = toolbarUi.addGroup();
createInsertAttachmentButton(group, user);
group = toolbarUi.addGroup();
new ToolbarButtonViewBuilder().setText("").applyTo(group.addClickButton(),null);
}
private void createBoldButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.bold())
.applyTo(b, createTextSelectionController(b, "fontWeight", "bold"));
}
private void createItalicButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.italic())
.applyTo(b, createTextSelectionController(b, "fontStyle", "italic"));
}
private void createUnderlineButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.underline())
.applyTo(b, createTextSelectionController(b, "textDecoration", "underline"));
}
private void createStrikethroughButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.strikethrough())
.applyTo(b, createTextSelectionController(b, "textDecoration", "line-through"));
}
private void createSuperscriptButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.superscript())
.applyTo(b, createTextSelectionController(b, "verticalAlign", "super"));
}
private void createSubscriptButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.subscript())
.applyTo(b, createTextSelectionController(b, "verticalAlign", "sub"));
}
private void createFontSizeButton(ToolbarView toolbar) {
SubmenuToolbarView submenu = toolbar.addSubmenu();
new ToolbarButtonViewBuilder()
.setIcon(css.fontSize())
.applyTo(submenu, null);
submenu.setShowDropdownArrow(false); // Icon already has dropdown arrow.
// TODO(kalman): default text size option.
ToolbarView group = submenu.addGroup();
for (int size : asArray(8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 28, 32, 36, 42, 48, 56, 64, 72)) {
ToolbarToggleButton b = group.addToggleButton();
double baseSize = 12.0;
b.setVisualElement(createFontSizeElement(baseSize, size));
b.setListener(createTextSelectionController(b, "fontSize", (size / baseSize) + "em"));
}
}
private Element createFontSizeElement(double baseSize, double size) {
Element e = Document.get().createSpanElement();
e.getStyle().setFontSize(size / baseSize, Unit.EM);
e.setInnerText(((int) size) + "");
return e;
}
private void createFontFamilyButton(ToolbarView toolbar) {
SubmenuToolbarView submenu = toolbar.addSubmenu();
new ToolbarButtonViewBuilder()
.setIcon(css.fontFamily())
.applyTo(submenu, null);
submenu.setShowDropdownArrow(false); // Icon already has dropdown arrow.
createFontFamilyGroup(submenu.addGroup(), new FontFamily("Default", null));
createFontFamilyGroup(submenu.addGroup(),
new FontFamily("Sans Serif", "sans-serif"),
new FontFamily("Serif", "serif"),
new FontFamily("Wide", "arial black,sans-serif"),
new FontFamily("Narrow", "arial narrow,sans-serif"),
new FontFamily("Fixed Width", "monospace"));
createFontFamilyGroup(submenu.addGroup(),
new FontFamily("Arial", "arial,helvetica,sans-serif"),
new FontFamily("Comic Sans MS", "comic sans ms,sans-serif"),
new FontFamily("Courier New", "courier new,monospace"),
new FontFamily("Garamond", "garamond,serif"),
new FontFamily("Georgia", "georgia,serif"),
new FontFamily("Tahoma", "tahoma,sans-serif"),
new FontFamily("Times New Roman", "times new roman,serif"),
new FontFamily("Trebuchet MS", "trebuchet ms,sans-serif"),
new FontFamily("Verdana", "verdana,sans-serif"));
}
private void createFontFamilyGroup(ToolbarView toolbar, FontFamily... families) {
for (FontFamily family : families) {
ToolbarToggleButton b = toolbar.addToggleButton();
b.setVisualElement(createFontFamilyElement(family));
b.setListener(createTextSelectionController(b, "fontFamily", family.style));
}
}
private Element createFontFamilyElement(FontFamily family) {
Element e = Document.get().createSpanElement();
e.getStyle().setProperty("fontFamily", family.style);
e.setInnerText(family.description);
return e;
}
private void createClearFormattingButton(ToolbarView toolbar) {
new ToolbarButtonViewBuilder()
.setIcon(css.clearFormatting())
.applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() {
@Override public void onClicked() {
EditorAnnotationUtil.clearAnnotationsOverSelection(editor, asArray(
StyleAnnotationHandler.key("backgroundColor"),
StyleAnnotationHandler.key("color"),
StyleAnnotationHandler.key("fontFamily"),
StyleAnnotationHandler.key("fontSize"),
StyleAnnotationHandler.key("fontStyle"),
StyleAnnotationHandler.key("fontWeight"),
StyleAnnotationHandler.key("textDecoration")
// NOTE: add more as required.
));
createClearHeadingsListener().onClicked();
}
});
}
private void createInsertGadgetButton(ToolbarView toolbar, final ParticipantId user) {
new ToolbarButtonViewBuilder()
.setIcon(css.insertGadget())
.applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() {
@Override public void onClicked() {
final int from;
int tmpFrom = -1;
FocusedRange focusedRange = editor.getSelectionHelper().getSelectionRange();
if (focusedRange != null) {
tmpFrom = focusedRange.getFocus();
}
from = tmpFrom;
GadgetPopupView gadgetPopup = new GadgetPopupWidget();
gadgetPopup.init(new GadgetPopupWidget.Listener() {
@Override
public void onShow() {
}
@Override
public void onInsert(String url) {
if (url != null && !url.isEmpty()) {
XmlStringBuilder xml = GadgetXmlUtil.constructXml(url, "", user.getAddress());
if (from != -1) {
CMutableDocument doc = editor.getDocument();
Point<ContentNode> point = doc.locate(from);
doc.insertXml(point, xml);
} else {
LineContainers.appendLine(editor.getDocument(), xml);
}
}
}
@Override
public void onHide() {
}
});
gadgetPopup.show();
}
});
}
private void createInsertAttachmentButton(ToolbarView toolbar, final ParticipantId user) {
new ToolbarButtonViewBuilder().setIcon(css.insertAttachment()).setTooltip("Insert attachment")
.applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() {
@Override
public void onClicked() {
final int from;
int tmpFrom = -1;
FocusedRange focusedRange = editor.getSelectionHelper().getSelectionRange();
if (focusedRange != null) {
tmpFrom = focusedRange.getFocus();
}
from = tmpFrom;
AttachmentPopupView attachmentView = new AttachmentPopupWidget();
attachmentView.init(new Listener() {
@Override
public void onShow() {
}
@Override
public void onHide() {
}
@Override
public void onDone(String id, String fullFileName) {
// Insert a file name linking to the attachment URL.
int lastSlashPos = fullFileName.lastIndexOf("/");
int lastBackSlashPos = fullFileName.lastIndexOf("\\");
String fileName = fullFileName;
if (lastSlashPos != -1) {
fileName = fullFileName.substring(lastSlashPos + 1, fullFileName.length());
} else if (lastBackSlashPos != -1) {
fileName = fullFileName.substring(lastBackSlashPos + 1, fullFileName.length());
}
XmlStringBuilder xml = XmlStringBuilder.createFromXmlString(fileName);
int to = -1;
int docSize = editor.getDocument().size();
if (from != -1) {
CMutableDocument doc = editor.getDocument();
Point<ContentNode> point = doc.locate(from);
doc.insertXml(point, xml);
} else {
LineContainers.appendLine(editor.getDocument(), xml);
}
to = from + editor.getDocument().size() - docSize;
String linkValue =
GWT.getHostPageBaseURL() + "attachment/" + id + "?fileName=" + fileName;
EditorAnnotationUtil.setAnnotationOverRange(editor.getDocument(),
editor.getCaretAnnotations(), Link.MANUAL_KEY, linkValue, from, to);
EditorAnnotationUtil.setAnnotationOverRange(editor.getDocument(),
editor.getCaretAnnotations(), "attachment/id", id, from, to);
EditorAnnotationUtil.setAnnotationOverRange(editor.getDocument(),
editor.getCaretAnnotations(), "attachment/fileName", fileName, from, to);
}
});
attachmentView.setAttachmentId(attachmentIdGenerator.newAttachmentId());
attachmentView.show();
}
});
}
private void createInsertLinkButton(ToolbarView toolbar) {
// TODO (Yuri Z.) use createTextSelectionController when the full
// link doodad is incorporated
new ToolbarButtonViewBuilder()
.setIcon(css.insertLink())
.applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() {
@Override public void onClicked() {
FocusedRange range = editor.getSelectionHelper().getSelectionRange();
if (range == null || range.isCollapsed()) {
Window.alert("Select some text to create a link.");
return;
}
String rawLinkValue =
Window.prompt("Enter link: URL or Wave ID.", WaveRefConstants.WAVE_URI_PREFIX);
// user hit "ESC" or "cancel"
if (rawLinkValue == null) {
return;
}
try {
String linkAnnotationValue = Link.normalizeLink(rawLinkValue);
EditorAnnotationUtil.setAnnotationOverSelection(editor, Link.MANUAL_KEY,
linkAnnotationValue);
} catch (InvalidLinkException e) {
Window.alert(e.getLocalizedMessage());
}
}
});
}
private void createRemoveLinkButton(ToolbarView toolbar) {
new ToolbarButtonViewBuilder()
.setIcon(css.removeLink())
.applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() {
@Override public void onClicked() {
if (editor.getSelectionHelper().getSelectionRange() != null) {
EditorAnnotationUtil.clearAnnotationsOverSelection(editor, Link.LINK_KEYS);
}
}
});
}
private ToolbarClickButton.Listener createClearHeadingsListener() {
return new ParagraphTraversalController(editor, new ContentElement.Action() {
@Override public void execute(ContentElement e) {
e.getMutableDoc().setElementAttribute(e, Paragraph.SUBTYPE_ATTR, null);
}
});
}
private void createHeadingButton(ToolbarView toolbar) {
SubmenuToolbarView submenu = toolbar.addSubmenu();
new ToolbarButtonViewBuilder()
.setIcon(css.heading())
.applyTo(submenu, null);
submenu.setShowDropdownArrow(false); // Icon already has dropdown arrow.
ToolbarClickButton defaultButton = submenu.addClickButton();
new ToolbarButtonViewBuilder()
.setText("Default")
.applyTo(defaultButton, createClearHeadingsListener());
ToolbarView group = submenu.addGroup();
for (int level : asArray(1, 2, 3, 4)) {
ToolbarToggleButton b = group.addToggleButton();
b.setVisualElement(createHeadingElement(level));
b.setListener(createParagraphApplicationController(b, Paragraph.regularStyle("h" + level)));
}
}
private Element createHeadingElement(int level) {
Element e = Document.get().createElement("h" + level);
e.getStyle().setMarginTop(2, Unit.PX);
e.getStyle().setMarginBottom(2, Unit.PX);
e.setInnerText("Heading " + level);
return e;
}
private void createIndentButton(ToolbarView toolbar) {
ToolbarClickButton b = toolbar.addClickButton();
new ToolbarButtonViewBuilder()
.setIcon(css.indent())
.applyTo(b, new ParagraphTraversalController(editor, Paragraph.INDENTER));
}
private void createOutdentButton(ToolbarView toolbar) {
ToolbarClickButton b = toolbar.addClickButton();
new ToolbarButtonViewBuilder()
.setIcon(css.outdent())
.applyTo(b, new ParagraphTraversalController(editor, Paragraph.OUTDENTER));
}
private void createUnorderedListButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.unorderedlist())
.applyTo(b, createParagraphApplicationController(b, Paragraph.listStyle(null)));
}
private void createOrderedListButton(ToolbarView toolbar) {
ToolbarToggleButton b = toolbar.addToggleButton();
new ToolbarButtonViewBuilder()
.setIcon(css.orderedlist())
.applyTo(b, createParagraphApplicationController(
b, Paragraph.listStyle(Paragraph.LIST_STYLE_DECIMAL)));
}
private void createAlignButtons(ToolbarView toolbar) {
SubmenuToolbarView submenu = toolbar.addSubmenu();
new ToolbarButtonViewBuilder()
.setIcon(css.alignDrop())
.applyTo(submenu, null);
submenu.setShowDropdownArrow(false); // Icon already has dropdown arrow.
ToolbarView group = submenu.addGroup();
for (Alignment alignment : asArray(
new Alignment("Left", css.alignLeft(), Paragraph.Alignment.LEFT),
new Alignment("Centre", css.alignCentre(), Paragraph.Alignment.CENTER),
new Alignment("Right", css.alignRight(), Paragraph.Alignment.RIGHT))) {
ToolbarToggleButton b = group.addToggleButton();
new ToolbarButtonViewBuilder()
.setText(alignment.description)
.setIcon(alignment.iconCss)
.applyTo(b, createParagraphApplicationController(b, alignment.style));
}
}
/**
* Adds a button to this toolbar.
*/
public void addClickButton(String icon, final ClickHandler handler) {
ToolbarClickButton.Listener uiHandler = new ToolbarClickButton.Listener() {
@Override
public void onClicked() {
handler.onClicked(editor);
}
};
new ToolbarButtonViewBuilder().setIcon(icon).applyTo(toolbarUi.addClickButton(), uiHandler);
}
/**
* Starts listening to editor changes.
*
* @throws IllegalStateException if this toolbar is already enabled
* @throws IllegalArgumentException if the editor is <code>null</code>
*/
public void enable(Editor editor) {
this.editor.checkEditor(null);
Preconditions.checkArgument(editor != null);
this.editor.switchEditor(editor);
editor.addUpdateListener(updater);
updater.updateButtonStates();
}
/**
* Stops listening to editor changes.
*
* @throws IllegalStateException if this toolbar is not currently enabled
* @throws IllegalArgumentException if this toolbar is currently enabled for a
* different editor
*/
public void disable(Editor editor) {
this.editor.checkEditor(editor);
// The above won't throw if we're not currently enabled, but it makes sure
// 'editor' is the same as the current editor, if any. So if 'editor' is
// null, it means we aren't enabled (the wrapped editor is null too).
Preconditions.checkState(editor != null);
editor.removeUpdateListener(updater);
this.editor.switchEditor(null);
}
/**
* @return the {@link ToplevelToolbarWidget} backing this toolbar.
*/
public ToplevelToolbarWidget getWidget() {
return toolbarUi;
}
private ToolbarToggleButton.Listener createParagraphApplicationController(ToolbarToggleButton b,
LineStyle style) {
return updater.add(new ParagraphApplicationController(b, editor, style));
}
private ToolbarToggleButton.Listener createTextSelectionController(ToolbarToggleButton b,
String styleName, String value) {
return updater.add(new TextSelectionController(b, editor,
StyleAnnotationHandler.key(styleName), value));
}
private static <E> E[] asArray(E... elements) {
return elements;
}
}