View Javadoc
1   /**
2    * This file Copyright (c) 2010-2018 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                 if (event.getTab() == null) return;
176                 final MagnoliaTabWidget tab = event.getTab();
177                 final MagnoliaTabLabel label = tab.getLabel();
178                 if (label != null) {
179                     for (final MagnoliaTabLabel tabLabel : tabLabels) {
180                         tabLabel.removeStyleName("active");
181                     }
182                     label.addStyleName("active");
183                     showAll(false);
184                     activeTab = label;
185                     hiddenTabsPopup.menuWrapper.hide();
186                     if (!label.isVisible()) {
187                         reArrangeTabVisibility();
188                     }
189                 }
190             }
191         });
192 
193         eventBus.addHandler(TabCloseEvent.TYPE, new TabCloseEventHandler() {
194             @Override
195             public void onTabClosed(TabCloseEvent event) {
196                 final MagnoliaTabLabel tabLabel = event.getTab().getLabel();
197                 boolean wasActive = tabLabel.getStyleName().contains("active");
198                 if (wasActive) {
199                     final MagnoliaTabLabel nextLabel = getNextLabel(tabLabel);
200                     if (nextLabel != null) {
201                         nextLabel.addStyleName("active");
202                         activeTab = nextLabel;
203                     }
204                 }
205                 tabLabels.remove(tabLabel);
206                 remove(tabLabel);
207                 updateSingleTabStyle();
208                 reArrangeTabVisibility();
209             }
210         });
211 
212         eventBus.addHandler(ShowAllTabsEvent.TYPE, new ShowAllTabsHandler() {
213 
214             @Override
215             public void onShowAllTabs(ShowAllTabsEvent event) {
216                 for (final MagnoliaTabLabel tabLabel : tabLabels) {
217                     tabLabel.removeStyleName("active");
218                 }
219                 showAll(true);
220             }
221         });
222     }
223 
224     protected MagnoliaTabLabel getNextLabel(final MagnoliaTabLabel label) {
225         return CollectionUtil.getNext(tabLabels, label);
226     }
227 
228     public void addTabLabel(MagnoliaTabLabel label) {
229         label.setEventBus(eventBus);
230         if (!tabLabels.contains(label)) {
231             tabLabels.add(label);
232             // Keep hidden-tabs toggle and show-all button last in the DOM when inserting labels.
233             insert(label, tabContainer, tabLabels.size() - 1, true);
234             updateSingleTabStyle();
235         }
236     }
237 
238     public void updateSingleTabStyle() {
239         if (tabLabels.size() <= 1) {
240             tabContainer.addClassName(SINGLE_TAB_CLASS_NAME);
241         } else {
242             tabContainer.removeClassName(SINGLE_TAB_CLASS_NAME);
243         }
244     }
245 
246     public void addShowAllTab(boolean showAll, String label) {
247         if (showAll && showAllTab == null) {
248             showAllTab = new VShellShowAllTabLabel(label);
249             add(showAllTab, getElement());
250         } else if (!showAll && showAllTab != null) {
251             remove(showAllTab);
252             showAllTab = null;
253         }
254         reArrangeTabVisibility();
255     }
256 
257     private class VShellShowAllTabLabel extends SimplePanel {
258 
259         private final VButton textWrapper = new VButton();
260 
261         public VShellShowAllTabLabel(String label) {
262             super(DOM.createElement("li"));
263             addStyleName("show-all");
264             textWrapper.getElement().setInnerHTML(label);
265             textWrapper.getElement().setClassName("tab-title");
266             this.add(textWrapper);
267         }
268 
269         @Override
270         protected void onLoad() {
271             super.onLoad();
272             bindHandlers();
273         }
274 
275         private void bindHandlers() {
276             addDomHandler(new ClickHandler() {
277                 @Override
278                 public void onClick(ClickEvent event) {
279                     onClickGeneric(event.getNativeEvent());
280                 }
281             }, ClickEvent.getType());
282 
283             textWrapper.addClickHandler(new ClickHandler() {
284                 @Override
285                 public void onClick(ClickEvent event) {
286                     textWrapper.setFocus(false);
287                     onClickGeneric(event.getNativeEvent());
288                 }
289             });
290         }
291 
292         private void onClickGeneric(NativeEvent nativeEvent) {
293             eventBus.fireEvent(new ShowAllTabsEvent());
294             nativeEvent.stopPropagation();
295         }
296 
297     }
298 
299     /**
300      * The toggle display the hidden-tabs popup upon click.
301      */
302     static class HiddenTabsPopup extends Widget {
303 
304         private final DialogBox menuWrapper = new DialogBox(true);
305         private final HiddenTabsMenuBar menubar = new HiddenTabsMenuBar();
306         private final EventBus eventBus;
307 
308         public HiddenTabsPopup(EventBus eventBus) {
309             this.eventBus = eventBus;
310 
311             setElement(DOM.createElement("li"));
312             addStyleName("icon-arrow2_e");
313             addStyleName("hidden-tabs-popup-button");
314             menuWrapper.add(menubar);
315             menuWrapper.setStyleName("context-menu-wrapper");
316 
317             // Initially hide the component
318             setVisible(false);
319         }
320 
321         @Override
322         protected void onLoad() {
323             super.onLoad();
324             bindHandlers();
325         }
326 
327         public void addTabLabel(final MagnoliaTabLabel label) {
328             final MenuItem item = menubar.addItem(label.getCaption(), new ScheduledCommand() {
329                 @Override
330                 public void execute() {
331                     menuWrapper.hide();
332                     eventBus.fireEvent(new ActiveTabChangedEvent(label.getTab()));
333                 }
334             });
335             item.addStyleName("menu-item");
336         }
337 
338         private void bindHandlers() {
339             addDomHandler(new ClickHandler() {
340                 @Override
341                 public void onClick(ClickEvent event) {
342                     event.preventDefault();
343                     event.stopPropagation();
344                     menuWrapper.setPopupPosition(getAbsoluteLeft() + getOffsetWidth(), getAbsoluteTop());
345                     menuWrapper.show();
346                 }
347 
348             }, ClickEvent.getType());
349         }
350 
351         public void hide() {
352             menuWrapper.hide();
353             setVisible(false);
354         }
355 
356         public void showControlIfNeeded() {
357             setVisible(!menubar.isEmpty());
358         }
359 
360         private class HiddenTabsMenuBar extends MenuBar {
361 
362             public HiddenTabsMenuBar() {
363                 super(true);
364                 setStyleName("context-menu");
365                 addStyleName("hidden-tabs-menu");
366             }
367 
368             public boolean isEmpty() {
369                 return super.getItems().isEmpty();
370             }
371         }
372     }
373 
374     public void showAll(boolean showAll) {
375         if (showAllTab != null) {
376             if (showAll) {
377                 showAllTab.addStyleName("active");
378             } else {
379                 if (showAllTab.getStyleName().contains("active")) {
380                     showAllTab.removeStyleName("active");
381                 }
382             }
383         }
384     }
385 
386 }