/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * 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 org.uberfire.client.views.pfly.tab; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.enterprise.context.Dependent; import javax.inject.Inject; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.BeforeSelectionEvent; import com.google.gwt.event.logical.shared.BeforeSelectionHandler; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.ResizeComposite; import com.google.gwt.user.client.ui.Widget; import org.gwtbootstrap3.client.shared.event.TabShowEvent; import org.gwtbootstrap3.client.shared.event.TabShowHandler; import org.gwtbootstrap3.client.shared.event.TabShownEvent; import org.gwtbootstrap3.client.shared.event.TabShownHandler; import org.uberfire.client.mvp.PlaceManager; import org.uberfire.client.resources.WorkbenchResources; import org.uberfire.client.views.pfly.tab.TabPanelWithDropdowns.DropDownTab; import org.uberfire.client.workbench.panels.MultiPartWidget; import org.uberfire.client.workbench.panels.WorkbenchPanelPresenter; import org.uberfire.client.workbench.part.WorkbenchPartPresenter; import org.uberfire.client.workbench.part.WorkbenchPartPresenter.View; import org.uberfire.client.workbench.widgets.dnd.WorkbenchDragAndDropManager; import org.uberfire.mvp.Command; import org.uberfire.workbench.model.PartDefinition; import static org.uberfire.commons.validation.PortablePreconditions.checkNotNull; /** * A wrapper around {@link TabPanelWithDropdowns} that adds the following capabilities: * <ul> * <li>Tabs that don't fit in the tab bar are automatically collapsed into a dropdown * <li>Each tab gets a close button * <li>Obeys the RequiresResize/ProvidesResize contract (onResize() calls are propagated * to the visible tab content widgets) * <li>Participates in UberFire's panel focus system * </ul> */ @Dependent public class UberTabPanel extends ResizeComposite implements MultiPartWidget, ClickHandler { private static final int MARGIN = 20; final List<WorkbenchPartPresenter> parts = new ArrayList<WorkbenchPartPresenter>(); final Map<WorkbenchPartPresenter.View, TabPanelEntry> tabIndex = new HashMap<WorkbenchPartPresenter.View, TabPanelEntry>(); final Map<TabPanelEntry, WorkbenchPartPresenter.View> tabInvertedIndex = new HashMap<TabPanelEntry, WorkbenchPartPresenter.View>(); final Map<PartDefinition, TabPanelEntry> partTabIndex = new HashMap<PartDefinition, TabPanelEntry>(); private final List<Command> focusGainedHandlers = new ArrayList<Command>(); WorkbenchDragAndDropManager dndManager; private ResizeTabPanel tabPanel; private DropDownTab dropdownTab; /** * Flag protecting {@link #updateDisplayedTabs()} from recursively invoking itself through events that it causes. */ private boolean updating; private boolean hasFocus = false; private PlaceManager panelManager; @Inject public UberTabPanel(final PlaceManager panelManager, final @Resize ResizeTabPanel tabPanel) { this.panelManager = checkNotNull("panelManager", panelManager); this.tabPanel = checkNotNull("tabPanel", tabPanel); } @PostConstruct public void init() { this.dropdownTab = tabPanel.addDropdownTab("More..."); tabPanel.addShowHandler(new TabShowHandler() { @Override public void onShow(TabShowEvent e) { if (e.getTab() != null) { final TabPanelEntry selected = tabPanel.findEntryForTabWidget(e.getTab()); BeforeSelectionEvent .fire(UberTabPanel.this, tabInvertedIndex.get(selected).getPresenter().getDefinition()); } } }); tabPanel.addShownHandler(new TabShownHandler() { @Override public void onShown(TabShownEvent e) { onResize(); if (e.getTab() != null) { final TabPanelEntry selected = tabPanel.findEntryForTabWidget(e.getTab()); SelectionEvent .fire(UberTabPanel.this, tabInvertedIndex.get(selected).getPresenter().getDefinition()); } } }); tabPanel.addDomHandler(UberTabPanel.this, ClickEvent.getType()); initWidget(tabPanel); } @Override public void clear() { parts.clear(); tabPanel.clear(); dropdownTab.clear(); partTabIndex.clear(); tabIndex.clear(); tabInvertedIndex.clear(); } /** * Updates the {@link #tabPanel} to contain a tab for each part in {@link #parts} in the order the parts */ private void updateDisplayedTabs() { if (updating) { return; } try { updating = true; tabPanel.clear(); dropdownTab.clear(); if (parts.size() == 0) { return; } int availableSpace = tabPanel.getOffsetWidth(); TabPanelEntry selectedTab = null; // the number of regular (not dropdown) tabs in the tab bar int regularTabCount = 0; // add and measure all tabs for (int i = 0; i < parts.size(); i++) { WorkbenchPartPresenter part = parts.get(i); TabPanelEntry tabPanelEntry = partTabIndex.get(part.getDefinition()); if (tabPanelEntry.isActive()) { selectedTab = tabPanelEntry; } tabPanelEntry.setActive(false); tabPanel.addItem(tabPanelEntry); regularTabCount++; availableSpace -= tabPanelEntry.getTabWidget().getOffsetWidth(); } // if we didn't find any selected tab, let's select the first one if (selectedTab == null) { TabPanelEntry firstTab = getTab(0); selectedTab = firstTab; } // now work from right to left to find out how many tabs we have to collapse into the dropdown if (availableSpace < 0) { LinkedList<TabPanelEntry> newDropdownContents = new LinkedList<TabPanelEntry>(); dropdownTab.setText("More..."); tabPanel.addDropdownTab(dropdownTab); while (availableSpace - dropdownTab.getTabWidth() < 0 && regularTabCount > 1) { // get the last tab that isn't the dropdown tab TabPanelEntry tab = getTab(--regularTabCount); availableSpace += tab.getTabWidget().getOffsetWidth(); tabPanel.remove(tab); newDropdownContents.addFirst(tab); if (tab == selectedTab) { dropdownTab.setText(selectedTab.getTitle()); } } for (TabPanelEntry l : newDropdownContents) { dropdownTab.addItem(l); } } selectedTab.showTab(); } finally { updating = false; } } private TabPanelEntry getTab(int i) { return checkNotNull("part entry in map", partTabIndex.get(parts.get(i).getDefinition())); } @Override public boolean selectPart(final PartDefinition id) { final TabPanelEntry tab = partTabIndex.get(id); if (tab != null) { tab.showTab(); } return false; } @Override public boolean remove(final PartDefinition id) { final TabPanelEntry tab = partTabIndex.get(id); if (tab == null) { return false; } final boolean wasActive = tab.isActive(); View partView = tabInvertedIndex.remove(tab); int removedTabIndex = parts.indexOf(partView.getPresenter()); parts.remove(removedTabIndex); tabIndex.remove(partView); partTabIndex.remove(id); updateDisplayedTabs(); if (removedTabIndex >= 0 && wasActive && parts.size() > 0) { selectPart(parts.get(removedTabIndex <= 0 ? 0 : removedTabIndex - 1).getDefinition()); } return true; } @Override public void changeTitle(final PartDefinition id, final String title, final IsWidget titleDecoration) { final TabPanelEntry tab = partTabIndex.get(id); if (tab != null) { tab.setTitle(title); } } @Override public HandlerRegistration addBeforeSelectionHandler(final BeforeSelectionHandler<PartDefinition> handler) { return addHandler(handler, BeforeSelectionEvent.getType()); } @Override public HandlerRegistration addSelectionHandler(final SelectionHandler<PartDefinition> handler) { return addHandler(handler, SelectionEvent.getType()); } @Override public void setPresenter(final WorkbenchPanelPresenter presenter) { // not needed } @Override public void addPart(final WorkbenchPartPresenter.View view) { if (!tabIndex.containsKey(view)) { final TabPanelEntry tab = tabPanel.addItem(view.getPresenter().getTitle(), view.asWidget()); resizeIfNeeded(view.asWidget()); tabIndex.put(view, tab); tabInvertedIndex.put(tab, view); partTabIndex.put(view.getPresenter().getDefinition(), tab); dndManager.makeDraggable(view, tab.getTabWidget()); addCloseToTab(tab); parts.add(view.getPresenter()); tabIndex.put(view, tab); updateDisplayedTabs(); } } /** * The GwtBootstrap3 TabPanel doesn't support the RequiresResize/ProvidesResize contract, and UberTabPanel fills in * the gap. This helper method allows us to call onResize() on the widgets that need it. * @param widget the widget that has just been resized */ private void resizeIfNeeded(final Widget widget) { if (isAttached() && widget instanceof RequiresResize) { ((RequiresResize) widget).onResize(); } } @Override public void onResize() { updateDisplayedTabs(); tabPanel.onResize(); } private void addCloseToTab(final TabPanelEntry tab) { final Button close = new Button("×"); close.setStyleName("close"); close.addStyleName(WorkbenchResources.INSTANCE.CSS().tabCloseButton()); close.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { final WorkbenchPartPresenter.View partToDeselect = tabInvertedIndex.get(tab); panelManager.closePlace(partToDeselect.getPresenter().getDefinition().getPlace()); } }); tab.getTabWidget().addToAnchor(close); } @Override public void setDndManager(final WorkbenchDragAndDropManager dndManager) { this.dndManager = dndManager; } @Override public void setFocus(final boolean hasFocus) { this.hasFocus = hasFocus; tabPanel.setFocus(hasFocus); } @Override public void onClick(final ClickEvent event) { if (!hasFocus) { fireFocusGained(); View view = getSelectedPart(); if (view != null) { SelectionEvent.fire(UberTabPanel.this, view.getPresenter().getDefinition()); } } } private View getSelectedPart() { return tabInvertedIndex.get(tabPanel.getActiveTab()); } private void fireFocusGained() { for (int i = focusGainedHandlers.size() - 1; i >= 0; i--) { focusGainedHandlers.get(i).execute(); } } @Override public void addOnFocusHandler(final Command doWhenFocused) { focusGainedHandlers.add(checkNotNull("doWhenFocused", doWhenFocused)); } @Override public int getPartsSize() { return partTabIndex.size(); } @Override public Collection<PartDefinition> getParts() { return Collections.unmodifiableSet(partTabIndex.keySet()); } }