/* * Copyright 2016 MovingBlocks * * 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.terasology.rendering.nui.widgets; import com.google.common.collect.Lists; import org.terasology.math.Border; import org.terasology.math.geom.Rect2i; import org.terasology.math.geom.Vector2i; import org.terasology.rendering.assets.font.Font; import org.terasology.rendering.nui.BaseInteractionListener; import org.terasology.rendering.nui.Canvas; import org.terasology.rendering.nui.InteractionListener; import org.terasology.rendering.nui.SubRegion; import org.terasology.rendering.nui.databinding.Binding; import org.terasology.rendering.nui.databinding.DefaultBinding; import org.terasology.rendering.nui.events.NUIMouseClickEvent; import org.terasology.rendering.nui.events.NUIMouseWheelEvent; import org.terasology.rendering.nui.itemRendering.ItemRenderer; import org.terasology.rendering.nui.itemRendering.ToStringTextRenderer; import java.util.ArrayList; import java.util.List; /** * A scrollable dropdown widget. * * @param <T> the list element type */ public class UIDropdownScrollable<T> extends UIDropdown<T> { private static final String LIST = "list"; private static final String LIST_ITEM = "list-item"; private UIScrollbar verticalBar = new UIScrollbar(true); private int visibleOptionsNum = 5; private Binding<List<T>> options = new DefaultBinding<>(new ArrayList<>()); private Binding<T> selection = new DefaultBinding<>(); private List<InteractionListener> optionListeners = Lists.newArrayList(); private ItemRenderer<T> optionRenderer = new ToStringTextRenderer<>(); private boolean opened; private InteractionListener mainListener = new BaseInteractionListener() { @Override public boolean onMouseClick(NUIMouseClickEvent event) { opened = !opened; optionListeners.clear(); if (opened) { for (int i = 0; i < getOptions().size(); ++i) { optionListeners.add(new ItemListener(i)); } } return true; } @Override public boolean onMouseWheel(NUIMouseWheelEvent event) { int scrollMultiplier = 0 - verticalBar.getRange() / getOptions().size(); verticalBar.setValue(verticalBar.getValue() + event.getWheelTurns() * scrollMultiplier); return true; } }; public UIDropdownScrollable() { } public UIDropdownScrollable(String id) { super(id); } @Override public boolean isSkinAppliedByCanvas() { return false; } @Override public void onDraw(Canvas canvas) { canvas.drawBackground(); try (SubRegion ignored = canvas.subRegion(canvas.getCurrentStyle().getMargin().shrink(canvas.getRegion()), false)) { if (selection.get() != null) { optionRenderer.draw(selection.get(), canvas); } } if (!isEnabled()) { // do not open and do not add an interaction region } else if (opened) { canvas.setPart(LIST); canvas.setDrawOnTop(true); Font font = canvas.getCurrentStyle().getFont(); Border itemMargin = canvas.getCurrentStyle().getMargin(); // Limit number of visible options float optionsSize = options.get().size() <= visibleOptionsNum ? options.get().size() : (visibleOptionsNum + 0.5f); // Calculate total options height int itemHeight = itemMargin.getTotalHeight() + font.getLineHeight(); int height = (int) (itemHeight * optionsSize + canvas.getCurrentStyle().getBackgroundBorder().getTotalHeight()); canvas.addInteractionRegion(mainListener, Rect2i.createFromMinAndSize(0, 0, canvas.size().x, canvas.size().y + height)); // Dropdown Background Frame Rect2i frame = Rect2i.createFromMinAndSize(0, canvas.size().y, canvas.size().x, height); canvas.drawBackground(frame); canvas.setPart(LIST_ITEM); if (options.get().size() > visibleOptionsNum) { createScrollbarItems(canvas, frame, font, itemMargin, height, itemHeight); } else { createNoScrollItems(canvas, itemMargin, itemHeight); } } else { canvas.addInteractionRegion(mainListener); } } /** * Located in the onDraw method, this draws the menu items when the scrollbar is unnecessary. * * @param canvas {@link Canvas} from the onDraw method. * @param itemMargin Margin around every menu item. * @param itemHeight Height per menu item. */ private void createNoScrollItems(Canvas canvas, Border itemMargin, int itemHeight) { for (int i = 0; i < optionListeners.size(); ++i) { readItemMouseOver(canvas, i); Rect2i itemRegion = Rect2i.createFromMinAndSize(0, canvas.size().y + itemHeight * i, canvas.size().x, itemHeight); drawItem(canvas, itemMargin, i, itemRegion); } } /** * Located in the onDraw method, this draws the menu items with a scrollbar. * * @param canvas {@link Canvas} from the onDraw method. * @param frame Menu frame. * @param font {@link Font} used in the menu. * @param itemMargin Margin around every menu item. * @param height Total menu height. * @param itemHeight Height per menu item. */ private void createScrollbarItems(Canvas canvas, Rect2i frame, Font font, Border itemMargin, int height, int itemHeight) { // Scrollable Area Rect2i scrollableArea = Rect2i.createFromMinAndSize(0, canvas.size().y, canvas.size().x, height - itemMargin.getBottom()); // Scrollbar Measurement int scrollbarWidth = canvas.calculateRestrictedSize(verticalBar, new Vector2i(canvas.size().x, canvas.size().y)).x; int scrollbarHeight = frame.size().y - itemMargin.getTop(); int availableWidth = frame.size().x - scrollbarWidth; int scrollbarXPos = availableWidth - itemMargin.getRight(); int scrollbarYPos = itemMargin.getTotalHeight() * 2 + font.getLineHeight(); // Draw Scrollbar Rect2i scrollbarRegion = Rect2i.createFromMinAndSize(scrollbarXPos, scrollbarYPos, scrollbarWidth, scrollbarHeight); canvas.drawWidget(verticalBar, scrollbarRegion); // Set the range of Scrollbar float maxVertBarDesired = itemHeight * (optionListeners.size() - visibleOptionsNum - 0.5f) + itemMargin.getBottom(); verticalBar.setRange((int) maxVertBarDesired); for (int i = 0; i < optionListeners.size(); ++i) { readItemMouseOver(canvas, i); Rect2i itemRegion = Rect2i.createFromMinAndSize(0, itemHeight * i - verticalBar.getValue(), availableWidth, itemHeight); // If outside location, then hide try (SubRegion ignored = canvas.subRegion(scrollableArea, true)) { drawItem(canvas, itemMargin, i, itemRegion); } } } /** * Looks for MouseOver event for every item in the menu. * * @param canvas {@link Canvas} from the onDraw method. * @param i Item index. */ private void readItemMouseOver(Canvas canvas, int i) { if (optionListeners.get(i).isMouseOver()) { canvas.setMode(HOVER_MODE); } else { canvas.setMode(DEFAULT_MODE); } } /** * Draws the item on the {@link Canvas}. * * @param canvas {@link Canvas} from the onDraw method. * @param itemMargin Margin around every menu item. * @param i Item index. * @param itemRegion Region of the item in the menu. */ private void drawItem(Canvas canvas, Border itemMargin, int i, Rect2i itemRegion) { canvas.drawBackground(itemRegion); optionRenderer.draw(options.get().get(i), canvas, itemMargin.shrink(itemRegion)); canvas.addInteractionRegion(optionListeners.get(i), itemRegion); } @Override public void onLoseFocus() { super.onLoseFocus(); String mode = verticalBar.getMode(); if (!mode.equals("active")) { opened = false; super.onGainFocus(); } } public void bindOptions(Binding<List<T>> binding) { options = binding; } /** * @return A List containing all the options. */ public List<T> getOptions() { return options.get(); } /** * @param values A List containing the new options. */ public void setOptions(List<T> values) { this.options.set(values); } public void bindSelection(Binding<T> binding) { this.selection = binding; } /** * @return The currently selected item. */ public T getSelection() { return selection.get(); } /** * @param value The item to set as selected. */ public void setSelection(T value) { selection.set(value); } /** * @param itemRenderer The new item renderer. */ public void setOptionRenderer(ItemRenderer<T> itemRenderer) { optionRenderer = itemRenderer; } /** * @return The number of options visible. */ public int getVisibleOptions() { return visibleOptionsNum; } /** * @param num The number of visible options. */ public void setVisibleOptions(int num) { visibleOptionsNum = num; } private class ItemListener extends BaseInteractionListener { private int index; ItemListener(int index) { this.index = index; } @Override public boolean onMouseClick(NUIMouseClickEvent event) { setSelection(getOptions().get(index)); opened = false; return true; } } }