/** * 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.widget.toolbar; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.StyleInjector; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.resources.client.ImageResource.ImageOptions; import com.google.gwt.resources.client.ImageResource.RepeatStyle; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import org.waveprotocol.wave.client.widget.overflowpanel.OverflowPanelUpdater; import org.waveprotocol.wave.client.widget.overflowpanel.OverflowPanelUpdater.OverflowPanel; import org.waveprotocol.wave.client.widget.toolbar.buttons.AbstractToolbarButton; import org.waveprotocol.wave.client.widget.toolbar.buttons.HorizontalToolbarButtonWidget; import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarButtonUiProxy; import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarButtonView; import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarButtonView.State; import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarClickButton; import org.waveprotocol.wave.client.widget.toolbar.buttons.ToolbarToggleButton; import org.waveprotocol.wave.client.widget.toolbar.buttons.VerticalToolbarButtonWidget; import org.waveprotocol.wave.model.util.CollectionUtils; import java.util.List; /** * A {@link ToolbarView} to be used as a top-level widget. * * @author kalman@google.com (Benjamin Kalman) */ public final class ToplevelToolbarWidget extends Composite implements GroupingToolbar.View, OverflowPanel, SubmenuItem.Parent { interface Resources extends ClientBundle { @Source("ToplevelToolbarWidget.css") Css css(); @Source("button_fill.png") @ImageOptions(repeatStyle = RepeatStyle.Horizontal) ImageResource fillImage(); @Source("toolbar_more_button.png") ImageResource overflowButtonIcon(); } interface Css extends CssResource { String toolbar(); String overflowButton(); String overflowButtonIcon(); } /** * An item in the toolbar. */ private static final class Item { /** The item's widget in the toplevel toolbar. */ public final HorizontalToolbarButtonWidget onToplevel; /** The item's widget in the overflow toolbar. */ public final VerticalToolbarButtonWidget onOverflow; /** * The proxy of the item, always delegates to either {@link #onToplevel} or * {@link #onOverflow} depending on the placement of the item. */ public final ToolbarButtonUiProxy proxy; /** * The item as an {@link AbstractToolbarButton}, the component of the item * returned to callers that add buttons and submenus. Maintained in order to * change parents as the item overflows (and un-overflows), and to remove * the item. */ public AbstractToolbarButton asAbstractButton = null; public Item(HorizontalToolbarButtonWidget onToplevel, VerticalToolbarButtonWidget onOverflow, ToolbarButtonUiProxy proxy) { this.onToplevel = onToplevel; this.onOverflow = onOverflow; this.proxy = proxy; } } interface Binder extends UiBinder<Widget, ToplevelToolbarWidget> {} private static final Binder BINDER = GWT.create(Binder.class); @UiField(provided = true) static final Resources res = GWT.create(Resources.class); static { StyleInjector.inject(res.css().getText(), true); } @UiField FlowPanel self; @UiField SimplePanel overflowButton; /** The overflow submenu. */ private final SubmenuToolbarWidget overflowSubmenu = new SubmenuToolbarWidget(new ToolbarToggleButton(new HorizontalToolbarButtonWidget())); /** The logic for controlling which items show in the overflow submenu. */ private final OverflowPanelUpdater overflowLogic; /** Items in the menu. */ private final List<Item> items = CollectionUtils.newArrayList(); public ToplevelToolbarWidget() { initWidget(BINDER.createAndBindUi(this)); overflowButton.setWidget(overflowSubmenu.hackGetWidget()); overflowSubmenu.addDebugClass("more"); // Build the "..." icon. Element icon = DOM.createDiv(); icon.setClassName(res.css().overflowButtonIcon()); overflowSubmenu.setVisualElement(icon); overflowSubmenu.setShowDivider(true); // Attach overflow logic. overflowLogic = new OverflowPanelUpdater(this); } // // For vanilla ToolbarView // @Override public ToolbarClickButton addClickButton() { ToolbarClickButton button = insertClickButton(items.size()); showDividerIfNotFirst(button); return button; } @Override public ToolbarToggleButton addToggleButton() { ToolbarToggleButton button = insertToggleButton(items.size()); showDividerIfNotFirst(button); return button; } @Override public SubmenuToolbarView addSubmenu() { SubmenuToolbarView submenu = insertSubmenu(items.size()); showDividerIfNotFirst(submenu); return submenu; } private void showDividerIfNotFirst(ToolbarButtonView button) { if (button.hackGetWidget() != items.get(0).onToplevel) { button.setShowDivider(true); } } @Override public ToolbarView addGroup() { return new GroupingToolbar(this, addFakeItem()); } /** * Adds a fake item that isn't rendered but still has an entry in items * (so that it plays correctly with overflowing, etc). */ private ToolbarButtonView addFakeItem() { // NOTE: simplest way to add a fake item is to add an invisible button. ToolbarButtonView fakeButton = addClickButton(); fakeButton.setState(State.INVISIBLE); return fakeButton; } @Override public void clearItems() { self.clear(); items.clear(); } // // For GroupingToolbar.View // @Override public ToolbarClickButton insertClickButton(int beforeIndex) { Item item = insertItem(beforeIndex); ToolbarClickButton button = new ToolbarClickButton(item.proxy); item.asAbstractButton = button; return button; } @Override public ToolbarToggleButton insertToggleButton(int beforeIndex) { Item item = insertItem(beforeIndex); ToolbarToggleButton button = new ToolbarToggleButton(item.proxy); item.asAbstractButton = button; return button; } @Override public SubmenuToolbarView insertSubmenu(int beforeIndex) { Item item = insertItem(beforeIndex); SubmenuToolbarWidget submenu = new SubmenuToolbarWidget(new ToolbarToggleButton(item.proxy)); submenu.setShowDropdownArrow(true); item.asAbstractButton = submenu; return submenu; } @Override public int indexOf(ToolbarButtonView button) { for (int i = 0; i < items.size(); i++) { if (items.get(i).asAbstractButton == button) { return i; } } throw new IllegalArgumentException("button is not in this toolbar"); } /** * Adds a new item to the toolbar, handling its placement in both the toplevel * and overflow toolbars. */ private Item insertItem(int beforeIndex) { // The widget for the toplevel toolbar. HorizontalToolbarButtonWidget toplevelButton = new HorizontalToolbarButtonWidget(); self.insert(toplevelButton, beforeIndex); overflowLogic.updateStateEventually(); // The widget for the overflow toolbar. Construct manually and use // hackAddWidget so that the ToolbarButtonViewProxy can manage the state // of the submenus correctly. VerticalToolbarButtonWidget overflowButton = new VerticalToolbarButtonWidget(); overflowSubmenu.hackInsertWidget(overflowButton, beforeIndex); // Return the item, initially proxying the toplevel button. Item item = new Item(toplevelButton, overflowButton, new ToolbarButtonUiProxy(toplevelButton)); items.add(beforeIndex, item); return item; } // // Resizing // public void onResizeDone() { overflowLogic.updateStateEventually(); } // // OverflowPanel // @Override public boolean hasOverflowed(int index) { return items.get(index).onToplevel.getElement().getOffsetTop() > 0; } @Override public boolean isVisible(int index) { return items.get(index).proxy.hackGetState() != State.INVISIBLE; } @Override public void moveToOverflowBucket(int index) { Item item = items.get(index); // The item is in the overflow menu now, so parent is the overflow submenu. item.asAbstractButton.setParent(overflowSubmenu); // ... and so is the proxy. item.proxy.setDelegate(item.onOverflow); // Even though there is overflow: hidden, explicitly hide the toplevel // buttons so that fast resize events don't look strange (e.g. wrong icon // placement). item.onToplevel.setState(State.INVISIBLE); // Force a state change event to possibly enable the overflow submenu. overflowSubmenu.onChildStateChanged(item.asAbstractButton, item.proxy.hackGetState()); } @Override public void onBeginOverflowLayout() { // Reset all the items to the toplevel and hide the overflow submenu; // very difficult to calculate overflow without a consistent state. for (Item item : items) { // The item is in the toplevel now, so the toplevel is the parent. item.asAbstractButton.setParent(this); // ... and the proxy is on the toplevel. item.proxy.setDelegate(item.onToplevel); // Item now invisible on the overflow menu. This is done to all // items of course, so by the end the overflow submenu will think that // all buttons are disabled. item.onOverflow.setState(State.INVISIBLE); } overflowSubmenu.setState(State.INVISIBLE); } @Override public void onEndOverflowLayout() { // Hide the divider of the first overflowed button, if any. Since this is // set on the button itself and not the proxy, the divider state will be // reset to the correct value next time there is an overflow. for (Item item : items) { if (item.proxy.getDelegate() == item.onOverflow) { item.onOverflow.setShowDivider(false); // TODO(kalman): Don't hide all the dividers. //break; } } } @Override public void showMoreButton() { // This is called before moving any items to the overflow panel; doing so // will result in the submenu enabling itself so long as the button states // are kept up to date. So, for now, just make it visible and disabled. overflowSubmenu.setState(State.DISABLED); } @Override public int getWidgetCount() { return items.size(); } // // SubmenuItem.Parent // @Override public void onChildStateChanged(SubmenuItem item, State newState) { overflowLogic.updateStateEventually(); } @Override public void onActionPerformed() { } }