// Copyright 2012 Google Inc. All Rights Reserved. // // 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.collide.client.status; import com.google.collide.client.status.StatusMessage.MessageType; import com.google.collide.client.testing.DebugAttributeSetter; import com.google.collide.client.testing.DebugId; import com.google.collide.client.util.CssUtils; import com.google.collide.client.util.Elements; import com.google.collide.client.util.logging.Log; import com.google.collide.json.client.JsoArray; import com.google.collide.mvp.CompositeView; import com.google.collide.mvp.UiComponent; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.ImageResource; import elemental.css.CSSStyleDeclaration; import elemental.css.CSSStyleDeclaration.Display; import elemental.css.CSSStyleDeclaration.Visibility; import elemental.events.Event; import elemental.events.EventListener; import elemental.html.DivElement; import elemental.html.Element; import elemental.html.PreElement; import elemental.html.SpanElement; /** * The StatusPresenter handles status events and renders them into the UI. This * presenter is meant to be injected into some already constructed DOM space * since it does not define its own dimensions. * */ public class StatusPresenter extends UiComponent<StatusPresenter.View> implements StatusHandler { public interface Css extends CssResource { String actionsContainer(); String action(); String expandedFatal(); String expandedRegular(); String fatal(); String fatalImage(); String inline(); String longText(); String more(); String statusArea(); String statusConfirmation(); String statusDismiss(); String statusError(); String statusLoading(); String statusText(); } public interface Resources extends ClientBundle { @Source("close.png") ImageResource close(); @Source("fatal_border.png") ImageResource fatalBorder(); @Source({"com/google/collide/client/common/constants.css", "StatusPresenter.css"}) Css statusPresenterCss(); } public static class View extends CompositeView<StatusPresenter.ViewEvents> { private static final int PADDING_WIDTH = 60; private DivElement actionsContainer; private final Css css; private DivElement fatalImage; private PreElement longText; private SpanElement more; private DivElement statusDismiss; private StatusMessage statusMessage; private DivElement statusText; public View(Resources resources) { this.css = resources.statusPresenterCss(); setElement(createDom()); } public void clear() { getElement().getStyle().setVisibility(Visibility.HIDDEN); statusDismiss.getStyle().setVisibility(Visibility.HIDDEN); longText.getStyle().setDisplay(Display.NONE); } public void renderStatus(StatusMessage message) { this.statusMessage = message; getElement().getStyle().setVisibility(Visibility.VISIBLE); longText.getStyle().setDisplay(CSSStyleDeclaration.Display.NONE); final JsoArray<StatusAction> statusActions = message.getActions(); actionsContainer.getStyle().setDisplay( statusActions.size() > 0 ? Display.BLOCK : Display.NONE); actionsContainer.setInnerHTML(""); for (int i = 0, n = statusActions.size(); i < n; i++) { final StatusAction statusAction = statusActions.get(i); SpanElement action = Elements.createSpanElement(css.action()); statusAction.renderAction(action); action.setOnClick(new EventListener() { @Override public void handleEvent(Event evt) { statusAction.onAction(); } }); actionsContainer.appendChild(action); } statusText.setTextContent(message.getText()); // Render a countdown if the message is going to expire. if (message.getTimeToExpiry() > 0) { final StatusMessage msg = message; Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() { @Override public boolean execute() { if (msg == StatusPresenter.View.this.statusMessage) { int seconds = msg.getTimeToExpiry() / 1000; statusText.setTextContent(msg.getText() + " ..." + seconds); return seconds > 0; } else { return false; } } }, 1000); } if (!message.getLongText().isEmpty()) { longText.setTextContent(message.getLongText()); more.getStyle().setDisplay(Display.INLINE_BLOCK); } else { more.getStyle().setDisplay(Display.NONE); } if (message.isDismissable()) { statusDismiss.getStyle().setVisibility(Visibility.VISIBLE); } else { statusDismiss.getStyle().setVisibility(Visibility.HIDDEN); } if (message.getType() != MessageType.FATAL) { // Size and center the message dynamicallyPositionMessage(); } else { // Fatal messages are 100% width. getElement().getStyle().setWidth(100, CSSStyleDeclaration.Unit.PCT); getElement().getStyle().setMarginLeft(0, CSSStyleDeclaration.Unit.PX); } // Render the particular message type's style DebugAttributeSetter debugSetter = new DebugAttributeSetter(); switch (message.getType()) { case LOADING: debugSetter.add("status", "loading"); getElement().setClassName(css.statusLoading()); break; case CONFIRMATION: debugSetter.add("status", "confirmation"); getElement().setClassName(css.statusConfirmation()); break; case ERROR: debugSetter.add("status", "error"); getElement().setClassName(css.statusError()); break; case FATAL: debugSetter.add("status", "fatal"); getElement().setClassName(css.fatal()); getDelegate().onStatusExpanded(); break; default: debugSetter.add("status", "unknown"); Log.error(getClass(), "Got a status message of unknown type " + message.getType()); } debugSetter.on(getElement()); } private Element createDom() { DivElement root = Elements.createDivElement(css.statusArea()); statusText = Elements.createDivElement(css.statusText()); statusDismiss = Elements.createDivElement(css.statusDismiss()); statusDismiss.setOnClick(new EventListener() { @Override public void handleEvent(Event evt) { getDelegate().onStatusDismissed(); } }); new DebugAttributeSetter().setId(DebugId.STATUS_PRESENTER).on(root); longText = Elements.createPreElement(); longText.setClassName(css.longText()); longText.getStyle().setDisplay(CSSStyleDeclaration.Display.NONE); more = Elements.createSpanElement(); more.setClassName(css.more()); more.setTextContent("show more details..."); more.setOnClick(new EventListener() { @Override public void handleEvent(Event evt) { getDelegate().onStatusExpanded(); } }); actionsContainer = Elements.createDivElement(); actionsContainer.setClassName(css.actionsContainer()); fatalImage = Elements.createDivElement(css.fatalImage()); root.appendChild(fatalImage); root.appendChild(statusText); root.appendChild(more); root.appendChild(actionsContainer); root.appendChild(statusDismiss); root.appendChild(longText); return root; } /** * Size the message dynamically to scale with the text but not taking up * more than 50% of the screen width. */ private void dynamicallyPositionMessage() { int messageWidth = statusText.getScrollWidth() + actionsContainer.getScrollWidth() + more.getScrollWidth() + statusDismiss.getScrollWidth() + PADDING_WIDTH; int maxWidth = Elements.getBody().getClientWidth() / 2; int width = CssUtils.isVisible(longText) ? maxWidth : Math.min(messageWidth, maxWidth); getElement().getStyle().setWidth(width, CSSStyleDeclaration.Unit.PX); getElement().getStyle().setMarginLeft(width / -2, CSSStyleDeclaration.Unit.PX); } } public interface ViewEvents { void onStatusDismissed(); void onStatusExpanded(); } public static StatusPresenter create(Resources resources) { View view = new View(resources); return new StatusPresenter(view); } private StatusMessage statusMessage; protected StatusPresenter(View view) { super(view); handleViewEvents(); } @Override public void clear() { getView().clear(); statusMessage = null; } @Override public void onStatusMessage(StatusMessage msg) { statusMessage = msg; getView().renderStatus(msg); } private void handleViewEvents() { getView().setDelegate(new ViewEvents() { @Override public void onStatusDismissed() { if (statusMessage != null) { statusMessage.cancel(); } } @Override public void onStatusExpanded() { getView().more.getStyle().setDisplay(CSSStyleDeclaration.Display.NONE); getView().longText.getStyle().setDisplay(CSSStyleDeclaration.Display.BLOCK); if (statusMessage.getType() != MessageType.FATAL) { getView().getElement().addClassName(getView().css.expandedRegular()); getView().dynamicallyPositionMessage(); } else { getView().getElement().addClassName(getView().css.expandedFatal()); } } }); } }