View Javadoc
1   /**
2    * This file Copyright (c) 2010-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.gwt.client.tabsheet.widget;
35  
36  import info.magnolia.ui.vaadin.gwt.client.tabsheet.event.ActiveTabChangedEvent;
37  import info.magnolia.ui.vaadin.gwt.client.tabsheet.event.ShowAllTabsEvent;
38  import info.magnolia.ui.vaadin.gwt.client.tabsheet.event.ShowAllTabsHandler;
39  import info.magnolia.ui.vaadin.gwt.client.tabsheet.event.TabCloseEvent;
40  import info.magnolia.ui.vaadin.gwt.client.tabsheet.event.TabCloseEventHandler;
41  import info.magnolia.ui.vaadin.gwt.client.tabsheet.tab.widget.MagnoliaTabLabel;
42  import info.magnolia.ui.vaadin.gwt.client.tabsheet.tab.widget.MagnoliaTabWidget;
43  import info.magnolia.ui.vaadin.gwt.client.tabsheet.util.CollectionUtil;
44  
45  import java.util.Collections;
46  import java.util.HashMap;
47  import java.util.Iterator;
48  import java.util.LinkedList;
49  import java.util.List;
50  import java.util.Map;
51  
52  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
53  import com.google.gwt.dom.client.NativeEvent;
54  import com.google.gwt.event.dom.client.ClickEvent;
55  import com.google.gwt.event.dom.client.ClickHandler;
56  import com.google.gwt.user.client.DOM;
57  import com.google.gwt.user.client.Element;
58  import com.google.gwt.user.client.ui.ComplexPanel;
59  import com.google.gwt.user.client.ui.DialogBox;
60  import com.google.gwt.user.client.ui.MenuBar;
61  import com.google.gwt.user.client.ui.MenuItem;
62  import com.google.gwt.user.client.ui.SimplePanel;
63  import com.google.gwt.user.client.ui.Widget;
64  import com.google.web.bindery.event.shared.EventBus;
65  import com.vaadin.client.ui.VButton;
66  
67  /**
68   * A bar that contains the tab labels and controls the switching between tabs.
69   */
70  public class TabBarWidget extends ComplexPanel {
71  
72      private static final String SINGLE_TAB_CLASS_NAME = "single-tab";
73  
74      private final List<MagnoliaTabLabel> tabLabels = new LinkedList<>();
75  
76      private final Element tabContainer = DOM.createElement("ul");
77  
78      private final EventBus eventBus;
79  
80      private VShellShowAllTabLabel showAllTab;
81  
82      private final HiddenTabsPopup hiddenTabsPopup;
83  
84      private MagnoliaTabLabel activeTab;
85  
86      public TabBarWidget(EventBus eventBus) {
87          this.eventBus = eventBus;
88          setElement(tabContainer);
89          setStyleName("nav");
90          addStyleDependentName("tabs");
91  
92          hiddenTabsPopup = new HiddenTabsPopup(eventBus);
93          add(hiddenTabsPopup, tabContainer);
94          bindHandlers();
95      }
96  
97      public void reArrangeTabVisibility() {
98          if (tabLabels.isEmpty()) {
99              return;
100         }
101         // Is this ever possible?
102         if (activeTab == null) {
103             activeTab = tabLabels.get(0);
104         }
105 
106         // Reset hidden-tabs popup
107         hiddenTabsPopup.setVisible(true);
108         int toggleWidth = hiddenTabsPopup.getOffsetWidth();
109         int availableWidth = tabContainer.getOffsetWidth();
110 
111         // 1. Active tab is always visible, count its width first
112         activeTab.setVisible(true);
113         availableWidth -= activeTab.getOffsetWidth();
114 
115         // 2. Account for show-all tab if needed
116         if (showAllTab != null) {
117             availableWidth -= showAllTab.getOffsetWidth();
118         }
119 
120         // Collect every tab's width (we'll have to know anyway)
121         Map<MagnoliaTabLabel, Integer> tabWidths = new HashMap<>();
122         for (MagnoliaTabLabel tab : tabLabels) {
123             if (tab == activeTab) {
124                 continue;
125             }
126             tab.setVisible(true);
127             tabWidths.put(tab, tab.getOffsetWidth());
128         }
129 
130         // 3. Squeeze all other tabs from start to end, as many as width allows
131         Iterator<MagnoliaTabLabel> it = tabLabels.iterator();
132         boolean outOfSpace = false;
133         while (it.hasNext()) {
134             MagnoliaTabLabel tab = it.next();
135             // Active tab is already accounted for, don't hide it
136             if (tab == activeTab) {
137                 continue;
138             }
139             if (!outOfSpace) {
140                 int width = tabWidths.get(tab);
141                 int maxWidth = Collections.max(tabWidths.values());
142                 // Either I have enough space for the next tab and it's the last one
143                 if ((!it.hasNext() && availableWidth >= width)
144                         // Either I need enough space for the widest of the next tabs, plus the toggle
145                         // Why widest? The tab bar may have to accommodate it, not necessarily now, but maybe when switching.
146                         || (maxWidth + toggleWidth <= availableWidth)) {
147                     tabWidths.remove(tab);
148                     availableWidth -= width;
149                     continue;
150                 } else {
151                     outOfSpace = true;
152                 }
153             }
154             // If we're there we've run out of space
155             tab.setVisible(false);
156         }
157 
158         // 4. Compute content of the hidden tabs popup
159         hiddenTabsPopup.hide();
160         hiddenTabsPopup.menubar.clearItems();
161         it = tabLabels.iterator();
162         while (it.hasNext()) {
163             MagnoliaTabLabel tab = it.next();
164             if (!tab.isVisible()) {
165                 hiddenTabsPopup.addTabLabel(tab);
166             }
167         }
168         hiddenTabsPopup.showControlIfNeeded();
169     }
170 
171     private void bindHandlers() {
172         eventBus.addHandler(ActiveTabChangedEvent.TYPE, new ActiveTabChangedEvent.Handler() {
173             @Override
174             public void onActiveTabChanged(final ActiveTabChangedEvent event) {
175                 final MagnoliaTabWidget tab = event.getTab();
176                 final MagnoliaTabLabel label = tab.getLabel();
177                 if (label != null) {
178                     for (final MagnoliaTabLabel tabLabel : tabLabels) {
179                         tabLabel.removeStyleName("active");
180                     }
181                     label.addStyleName("active");
182                     showAll(false);
183                     activeTab = label;
184                     hiddenTabsPopup.menuWrapper.hide();
185                     if (!label.isVisible()) {
186                         reArrangeTabVisibility();
187                     }
188                 }
189             }
190         });
191 
192         eventBus.addHandler(TabCloseEvent.TYPE, new TabCloseEventHandler() {
193             @Override
194             public void onTabClosed(TabCloseEvent event) {
195                 final MagnoliaTabLabel tabLabel = event.getTab().getLabel();
196                 boolean wasActive = tabLabel.getStyleName().contains("active");
197                 if (wasActive) {
198                     final MagnoliaTabLabel nextLabel = getNextLabel(tabLabel);
199                     if (nextLabel != null) {
200                         nextLabel.addStyleName("active");
201                         activeTab = nextLabel;
202                     }
203                 }
204                 tabLabels.remove(tabLabel);
205                 remove(tabLabel);
206                 updateSingleTabStyle();
207                 reArrangeTabVisibility();
208             }
209         });
210 
211         eventBus.addHandler(ShowAllTabsEvent.TYPE, new ShowAllTabsHandler() {
212 
213             @Override
214             public void onShowAllTabs(ShowAllTabsEvent event) {
215                 for (final MagnoliaTabLabel tabLabel : tabLabels) {
216                     tabLabel.removeStyleName("active");
217                 }
218                 showAll(true);
219             }
220         });
221     }
222 
223     protected MagnoliaTabLabel getNextLabel(final MagnoliaTabLabel label) {
224         return CollectionUtil.getNext(tabLabels, label);
225     }
226 
227     public void addTabLabel(MagnoliaTabLabel label) {
228         label.setEventBus(eventBus);
229         if (!tabLabels.contains(label)) {
230             tabLabels.add(label);
231             // Keep hidden-tabs toggle and show-all button last in the DOM when inserting labels.
232             insert(label, tabContainer, tabLabels.size() - 1, true);
233             updateSingleTabStyle();
234         }
235     }
236 
237     public void updateSingleTabStyle() {
238         if (tabLabels.size() <= 1) {
239             tabContainer.addClassName(SINGLE_TAB_CLASS_NAME);
240         } else {
241             tabContainer.removeClassName(SINGLE_TAB_CLASS_NAME);
242         }
243     }
244 
245     public void addShowAllTab(boolean showAll, String label) {
246         if (showAll && showAllTab == null) {
247             showAllTab = new VShellShowAllTabLabel(label);
248             add(showAllTab, getElement());
249         } else if (!showAll && showAllTab != null) {
250             remove(showAllTab);
251             showAllTab = null;
252         }
253         reArrangeTabVisibility();
254     }
255 
256     private class VShellShowAllTabLabel extends SimplePanel {
257 
258         private final VButton textWrapper = new VButton();
259 
260         public VShellShowAllTabLabel(String label) {
261             super(DOM.createElement("li"));
262             addStyleName("show-all");
263             textWrapper.getElement().setInnerHTML(label);
264             textWrapper.getElement().setClassName("tab-title");
265             this.add(textWrapper);
266         }
267 
268         @Override
269         protected void onLoad() {
270             super.onLoad();
271             bindHandlers();
272         }
273 
274         private void bindHandlers() {
275             addDomHandler(new ClickHandler() {
276                 @Override
277                 public void onClick(ClickEvent event) {
278                     onClickGeneric(event.getNativeEvent());
279                 }
280             }, ClickEvent.getType());
281 
282             textWrapper.addClickHandler(new ClickHandler() {
283                 @Override
284                 public void onClick(ClickEvent event) {
285                     textWrapper.setFocus(false);
286                     onClickGeneric(event.getNativeEvent());
287                 }
288             });
289         }
290 
291         private void onClickGeneric(NativeEvent nativeEvent) {
292             eventBus.fireEvent(new ShowAllTabsEvent());
293             nativeEvent.stopPropagation();
294         }
295 
296     }
297 
298     /**
299      * The toggle display the hidden-tabs popup upon click.
300      */
301     static class HiddenTabsPopup extends Widget {
302 
303         private final DialogBox menuWrapper = new DialogBox(true);
304         private final HiddenTabsMenuBar menubar = new HiddenTabsMenuBar();
305         private final EventBus eventBus;
306 
307         public HiddenTabsPopup(EventBus eventBus) {
308             this.eventBus = eventBus;
309 
310             setElement(DOM.createElement("li"));
311             addStyleName("icon-arrow2_e");
312             addStyleName("hidden-tabs-popup-button");
313             menuWrapper.add(menubar);
314             menuWrapper.setStyleName("context-menu-wrapper");
315 
316             // Initially hide the component
317             setVisible(false);
318         }
319 
320         @Override
321         protected void onLoad() {
322             super.onLoad();
323             bindHandlers();
324         }
325 
326         public void addTabLabel(final MagnoliaTabLabel label) {
327             final MenuItem item = menubar.addItem(label.getCaption(), new ScheduledCommand() {
328                 @Override
329                 public void execute() {
330                     menuWrapper.hide();
331                     eventBus.fireEvent(new ActiveTabChangedEvent(label.getTab()));
332                 }
333             });
334             item.addStyleName("menu-item");
335         }
336 
337         private void bindHandlers() {
338             addDomHandler(new ClickHandler() {
339                 @Override
340                 public void onClick(ClickEvent event) {
341                     event.preventDefault();
342                     event.stopPropagation();
343                     menuWrapper.setPopupPosition(getAbsoluteLeft() + getOffsetWidth(), getAbsoluteTop());
344                     menuWrapper.show();
345                 }
346 
347             }, ClickEvent.getType());
348         }
349 
350         public void hide() {
351             menuWrapper.hide();
352             setVisible(false);
353         }
354 
355         public void showControlIfNeeded() {
356             setVisible(!menubar.isEmpty());
357         }
358 
359         private class HiddenTabsMenuBar extends MenuBar {
360 
361             public HiddenTabsMenuBar() {
362                 super(true);
363                 setStyleName("context-menu");
364                 addStyleName("hidden-tabs-menu");
365             }
366 
367             public boolean isEmpty() {
368                 return super.getItems().isEmpty();
369             }
370         }
371     }
372 
373     public void showAll(boolean showAll) {
374         if (showAllTab != null) {
375             if (showAll) {
376                 showAllTab.addStyleName("active");
377             } else {
378                 if (showAllTab.getStyleName().contains("active")) {
379                     showAllTab.removeStyleName("active");
380                 }
381             }
382         }
383     }
384 
385 }