/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.http.client.URL; 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.color.ColorHelper; import org.waveprotocol.wave.client.wavepanel.impl.toolbar.gadget.GadgetInfoProviderImpl; import org.waveprotocol.wave.client.wavepanel.impl.toolbar.gadget.GadgetSelectorWidget; import org.waveprotocol.wave.client.wavepanel.impl.toolbar.gadget.GwtGadgetInfoParser; import org.waveprotocol.wave.client.wavepanel.view.AttachmentPopupView; import org.waveprotocol.wave.client.wavepanel.view.AttachmentPopupView.Listener; import org.waveprotocol.wave.client.widget.popup.UniversalPopup; 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.id.WaveId; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.waveref.WaveRef; import org.waveprotocol.wave.util.escapers.GwtWaverefEncoder; import org.waveprotocol.wave.client.editor.content.FocusedContentRange; import org.waveprotocol.wave.client.doodad.attachment.ImageThumbnail; import org.waveprotocol.wave.client.doodad.attachment.render.ImageThumbnailWrapper; /** * 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; /** The id of the currently loaded wave. */ private WaveId waveId; 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, WaveId waveId) { this.css = css; this.toolbarUi = toolbarUi; this.user = user; this.waveId = waveId; attachmentIdGenerator = new AttachmentIdGeneratorImpl(idGenerator); } /** * Attaches editor behaviour to a toolbar, adding all the edit buttons. */ public static EditToolbar create(ParticipantId user, IdGenerator idGenerator, WaveId waveId) { ToplevelToolbarWidget toolbarUi = new ToplevelToolbarWidget(); EditorToolbarResources.Css css = EditorToolbarResources.Loader.res.css(); return new EditToolbar(css, toolbarUi, user, idGenerator, waveId); } /** 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(); createFontColorButton(group); createFontBackColorButton(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); } 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 createFontBackColorButton(ToolbarView toolbar) { final ToolbarClickButton button = toolbar.addClickButton(); new ToolbarButtonViewBuilder() .setIcon(css.backcolor()) .applyTo(button, new ToolbarClickButton.Listener() { @Override public void onClicked() { ColorHelper.onSetBackColor(editor, button); } }); } private void createFontColorButton(ToolbarView toolbar) { final ToolbarClickButton button = toolbar.addClickButton(); new ToolbarButtonViewBuilder() .setIcon(css.color()) .applyTo(button, new ToolbarClickButton.Listener() { @Override public void onClicked() { ColorHelper.onSetColor(editor, button); } }); } 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"), StyleAnnotationHandler.key("verticalAlign") // 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 FocusedRange focusedRange = editor.getSelectionHelper().getSelectionRange(); GadgetSelectorWidget selector = new GadgetSelectorWidget(new GadgetInfoProviderImpl(new GwtGadgetInfoParser())); selector.addFeaturedOptions(); final UniversalPopup popup = selector.showInPopup(); selector.setListener(new GadgetSelectorWidget.Listener() { @Override public void onSelect(String url) { insertGadget(url, focusedRange); popup.hide(); } }); } }); } private void insertGadget(String url, FocusedRange focusedRange) { int from = -1; if (focusedRange != null) { from = focusedRange.getFocus(); } if (url != null && !url.isEmpty()) { XmlStringBuilder xml = GadgetXmlUtil.constructXml(url, "", user.getAddress()); CMutableDocument document = editor.getDocument(); if (document == null) { return; } if (from != -1) { Point<ContentNode> point = document.locate(from); document.insertXml(point, xml); } else { LineContainers.appendLine(document, xml); } } } private void createInsertAttachmentButton(ToolbarView toolbar, final ParticipantId user) { WaveRef waveRef = WaveRef.of(waveId); Preconditions.checkState(waveRef != null); final String waveRefToken = URL.encode(GwtWaverefEncoder.encodeToUriQueryString(waveRef)); new ToolbarButtonViewBuilder().setIcon(css.insertAttachment()).setTooltip("Insert attachment") .applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() { @Override public void onClicked() { int tmpCursor = -1; FocusedRange focusedRange = editor.getSelectionHelper().getSelectionRange(); if (focusedRange != null) { tmpCursor = focusedRange.getFocus(); } final int cursorLoc = tmpCursor; AttachmentPopupView attachmentView = new AttachmentPopupWidget(); attachmentView.init(new Listener() { @Override public void onShow() { } @Override public void onHide() { } @Override public void onDone(String encodedWaveRef, String attachmentId, 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()); } /* * From UploadToolbarAction in Walkaround * @author hearnden@google.com (David Hearnden) */ CMutableDocument doc = editor.getDocument(); FocusedContentRange selection = editor.getSelectionHelper().getSelectionPoints(); Point<ContentNode> point; if (selection != null) { point = selection.getFocus(); } else { // Focus was probably lost. Bring it back. editor.focus(false); selection = editor.getSelectionHelper().getSelectionPoints(); if (selection != null) { point = selection.getFocus(); } else { // Still no selection. Oh well, put it at the end. point = doc.locate(doc.size() - 1); } } XmlStringBuilder content = ImageThumbnail.constructXml(attachmentId, fileName); ImageThumbnailWrapper thumbnail = ImageThumbnailWrapper.of(doc.insertXml(point, content)); thumbnail.setAttachmentId(attachmentId); } }); attachmentView.setAttachmentId(attachmentIdGenerator.newAttachmentId()); attachmentView.setWaveRef(waveRefToken); 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() { LinkerHelper.onCreateLink(editor); } }); } private void createRemoveLinkButton(ToolbarView toolbar) { new ToolbarButtonViewBuilder() .setIcon(css.removeLink()) .applyTo(toolbar.addClickButton(), new ToolbarClickButton.Listener() { @Override public void onClicked() { LinkerHelper.onClearLink(editor); } }); } 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)); } @SuppressWarnings("unchecked") private static <E> E[] asArray(E... elements) { return elements; } }