/* Copyright (c) 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 com.google.appengine.demos.sticky.client; import com.google.appengine.demos.sticky.client.model.Author; import com.google.appengine.demos.sticky.client.model.Model; import com.google.appengine.demos.sticky.client.model.Note; import com.google.appengine.demos.sticky.client.model.Surface; import com.google.gwt.dom.client.AnchorElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.dom.client.Style; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.AbstractImagePrototype; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.PushButton; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.ToggleButton; /** * A widget that displays the Ui associated with the header of the application. * This includes buttons for adding notes, bring up the surface list and * information about the current surface and user. * * @author knorton@google.com (Kelly Norton) */ public class HeaderView extends FlowPanel implements Model.DataObserver, Surface.Observer { /** * Declaration of image bundle resources used in this widget. */ public interface Images extends SurfaceListView.Images { @Resource("header-add-author-button-hv.gif") AbstractImagePrototype headerAddAuthorButtonHv(); @Resource("header-add-author-button-up.gif") AbstractImagePrototype headerAddAuthorButtonUp(); @Resource("header-add-button-dn.gif") AbstractImagePrototype headerAddButtonDn(); @Resource("header-add-button-hv.gif") AbstractImagePrototype headerAddButtonHv(); @Resource("header-add-button-up.gif") AbstractImagePrototype headerAddButtonUp(); @Resource("header-surfaces-button-dn.gif") AbstractImagePrototype headerSurfacesButtonDn(); @Resource("header-surfaces-button-hv.gif") AbstractImagePrototype headerSurfacesButtonHv(); @Resource("header-surfaces-button-up.gif") AbstractImagePrototype headerSurfacesButtonUp(); } /** * Encapsulates the views and behavior associated with the Ui to add an * author. */ private class EditController implements BlurHandler, KeyPressHandler, ClickHandler { /** * A view displayed after a user submitted an author's email address, but * before the {@link Model} responds with success or failure. If the server * reports a failure, the {@link PendingAuthorView} is also used to display * that error. */ private class PendingAuthorView extends SimplePanel implements ClickHandler, Model.SuccessCallback { private final String name; /** * Constructor. * * @param name * the name to display while the model saves the change on the * server */ public PendingAuthorView(String name) { super(Document.get().createSpanElement()); setStyleName("header-new-author"); this.name = name; final int index = name.indexOf('@'); if (index < 0) { setName(name); showError("invalid, click to fix."); } else { setName(name.substring(0, index)); model.addAuthorToSurface(model.getSelectedSurface(), name, this); } } public void onClick(ClickEvent event) { removePendingAuthorView(this); edit(name); } public void onResponse(boolean success) { if (success) { removePendingAuthorView(this); button.setVisible(true); } else { showError("not found, click to fix."); } } private void setName(String name) { getElement().setInnerText(", " + name); } private void showError(String message) { final SpanElement errorElement = getElement().appendChild( Document.get().createSpanElement()); errorElement.setClassName("header-new-author-error"); final Style errorStyle = errorElement.getStyle(); errorStyle.setProperty("color", "#800"); errorStyle.setProperty("paddingLeft", "2px"); errorStyle.setProperty("fontSize", "80%"); errorElement.setInnerText(message); addHandler(this, ClickEvent.getType()); sinkEvents(Event.ONCLICK); } } private final PushButton button; private final TextBox textBox = new TextBox(); private final Model model; private boolean hasUserData; private boolean editMode; /** * Constructor. * * @param model * the model to use for persisting changes * @param images * a bundle of images to be used for internal widgets * @param styleName * the style name to be applied to the text box */ public EditController(Model model, Images images, String styleName) { this.model = model; button = Buttons.createPushButtonWithImageStates(images .headerAddAuthorButtonUp().createImage(), images .headerAddAuthorButtonHv().createImage(), "header-add-author-button", this); textBox.addBlurHandler(this); textBox.addKeyPressHandler(this); textBox.setVisible(false); textBox.setStyleName(styleName); } /** * Displays the text box and transitons the controller to edit mode. * */ public void edit() { textBox.setText("Enter user's email address"); hasUserData = false; textBox.setStyleName("header-author-edit-nodata"); enterEditMode(); } /** * Displays the text box and transitions the controller to edit mode. * * @param contents * the initial contents for the text box */ public void edit(String contents) { textBox.setText(contents); hasUserData = true; textBox.setStyleName("header-author-edit"); enterEditMode(); } /** * Gets the add button. * * @return */ public PushButton getAddButton() { return button; } /** * Gets the text box for entry of author email address. * * @return */ public TextBox getTextBox() { return textBox; } public void onBlur(BlurEvent event) { // Unintended blur events can happen as visibility is toggled. editMode // ensures that we only handle blur when we are editing. Setting a boolean // is more efficient than unhooking the handler. if (editMode) { commit(); } } public void onClick(ClickEvent event) { button.setVisible(false); edit(); } public void onKeyPress(KeyPressEvent event) { final char charCode = event.getCharCode(); switch (charCode) { case KeyCodes.KEY_ENTER: commit(); break; // It is common for escape to cancel modal editing. case KeyCodes.KEY_ESCAPE: cancel(); break; default: hasUserData = true; textBox.setStyleName("header-author-edit"); } } private void cancel() { resetTextBox(); button.setVisible(true); } private void commit() { final String value = textBox.getValue().trim(); if (hasUserData && value.length() > 0) { createPendingAuthorView(value); resetTextBox(); } else { cancel(); } } private PendingAuthorView createPendingAuthorView(String name) { final Element parentElement = textBox.getElement().getParentElement() .cast(); final Element element = parentElement.insertBefore( Document.get().createSpanElement(), textBox.getElement()).cast(); final PendingAuthorView view = new PendingAuthorView(name); HeaderView.this.add(view, element); return view; } private void enterEditMode() { textBox.setVisible(true); textBox.setFocus(true); textBox.selectAll(); editMode = true; } private void removePendingAuthorView(PendingAuthorView view) { final Element pendingElement = view.getElement().getParentElement() .cast(); HeaderView.this.remove(view); pendingElement.getParentElement().removeChild(pendingElement); } private void resetTextBox() { textBox.setValue(""); textBox.setVisible(false); editMode = false; } } /** * A simple view to display the current user's name and a signout link. */ private static class LoginInfoView extends FlowPanel implements ClickHandler { private final SpanElement userElem; private final AnchorElement linkElem; /** * @param author * the current author * @param logoutUrl * a url that can be used to logout */ public LoginInfoView(Author author, String logoutUrl) { assert author != null; final Element element = getElement(); userElem = element.appendChild(Document.get().createSpanElement()); linkElem = element.appendChild(Document.get().createAnchorElement()); element.setId("login-info"); userElem.setId("login-info-name"); userElem.setInnerText(author.getName()); linkElem.setId("login-info-link"); linkElem.setInnerText("sign out"); linkElem.setHref(logoutUrl); } public void onClick(ClickEvent event) { Window.alert("click"); } } /** * Encapsulates the views and behaviors associated with displaying and closing * the {@link SurfaceListView} and keeping the associated {@link ToggleButton} * in sync. */ private static class SurfaceListViewController implements ClickHandler, SurfaceListView.Observer { private final ToggleButton button; private final SurfaceListView view; /** * @param model * the model to which the enclosed {@link SurfaceListView} will * communicate. * @param images * a bundle of images used for internal Ui elements * @param styleName * a style name for the enclosed {@link ToggleButton} */ public SurfaceListViewController(Model model, Images images, String styleName) { button = Buttons.createToggleButtonWithImageStates(images .headerSurfacesButtonUp().createImage(), images .headerSurfacesButtonHv().createImage(), images .headerSurfacesButtonDn().createImage(), styleName, this); view = new SurfaceListView(images, model, this); } /** * Gets the button that is used to show and hide the surface list view. * * @return */ public ToggleButton getButton() { return button; } /** * Gets the surface list view. * * @return */ public SurfaceListView getSurfaceListView() { return view; } public void onClick(ClickEvent event) { view.setVisible(button.isDown()); } public void onHide() { button.setDown(false); } public void onShow() { } } private static int NOTE_DEFAULT_X = 100; private static int NOTE_DEFAULT_Y = 100; private static int NOTE_DEFAULT_WIDTH = 300; private static int NOTE_DEFAULT_HEIGHT = 250; private final SpanElement surfaceNameElement = Document.get() .createSpanElement(); private final SpanElement authorNamesElement = Document.get() .createSpanElement(); private final EditController editController; /** * @param parent * the parent for this widget * @param model * the model to which the Ui will bind itself */ public HeaderView(Images images, RootPanel parent, final Model model) { parent.add(this); // Setup list of surfaces. final SurfaceListViewController controller = new SurfaceListViewController( model, images, "spc-button"); parent.add(controller.getSurfaceListView()); final Element elem = getElement(); elem.setId("header"); add(Buttons.createPushButtonWithImageStates(images.headerAddButtonUp() .createImage(), images.headerAddButtonHv().createImage(), images .headerAddButtonDn().createImage(), "add-button", new ClickHandler() { public void onClick(ClickEvent event) { model.createNote(NOTE_DEFAULT_X, NOTE_DEFAULT_Y, NOTE_DEFAULT_WIDTH, NOTE_DEFAULT_HEIGHT); } })); add(controller.getButton()); editController = new EditController(model, images, "header-author-edit"); final Surface surface = model.getSelectedSurface(); surface.addObserver(this); attachTitleView(images, surface); add(new LoginInfoView(model.getCurrentAuthor(), model.getLogoutUrl())); model.addDataObserver(this); } public void onNoteCreated(Note note) { } public void onSurfaceCreated(Surface surface) { } public void onSurfaceNotesReceived(Note[] notes) { } public void onSurfaceSelected(Surface nowSelected, Surface wasSelected) { if (wasSelected != null) { wasSelected.removeObserver(this); } nowSelected.addObserver(this); updateTitleView(nowSelected); } public void onSurfacesReceived(Surface[] surfaces) { } public void onUpdate(Surface surface) { updateTitleView(surface); } private void attachTitleView(Images images, Surface surface) { final Element titleElement = getElement().appendChild( Document.get().createDivElement()).cast(); titleElement.setId("header-title"); surfaceNameElement.setId("header-name"); authorNamesElement.setId("header-authors"); titleElement.appendChild(surfaceNameElement); titleElement.appendChild(authorNamesElement); add(editController.getTextBox(), titleElement); add(editController.getAddButton(), titleElement); updateTitleView(surface); } private void updateTitleView(Surface surface) { surfaceNameElement.setInnerText(surface.getTitle()); authorNamesElement.setInnerText("w/ " + surface.getAuthorNamesAsString()); } }