package org.ovirt.engine.ui.common.view;
import org.ovirt.engine.ui.common.presenter.ScrollableTabBarPresenterWidget;
import org.ovirt.engine.ui.common.widget.tab.AbstractCompositeTab;
import org.ovirt.engine.ui.common.widget.tab.RepeatingPushButton;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.Widget;
public class ScrollableTabBarView extends AbstractView implements ScrollableTabBarPresenterWidget.ViewDef {
public interface ViewUiBinder extends UiBinder<Widget, ScrollableTabBarView> {
ViewUiBinder uiBinder = GWT.create(ViewUiBinder.class);
}
/**
* element style name of 'min-width' that gets manipulated in the view.
*/
private static final String MIN_WIDTH_STYLE = "minWidth"; //$NON-NLS-1$
/**
* element style name of 'max-width' that gets manipulated in the view.
*/
private static final String MAX_WIDTH_STYLE = "maxWidth"; //$NON-NLS-1$
/**
* element style name of 'margin-top' that gets manipulated in the view.
*/
private static final String MARGIN_TOP = "marginTop"; //$NON-NLS-1$
/**
* The CSS styles available to be modified inside the view.
*/
interface Style extends CssResource {
/**
* The style associated with each drop down item.
*
* @return The style name as a string regardless of obfuscation level.
*/
String dropdownItem();
/**
* The style of the outer item container.
* @return The style name as a string regardless of obfuscation level.
*/
String dropdownItemContainer();
}
/**
* The style object using the UI-binder.
*/
@UiField
Style style;
/**
* The tab bar.
*/
@UiField
FlowPanel widgetBar;
/**
* The panel that is scrolled.
*/
@UiField
FlowPanel scrollPanel;
/**
* The left scroll button.
*/
@UiField
RepeatingPushButton scrollLeftButton;
/**
* The right scroll button.
*/
@UiField
RepeatingPushButton scrollRightButton;
/**
* The drop-down button for the menu list.
*/
@UiField
PushButton dropdownButton;
/**
* The drop-down menu list.
*/
@UiField
FlowPanel dropdownPanel;
/**
* The pop-up panel containing the drop-down menu list.
*/
@UiField
PopupPanel dropdownPopupPanel;
/**
* The minimum width needed to display all the widgets.
*/
private int widgetMinWidth;
/**
* The number of pixels to scroll when clicking the left/right scroll buttons.
*/
private int scrollDistance;
/**
* Constructor.
*/
public ScrollableTabBarView() {
initWidget(ViewUiBinder.uiBinder.createAndBindUi(this));
configureAutoHidePartners();
asWidget().addAttachHandler(event -> Scheduler.get().scheduleDeferred(() -> {
recalculateSize();
showScrollButtons();
}));
}
/**
* Configure the auto hide partner of the menu drop down button.
*/
private void configureAutoHidePartners() {
dropdownPopupPanel.addAutoHidePartner(dropdownButton.getElement());
}
@Override
public void addTabWidget(IsWidget tabWidget, int index) {
Widget listWidget = copyWidgetAsDropdownItem(tabWidget);
if (listWidget != null) {
widgetBar.insert(tabWidget, index);
dropdownPanel.insert(listWidget, index);
recalculateWidgetBarMinWidth();
showScrollButtons();
}
}
@Override
public void removeTabWidget(IsWidget tabWidget) {
int widgetIndex = widgetBar.getWidgetIndex(tabWidget);
widgetBar.remove(tabWidget);
dropdownPanel.remove(widgetIndex);
recalculateWidgetBarMinWidth();
showScrollButtons();
}
/**
* Copy the passed in widget WITHOUT the event handlers. Then add a click handler to the new widget
* and add the 'dropdownItem' style to them. The new widget is created from the original passed in widget
* without the original style sheet. New style sheets are added to make it look proper in the drop down
* list.
*
* @param widget The original widget, which is unchanged after this call.
* @return The new widget with click handler and dropdownItems style.
*/
private Widget copyWidgetAsDropdownItem(final IsWidget widget) {
HTML newWidget = null;
if (widget != null) {
newWidget = new HTML();
newWidget.setHTML(widget.asWidget().getElement().getString().replaceAll("class=\".*?\"", //$NON-NLS-1$
"class=\"" + style.dropdownItem() + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
newWidget.addClickHandler(event -> {
dropdownPopupPanel.hide();
widget.asWidget().getElement().scrollIntoView();
adjustButtons();
});
newWidget.addStyleName(style.dropdownItemContainer());
}
return newWidget;
}
/**
* Set the scroll distance in pixels.
*
* @param distance
* The distance to scroll.
*/
@Override
public void setScrollDistance(int distance) {
this.scrollDistance = distance;
}
/**
* Calculate the minimum width needed to display all the tabs on the bar. This works even if there are some
* right floating tabs.
*/
private void recalculateWidgetBarMinWidth() {
widgetBar.getElement().getStyle().setProperty(MIN_WIDTH_STYLE, calculateWidgetMinWidthNeeded(), Unit.PX);
}
/**
* Retrieve the cached minimum width needed. This is purely for performance reasons so we don't have to keep
* calculating the needed with based on the width of all the tabs.
* @return The current minimum width needed.
*/
private int getWidgetMinWidthNeeded() {
return widgetMinWidth;
}
/**
* Calculate the actual width needed based on the number and width of the tabs. This iterates over the tabs
* and collects the width of each. This method also caches the value so we don't have to iterate each time.
* The cached value can be retrieved with getWidgetMinWidthNeeded().
* @return The minimum width needed to display all the tabs.
*/
private int calculateWidgetMinWidthNeeded() {
int minWidth = 0;
if (widgetBar.getWidgetCount() > 0) {
for (int i = 0; i < widgetBar.getWidgetCount(); i++) {
Widget widget = widgetBar.getWidget(i);
if (widget.isVisible()) {
minWidth += widget.getElement().getOffsetWidth();
}
}
}
// Add 1 for browsers that don't report width properly.
minWidth++;
// Store this in a variable so we don't have to calculate it all the time.
// This assumes that when resizes/etc happen this gets called to recalculate everything.
widgetMinWidth = minWidth;
return minWidth;
}
/**
* Calculate the maximum width of the scrolling panel (the panel that contains the panel that has the buttons).
* The logic to determine the width of the scrolling panel:
* <ol>
* <li>Determine the width of the buttons. The width is 0 if they are not displayed.</li>
* <li>Determine the width of the panel containing the scrolling panel.</li>
* <li>The width needed is the width of the panel - the width of the buttons</li>
* </ol>
*/
private void recalculateScrollPanelMaxWidth() {
int leftArrowWidth = scrollLeftButton.isVisible() ? scrollLeftButton.getOffsetWidth() : 0;
int rightArrowWidth = scrollRightButton.isVisible() ? scrollRightButton.getOffsetWidth() : 0;
int dropdownWidth = dropdownButton.isVisible() ? dropdownButton.getOffsetWidth() : 0;
int maxScrollPanelWidth = asWidget().getOffsetWidth() - leftArrowWidth - rightArrowWidth - dropdownWidth;
if (maxScrollPanelWidth > 0) {
scrollPanel.getElement().getStyle().setProperty(MAX_WIDTH_STYLE, maxScrollPanelWidth, Unit.PX);
} else {
scrollPanel.getElement().getStyle().clearProperty(MAX_WIDTH_STYLE);
}
}
/**
* Position the scroll buttons to be properly positioned in the container.
*/
private void positionScrollButtons() {
// Calculate how far from the top the button needs to be to be centered.
int marginTop = (asWidget().getOffsetHeight() - scrollLeftButton.getOffsetHeight()) / 2;
scrollLeftButton.getElement().getStyle().setProperty(MARGIN_TOP, marginTop, Unit.PX);
scrollRightButton.getElement().getStyle().setProperty(MARGIN_TOP, marginTop, Unit.PX);
dropdownButton.getElement().getStyle().setProperty(MARGIN_TOP, marginTop, Unit.PX);
}
/**
* Left scroll bar button click handler.
* @param event The click event.
*/
@UiHandler("scrollLeftButton")
void handleLeftClick(ClickEvent event) {
adjustScroll(-scrollDistance);
}
/**
* Right scroll bar button click handler.
* @param event The click event.
*/
@UiHandler("scrollRightButton")
void handleRightClick(ClickEvent event) {
adjustScroll(scrollDistance);
}
/**
* Drop-down button click handler.
* @param event The click event.
*/
@UiHandler("dropdownButton")
void handleDropdownClick(ClickEvent event) {
if (!dropdownPopupPanel.isShowing()) {
for (int i = 0; i < widgetBar.getWidgetCount(); i++) {
if (widgetBar.getWidget(i) instanceof AbstractCompositeTab) {
dropdownPanel.getWidget(i).setVisible(((AbstractCompositeTab) widgetBar.getWidget(i)).isAccessible());
}
}
dropdownPanel.setVisible(true);
dropdownPopupPanel.showRelativeTo(dropdownButton);
} else {
dropdownPanel.setVisible(false);
dropdownPopupPanel.hide();
}
}
@Override
public void showScrollButtons() {
boolean isScrolling = isScrollingNecessary();
scrollRightButton.setVisible(isScrolling);
scrollLeftButton.setVisible(isScrolling);
dropdownButton.setVisible(isScrolling);
if (isScrolling) {
recalculateSize();
positionScrollButtons();
} else {
scrollTo(0);
scrollPanel.getElement().getStyle().setProperty(MAX_WIDTH_STYLE, asWidget().getOffsetWidth(), Unit.PX);
widgetBar.getElement()
.getStyle()
.setProperty(MIN_WIDTH_STYLE, asWidget().getElement().getOffsetWidth(), Unit.PX);
}
}
/**
* Determine if the scroll buttons should be visible.
* @return {@code true} if scrolling is necessary, false otherwise.
*/
private boolean isScrollingNecessary() {
int currentWidth = asWidget().getOffsetWidth();
int minWidth = getWidgetMinWidthNeeded();
return minWidth > 0 && currentWidth > 0 && currentWidth <= minWidth;
}
/**
* Adjust the scroll by a number of pixels. The value can be positive or negative.
* @param adjustment The number of pixels to adjust.
*/
private void adjustScroll(int adjustment) {
scrollTo(scrollPanel.getElement().getScrollLeft() + adjustment);
}
/**
* Scroll to a specific position.
* @param pos The position to scroll to.
*/
private void scrollTo(int pos) {
scrollPanel.getElement().setScrollLeft(pos);
adjustButtons();
}
/**
* Adjust the state of the scroll buttons based on the position of scroll panel. If you can't scroll more to the
* left, disable the left button, if you can't scroll to the right disable the right button.
*/
void adjustButtons() {
if (scrollPanel.getElement().getScrollLeft() <= 0) {
scrollLeftButton.setEnabled(false);
} else {
scrollLeftButton.setEnabled(true);
}
if ((scrollPanel.getElement().getScrollLeft() > 0
|| (widgetBar.getOffsetWidth() - scrollPanel.getOffsetWidth()) > 0)
&& scrollPanel.getElement().getScrollLeft()
>= (widgetBar.getOffsetWidth() - scrollPanel.getOffsetWidth())) {
scrollRightButton.setEnabled(false);
} else {
scrollRightButton.setEnabled(true);
}
}
@Override
public void recalculateSize() {
recalculateScrollPanelMaxWidth();
recalculateWidgetBarMinWidth();
}
@Override
public void setOffset(int left, boolean wantsLeft) {
if (wantsLeft) {
asWidget().getElement().getStyle().setLeft(left, Unit.PX);
}
asWidget().getElement().getStyle().setWidth(Window.getClientWidth() - left, Unit.PX);
recalculateSize();
showScrollButtons();
}
}