/** * A scroll pane to scroll another widget if it requires more space then available. * * <p>It requires the following child themes: * * <table> * <tr> * <th>Theme</th> * <th>Description</th> * </tr> * <tr> * <td>hscrollbar</td> * <td>The horizontal scrollbar</td> * </tr> * <tr> * <td>vscrollbar</td> * <td>The vertical scrollbar</td> * </tr> * <tr> * <td>dragButton</td> * <td>The drag button in the bottom right corner. Only needed when * hasDragButton is true.</td> * </tr> * </table> * * <br> * For the remaining theme parameters look at {@link * #applyThemeScrollPane(de.matthiasmann.twl.ThemeInfo) } * * @author Matthias Mann */ public class ScrollPane extends Widget { public static final StateKey STATE_DOWNARROW_ARMED = StateKey.get("downArrowArmed"); public static final StateKey STATE_RIGHTARROW_ARMED = StateKey.get("rightArrowArmed"); public static final StateKey STATE_HORIZONTAL_SCROLLBAR_VISIBLE = StateKey.get("horizontalScrollbarVisible"); public static final StateKey STATE_VERTICAL_SCROLLBAR_VISIBLE = StateKey.get("verticalScrollbarVisible"); public static final StateKey STATE_AUTO_SCROLL_UP = StateKey.get("autoScrollUp"); public static final StateKey STATE_AUTO_SCROLL_DOWN = StateKey.get("autoScrollDown"); /** Controls which axis of the scroll pane should be fixed */ public enum Fixed { /** No axis is fixed - the scroll pane may show 2 scroll bars */ NONE, /** The horizontal axis is fixed - only a vertical scroll bar may be shown */ HORIZONTAL, /** The vertical axis is fixed - only a horizontal scroll bar may be shown */ VERTICAL } /** * Indicates that the content handles scrolling itself. * * <p>This interfaces also allows for a larger scrollable size then the Widget size limitations. * * <p>The {@code ScrollPane} will set the size of content to the available content area. */ public interface Scrollable { /** * Called when the content is scrolled either by a call to {@link * ScrollPane#setScrollPositionX(int) }, {@link ScrollPane#setScrollPositionY(int) } or through * one of the scrollbars. * * @param scrollPosX the new horizontal scroll position. Always >= 0. * @param scrollPosY the new vertical scroll position. Always >= 0. */ public void setScrollPosition(int scrollPosX, int scrollPosY); } /** Custom auto scroll area checking. This is needed when the content has column headers. */ public interface AutoScrollable { /** * Returns the auto scroll direction for the specified mouse event. * * @param evt the mouse event which could trigger an auto scroll * @param autoScrollArea the size of the auto scroll area. This is a theme parameter of the * {@link ScrollPane} * @return the auto scroll direction. -1 for upwards 0 for no auto scrolling +1 for downwards * @see ScrollPane#checkAutoScroll(de.matthiasmann.twl.Event) */ public int getAutoScrollDirection(Event evt, int autoScrollArea); } /** * Custom page sizes for page scrolling and scroll bar thumb sizing. This is needed when the * content has column or row headers. */ public interface CustomPageSize { /** * Computes the horizontal page size based on the available width. * * @param availableWidth the available width (the visible area) * @return the page size. Must be > 0 and <= availableWidth */ public int getPageSizeX(int availableWidth); /** * Computes the vertical page size based on the available height. * * @param availableHeight the available height (the visible area) * @return the page size. Must be > 0 and <= availableHeight */ public int getPageSizeY(int availableHeight); } private static final int AUTO_SCROLL_DELAY = 50; final Scrollbar scrollbarH; final Scrollbar scrollbarV; private final Widget contentArea; private DraggableButton dragButton; private Widget content; private Fixed fixed = Fixed.NONE; private Dimension hscrollbarOffset = Dimension.ZERO; private Dimension vscrollbarOffset = Dimension.ZERO; private Dimension contentScrollbarSpacing = Dimension.ZERO; private boolean inLayout; private boolean expandContentSize; private boolean scrollbarsAlwaysVisible; private int scrollbarsToggleFlags; private int autoScrollArea; private int autoScrollSpeed; private Timer autoScrollTimer; private int autoScrollDirection; public ScrollPane() { this(null); } @SuppressWarnings("OverridableMethodCallInConstructor") public ScrollPane(Widget content) { this.scrollbarH = new Scrollbar(Scrollbar.Orientation.HORIZONTAL); this.scrollbarV = new Scrollbar(Scrollbar.Orientation.VERTICAL); this.contentArea = new Widget(); Runnable cb = new Runnable() { public void run() { scrollContent(); } }; scrollbarH.addCallback(cb); scrollbarH.setVisible(false); scrollbarV.addCallback(cb); scrollbarV.setVisible(false); contentArea.setClip(true); contentArea.setTheme(""); super.insertChild(contentArea, 0); super.insertChild(scrollbarH, 1); super.insertChild(scrollbarV, 2); setContent(content); setCanAcceptKeyboardFocus(true); } public Fixed getFixed() { return fixed; } /** * Controls if this scroll pane has a fixed axis which will not show a scrollbar. * * <p>Default is {@link Fixed#NONE} * * @param fixed the fixed axis. */ public void setFixed(Fixed fixed) { if (fixed == null) { throw new NullPointerException("fixed"); } if (this.fixed != fixed) { this.fixed = fixed; invalidateLayout(); } } public Widget getContent() { return content; } /** * Sets the widget which should be scrolled. * * <p>The following interfaces change the behavior of the scroll pane when they are implemented by * the content: * * <ul> * <li>{@link Scrollable} * <li>{@link AutoScrollable} * <li>{@link CustomPageSize} * </ul> * * @param content the new scroll pane content */ public void setContent(Widget content) { if (this.content != null) { contentArea.removeAllChildren(); this.content = null; } if (content != null) { this.content = content; contentArea.add(content); } } public boolean isExpandContentSize() { return expandContentSize; } /** * Control if the content size. * * <p>If set to true then the content size will be the larger of it's preferred size and the size * of the content area. If set to false then the content size will be it's preferred area. * * <p>Default is false * * @param expandContentSize true if the content should always cover the content area */ public void setExpandContentSize(boolean expandContentSize) { if (this.expandContentSize != expandContentSize) { this.expandContentSize = expandContentSize; invalidateLayoutLocally(); } } /** * Forces a layout of the scroll pane content to update the ranges of the scroll bars. * * <p>This method should be called after changes to the content which might affect it's size and * before computing a new scroll position. * * @see #scrollToAreaX(int, int, int) * @see #scrollToAreaY(int, int, int) */ public void updateScrollbarSizes() { invalidateLayoutLocally(); validateLayout(); } public int getScrollPositionX() { return scrollbarH.getValue(); } public int getMaxScrollPosX() { return scrollbarH.getMaxValue(); } public void setScrollPositionX(int pos) { scrollbarH.setValue(pos); } /** * Tries to make the specified horizontal area completely visible. If it is larger then the * horizontal page size then it scrolls to the start of the area. * * @param start the position of the area * @param size size of the area * @param extra the extra space which should be visible around the area * @see Scrollbar#scrollToArea(int, int, int) */ public void scrollToAreaX(int start, int size, int extra) { scrollbarH.scrollToArea(start, size, extra); } public int getScrollPositionY() { return scrollbarV.getValue(); } public int getMaxScrollPosY() { return scrollbarV.getMaxValue(); } public void setScrollPositionY(int pos) { scrollbarV.setValue(pos); } /** * Tries to make the specified vertical area completely visible. If it is larger then the vertical * page size then it scrolls to the start of the area. * * @param start the position of the area * @param size size of the area * @param extra the extra space which should be visible around the area * @see Scrollbar#scrollToArea(int, int, int) */ public void scrollToAreaY(int start, int size, int extra) { scrollbarV.scrollToArea(start, size, extra); } public int getContentAreaWidth() { return contentArea.getWidth(); } public int getContentAreaHeight() { return contentArea.getHeight(); } /** * Returns the horizontal scrollbar widget, be very careful with changes to it. * * @return the horizontal scrollbar */ public Scrollbar getHorizontalScrollbar() { return scrollbarH; } /** * Returns the vertical scrollbar widget, be very careful with changes to it. * * @return the vertical scrollbar */ public Scrollbar getVerticalScrollbar() { return scrollbarV; } /** * Creates a DragListener which can be used to drag the content of this ScrollPane around. * * @return a DragListener to scroll this this ScrollPane. */ public DraggableButton.DragListener createDragListener() { return new DraggableButton.DragListener() { int startScrollX; int startScrollY; public void dragStarted() { startScrollX = getScrollPositionX(); startScrollY = getScrollPositionY(); } public void dragged(int deltaX, int deltaY) { setScrollPositionX(startScrollX - deltaX); setScrollPositionY(startScrollY - deltaY); } public void dragStopped() {} }; } /** * Checks for an auto scroll event. This should be called when a drag & drop operation is in * progress and the drop target is inside a scroll pane. * * @param evt the mouse event which should be checked. * @return true if auto scrolling is started/active. * @see #stopAutoScroll() */ public boolean checkAutoScroll(Event evt) { GUI gui = getGUI(); if (gui == null) { stopAutoScroll(); return false; } autoScrollDirection = getAutoScrollDirection(evt); if (autoScrollDirection == 0) { stopAutoScroll(); return false; } setAutoScrollMarker(); if (autoScrollTimer == null) { autoScrollTimer = gui.createTimer(); autoScrollTimer.setContinuous(true); autoScrollTimer.setDelay(AUTO_SCROLL_DELAY); autoScrollTimer.setCallback( new Runnable() { public void run() { doAutoScroll(); } }); doAutoScroll(); } autoScrollTimer.start(); return true; } /** * Stops an activate auto scroll. This must be called when the drag & drop operation is finished. * * @see #checkAutoScroll(de.matthiasmann.twl.Event) */ public void stopAutoScroll() { if (autoScrollTimer != null) { autoScrollTimer.stop(); } autoScrollDirection = 0; setAutoScrollMarker(); } /** * Returns the ScrollPane instance which has the specified widget as content. * * @param widget the widget to retrieve the containing ScrollPane for. * @return the ScrollPane or null if that widget is not directly in a ScrollPane. * @see #setContent(de.matthiasmann.twl.Widget) */ public static ScrollPane getContainingScrollPane(Widget widget) { Widget ca = widget.getParent(); if (ca != null) { Widget sp = ca.getParent(); if (sp instanceof ScrollPane) { ScrollPane scrollPane = (ScrollPane) sp; assert scrollPane.getContent() == widget; return scrollPane; } } return null; } @Override public int getMinWidth() { int minWidth = super.getMinWidth(); int border = getBorderHorizontal(); // minWidth = Math.max(minWidth, scrollbarH.getMinWidth() + border); if (fixed == Fixed.HORIZONTAL && content != null) { int sbWidth = scrollbarV.isVisible() ? scrollbarV.getMinWidth() : 0; minWidth = Math.max(minWidth, content.getMinWidth() + border + sbWidth); } return minWidth; } @Override public int getMinHeight() { int minHeight = super.getMinHeight(); int border = getBorderVertical(); // minHeight = Math.max(minHeight, scrollbarV.getMinHeight() + border); if (fixed == Fixed.VERTICAL && content != null) { int sbHeight = scrollbarH.isVisible() ? scrollbarH.getMinHeight() : 0; minHeight = Math.max(minHeight, content.getMinHeight() + border + sbHeight); } return minHeight; } @Override public int getPreferredInnerWidth() { if (content != null) { switch (fixed) { case HORIZONTAL: int prefWidth = computeSize( content.getMinWidth(), content.getPreferredWidth(), content.getMaxWidth()); if (scrollbarV.isVisible()) { prefWidth += scrollbarV.getPreferredWidth(); } return prefWidth; case VERTICAL: return content.getPreferredWidth(); } } return 0; } @Override public int getPreferredInnerHeight() { if (content != null) { switch (fixed) { case HORIZONTAL: return content.getPreferredHeight(); case VERTICAL: int prefHeight = computeSize( content.getMinHeight(), content.getPreferredHeight(), content.getMaxHeight()); if (scrollbarH.isVisible()) { prefHeight += scrollbarH.getPreferredHeight(); } return prefHeight; } } return 0; } @Override public void insertChild(Widget child, int index) { throw new UnsupportedOperationException("use setContent"); } @Override public void removeAllChildren() { throw new UnsupportedOperationException("use setContent"); } @Override public Widget removeChild(int index) { throw new UnsupportedOperationException("use setContent"); } @Override protected void applyTheme(ThemeInfo themeInfo) { super.applyTheme(themeInfo); applyThemeScrollPane(themeInfo); } /** * The following theme parameters are required by the scroll pane: * * <table> * <tr> * <th>Parameter name</th> * <th>Type</th> * <th>Description</th> * </tr> * <tr> * <td>autoScrollArea</td> * <td>integer</td> * <td>The size of the auto scroll area</td> * </tr> * <tr> * <td>autoScrollSpeed</td> * <td>integer</td> * <td>The speed in pixels to scroll every 50 ms</td> * </tr> * <tr> * <td>hasDragButton</td> * <td>boolean</td> * <td>If the dragButton should be shown or not</td> * </tr> * <tr> * <td>scrollbarsAlwaysVisible</td> * <td>boolean</td> * <td>Show scrollbars always (true) or only when needed (false)</td> * </tr> * </table> * * <br> * The following optional parameters can be used to change the appearance of the scroll pane: * * <table> * <tr> * <th>Parameter name</th> * <th>Type</th> * <th>Description</th> * </tr> * <tr> * <td>hscrollbarOffset</td> * <td>Dimension</td> * <td>Moves the horizontal scrollbar but does not change the available area * for the scroll content.</td> * </tr> * <tr> * <td>vscrollbarOffset</td> * <td>Dimension</td> * <td>Moves the vertical scrollbar but does not change the available area * for the scroll content.</td> * </tr> * <tr> * <td>contentScrollbarSpacing</td> * <td>Dimension</td> * <td>An optional spacing between the scrollbar and the content area. This * is only applied when the corresponding scrollbar is visible. It should be * >= 0.</td> * </tr> * </table> * * @param themeInfo the theme info */ protected void applyThemeScrollPane(ThemeInfo themeInfo) { autoScrollArea = themeInfo.getParameter("autoScrollArea", 5); autoScrollSpeed = themeInfo.getParameter("autoScrollSpeed", autoScrollArea * 2); hscrollbarOffset = themeInfo.getParameterValue("hscrollbarOffset", false, Dimension.class, Dimension.ZERO); vscrollbarOffset = themeInfo.getParameterValue("vscrollbarOffset", false, Dimension.class, Dimension.ZERO); contentScrollbarSpacing = themeInfo.getParameterValue( "contentScrollbarSpacing", false, Dimension.class, Dimension.ZERO); scrollbarsAlwaysVisible = themeInfo.getParameter("scrollbarsAlwaysVisible", false); boolean hasDragButton = themeInfo.getParameter("hasDragButton", false); if (hasDragButton && dragButton == null) { dragButton = new DraggableButton(); dragButton.setTheme("dragButton"); dragButton.setListener( new DraggableButton.DragListener() { public void dragStarted() { scrollbarH.externalDragStart(); scrollbarV.externalDragStart(); } public void dragged(int deltaX, int deltaY) { scrollbarH.externalDragged(deltaX, deltaY); scrollbarV.externalDragged(deltaX, deltaY); } public void dragStopped() { scrollbarH.externalDragStopped(); scrollbarV.externalDragStopped(); } }); super.insertChild(dragButton, 3); } else if (!hasDragButton && dragButton != null) { assert super.getChild(3) == dragButton; super.removeChild(3); dragButton = null; } } protected int getAutoScrollDirection(Event evt) { if (content instanceof AutoScrollable) { return ((AutoScrollable) content).getAutoScrollDirection(evt, autoScrollArea); } if (contentArea.isMouseInside(evt)) { int mouseY = evt.getMouseY(); int areaY = contentArea.getY(); if ((mouseY - areaY) <= autoScrollArea || (contentArea.getBottom() - mouseY) <= autoScrollArea) { // use a 2nd check to decide direction in case the // autoScrollAreas overlap if (mouseY < (areaY + contentArea.getHeight() / 2)) { return -1; } else { return +1; } } } return 0; } @Override public void validateLayout() { if (!inLayout) { try { inLayout = true; if (content != null) { content.validateLayout(); } super.validateLayout(); } finally { inLayout = false; } } } @Override protected void childInvalidateLayout(Widget child) { if (child == contentArea) { // stop invalidate layout chain here when it comes from contentArea invalidateLayoutLocally(); } else { super.childInvalidateLayout(child); } } @Override protected void paintWidget(GUI gui) { // clear flags - used to detect layout loops scrollbarsToggleFlags = 0; } @Override protected void layout() { if (content != null) { int innerWidth = getInnerWidth(); int innerHeight = getInnerHeight(); int availWidth = innerWidth; int availHeight = innerHeight; innerWidth += vscrollbarOffset.getX(); innerHeight += hscrollbarOffset.getY(); int scrollbarHX = hscrollbarOffset.getX(); int scrollbarHY = innerHeight; int scrollbarVX = innerWidth; int scrollbarVY = vscrollbarOffset.getY(); int requiredWidth; int requiredHeight; boolean repeat; boolean visibleH = false; boolean visibleV = false; switch (fixed) { case HORIZONTAL: requiredWidth = availWidth; requiredHeight = content.getPreferredHeight(); break; case VERTICAL: requiredWidth = content.getPreferredWidth(); requiredHeight = availHeight; break; default: requiredWidth = content.getPreferredWidth(); requiredHeight = content.getPreferredHeight(); break; } // System.out.println("required="+requiredWidth+","+requiredHeight+" // avail="+availWidth+","+availHeight); int hScrollbarMax = 0; int vScrollbarMax = 0; // don't add scrollbars if we have zero size if (availWidth > 0 && availHeight > 0) { do { repeat = false; if (fixed != Fixed.HORIZONTAL) { hScrollbarMax = Math.max(0, requiredWidth - availWidth); if (hScrollbarMax > 0 || scrollbarsAlwaysVisible || ((scrollbarsToggleFlags & 3) == 3)) { repeat |= !visibleH; visibleH = true; int prefHeight = scrollbarH.getPreferredHeight(); scrollbarHY = innerHeight - prefHeight; availHeight = Math.max(0, scrollbarHY - contentScrollbarSpacing.getY()); } } else { hScrollbarMax = 0; requiredWidth = availWidth; } if (fixed != Fixed.VERTICAL) { vScrollbarMax = Math.max(0, requiredHeight - availHeight); if (vScrollbarMax > 0 || scrollbarsAlwaysVisible || ((scrollbarsToggleFlags & 12) == 12)) { repeat |= !visibleV; visibleV = true; int prefWidth = scrollbarV.getPreferredWidth(); scrollbarVX = innerWidth - prefWidth; availWidth = Math.max(0, scrollbarVX - contentScrollbarSpacing.getX()); } } else { vScrollbarMax = 0; requiredHeight = availHeight; } } while (repeat); } // if a scrollbar visibility state has changed set it's flag to // detect layout loops if (visibleH && !scrollbarH.isVisible()) { scrollbarsToggleFlags |= 1; } if (!visibleH && scrollbarH.isVisible()) { scrollbarsToggleFlags |= 2; } if (visibleV && !scrollbarV.isVisible()) { scrollbarsToggleFlags |= 4; } if (!visibleV && scrollbarV.isVisible()) { scrollbarsToggleFlags |= 8; } boolean changedH = visibleH ^ scrollbarH.isVisible(); boolean changedV = visibleV ^ scrollbarV.isVisible(); if (changedH || changedV) { if ((changedH && fixed == Fixed.VERTICAL) || (changedV && fixed == Fixed.HORIZONTAL)) { invalidateLayout(); } else { invalidateLayoutLocally(); } } int pageSizeX, pageSizeY; if (content instanceof CustomPageSize) { CustomPageSize customPageSize = (CustomPageSize) content; pageSizeX = customPageSize.getPageSizeX(availWidth); pageSizeY = customPageSize.getPageSizeY(availHeight); } else { pageSizeX = availWidth; pageSizeY = availHeight; } scrollbarH.setVisible(visibleH); scrollbarH.setMinMaxValue(0, hScrollbarMax); scrollbarH.setSize( Math.max(0, scrollbarVX - scrollbarHX), Math.max(0, innerHeight - scrollbarHY)); scrollbarH.setPosition(getInnerX() + scrollbarHX, getInnerY() + scrollbarHY); scrollbarH.setPageSize(Math.max(1, pageSizeX)); scrollbarH.setStepSize(Math.max(1, pageSizeX / 10)); scrollbarV.setVisible(visibleV); scrollbarV.setMinMaxValue(0, vScrollbarMax); scrollbarV.setSize( Math.max(0, innerWidth - scrollbarVX), Math.max(0, scrollbarHY - scrollbarVY)); scrollbarV.setPosition(getInnerX() + scrollbarVX, getInnerY() + scrollbarVY); scrollbarV.setPageSize(Math.max(1, pageSizeY)); scrollbarV.setStepSize(Math.max(1, pageSizeY / 10)); if (dragButton != null) { dragButton.setVisible(visibleH && visibleV); dragButton.setSize( Math.max(0, innerWidth - scrollbarVX), Math.max(0, innerHeight - scrollbarHY)); dragButton.setPosition(getInnerX() + scrollbarVX, getInnerY() + scrollbarHY); } contentArea.setPosition(getInnerX(), getInnerY()); contentArea.setSize(availWidth, availHeight); if (content instanceof Scrollable) { content.setPosition(contentArea.getX(), contentArea.getY()); content.setSize(availWidth, availHeight); } else if (expandContentSize) { content.setSize(Math.max(availWidth, requiredWidth), Math.max(availHeight, requiredHeight)); } else { content.setSize(Math.max(0, requiredWidth), Math.max(0, requiredHeight)); } AnimationState animationState = getAnimationState(); animationState.setAnimationState(STATE_HORIZONTAL_SCROLLBAR_VISIBLE, visibleH); animationState.setAnimationState(STATE_VERTICAL_SCROLLBAR_VISIBLE, visibleV); scrollContent(); } else { scrollbarH.setVisible(false); scrollbarV.setVisible(false); } } @Override protected boolean handleEvent(Event evt) { if (evt.isKeyEvent() && content != null && content.canAcceptKeyboardFocus()) { if (content.handleEvent(evt)) { content.requestKeyboardFocus(); return true; } } if (super.handleEvent(evt)) { return true; } switch (evt.getType()) { case KEY_PRESSED: case KEY_RELEASED: { int keyCode = evt.getKeyCode(); if (keyCode == Event.KEY_LEFT || keyCode == Event.KEY_RIGHT) { return scrollbarH.handleEvent(evt); } if (keyCode == Event.KEY_UP || keyCode == Event.KEY_DOWN || keyCode == Event.KEY_PRIOR || keyCode == Event.KEY_NEXT) { return scrollbarV.handleEvent(evt); } break; } case MOUSE_WHEEL: if (scrollbarV.isVisible()) { return scrollbarV.handleEvent(evt); } return false; } return evt.isMouseEvent() && contentArea.isMouseInside(evt); } @Override protected void paint(GUI gui) { if (dragButton != null) { AnimationState as = dragButton.getAnimationState(); as.setAnimationState(STATE_DOWNARROW_ARMED, scrollbarV.isDownRightButtonArmed()); as.setAnimationState(STATE_RIGHTARROW_ARMED, scrollbarH.isDownRightButtonArmed()); } super.paint(gui); } void scrollContent() { if (content instanceof Scrollable) { Scrollable scrollable = (Scrollable) content; scrollable.setScrollPosition(scrollbarH.getValue(), scrollbarV.getValue()); } else { content.setPosition( contentArea.getX() - scrollbarH.getValue(), contentArea.getY() - scrollbarV.getValue()); } } void setAutoScrollMarker() { int scrollPos = scrollbarV.getValue(); AnimationState animationState = getAnimationState(); animationState.setAnimationState( STATE_AUTO_SCROLL_UP, autoScrollDirection < 0 && scrollPos > 0); animationState.setAnimationState( STATE_AUTO_SCROLL_DOWN, autoScrollDirection > 0 && scrollPos < scrollbarV.getMaxValue()); } void doAutoScroll() { scrollbarV.setValue(scrollbarV.getValue() + autoScrollDirection * autoScrollSpeed); setAutoScrollMarker(); } }
/** @author Matthias Mann */ public class ItemSlot extends Widget { public static final StateKey STATE_DRAG_ACTIVE = StateKey.get("dragActive"); public static final StateKey STATE_DROP_OK = StateKey.get("dropOk"); public static final StateKey STATE_DROP_BLOCKED = StateKey.get("dropBlocked"); public interface DragListener { public void dragStarted(ItemSlot slot, Event evt); public void dragging(ItemSlot slot, Event evt); public void dragStopped(ItemSlot slot, Event evt); } private String item; private Image icon; private DragListener listener; private boolean dragActive; private ParameterMap icons; private GunMode gun_mode; public ItemSlot() {} public String getItem() { return item; } public void setItem(String item) { this.item = item; findIcon(); } public void setItemAndGunMode(String item, GunMode mode) { this.item = item; this.gun_mode = mode; findIcon(); } public GunMode getGunMode() { return gun_mode; } public boolean canDrop() { return item == null; } public Image getIcon() { return icon; } public DragListener getListener() { return listener; } public void setListener(DragListener listener) { this.listener = listener; } public void setDropState(boolean drop, boolean ok) { AnimationState as = getAnimationState(); as.setAnimationState(STATE_DROP_OK, drop && ok); as.setAnimationState(STATE_DROP_BLOCKED, drop && !ok); } @Override protected boolean handleEvent(Event evt) { if (evt.isMouseEventNoWheel()) { if (dragActive) { if (evt.isMouseDragEnd()) { if (listener != null) { listener.dragStopped(this, evt); } dragActive = false; getAnimationState().setAnimationState(STATE_DRAG_ACTIVE, false); } else if (listener != null) { listener.dragging(this, evt); } } else if (evt.isMouseDragEvent()) { dragActive = true; getAnimationState().setAnimationState(STATE_DRAG_ACTIVE, true); if (listener != null) { listener.dragStarted(this, evt); } } return true; } return super.handleEvent(evt); } @Override protected void paintWidget(GUI gui) { if (!dragActive && icon != null) { icon.draw(getAnimationState(), getInnerX(), getInnerY(), getInnerWidth(), getInnerHeight()); } } @Override protected void paintDragOverlay(GUI gui, int mouseX, int mouseY, int modifier) { if (icon != null) { final int innerWidth = getInnerWidth(); final int innerHeight = getInnerHeight(); icon.draw( getAnimationState(), mouseX - innerWidth / 2, mouseY - innerHeight / 2, innerWidth, innerHeight); } } @Override protected void applyTheme(ThemeInfo themeInfo) { super.applyTheme(themeInfo); icons = themeInfo.getParameterMap("icons"); findIcon(); } private void findIcon() { if (item == null || icons == null) { icon = null; } else { icon = icons.getImage(item); } } public String toString() { return "Item slot: " + item + ", " + gun_mode; } }
/** * A renderer using only GL11 features. * * <p>For correct rendering the OpenGL viewport size must be synchronized. * * @author Matthias Mann * @see #syncViewportSize() */ public class LWJGLRenderer implements Renderer, LineRenderer { public static final StateKey STATE_LEFT_MOUSE_BUTTON = StateKey.get("leftMouseButton"); public static final StateKey STATE_MIDDLE_MOUSE_BUTTON = StateKey.get("middleMouseButton"); public static final StateKey STATE_RIGHT_MOUSE_BUTTON = StateKey.get("rightMouseButton"); public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_X = FontParameter.newParameter("offsetX", 0); public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_Y = FontParameter.newParameter("offsetY", 0); public static final FontParameter.Parameter<Integer> FONTPARAM_UNDERLINE_OFFSET = FontParameter.newParameter("underlineOffset", 0); private final IntBuffer ib16; final int maxTextureSize; private int viewportX; private int viewportBottom; private int width; private int height; private boolean hasScissor; private final TintStack tintStateRoot; private final Cursor emptyCursor; private boolean useQuadsForLines; private boolean useSWMouseCursors; private SWCursor swCursor; private int mouseX; private int mouseY; private LWJGLCacheContext cacheContext; private FontMapper fontMapper; final SWCursorAnimState swCursorAnimState; final ArrayList<TextureArea> textureAreas; final ArrayList<TextureAreaRotated> rotatedTextureAreas; final ArrayList<LWJGLDynamicImage> dynamicImages; protected TintStack tintStack; protected final ClipStack clipStack; protected final Rect clipRectTemp; @SuppressWarnings("OverridableMethodCallInConstructor") public LWJGLRenderer() throws LWJGLException { this.ib16 = BufferUtils.createIntBuffer(16); this.textureAreas = new ArrayList<TextureArea>(); this.rotatedTextureAreas = new ArrayList<TextureAreaRotated>(); this.dynamicImages = new ArrayList<LWJGLDynamicImage>(); this.tintStateRoot = new TintStack(); this.tintStack = tintStateRoot; this.clipStack = new ClipStack(); this.clipRectTemp = new Rect(); syncViewportSize(); GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE, ib16); maxTextureSize = ib16.get(0); if (Mouse.isCreated()) { int minCursorSize = Cursor.getMinCursorSize(); IntBuffer tmp = BufferUtils.createIntBuffer(minCursorSize * minCursorSize); emptyCursor = new Cursor( minCursorSize, minCursorSize, minCursorSize / 2, minCursorSize / 2, 1, tmp, null); } else { emptyCursor = null; } swCursorAnimState = new SWCursorAnimState(); } public boolean isUseQuadsForLines() { return useQuadsForLines; } public void setUseQuadsForLines(boolean useQuadsForLines) { this.useQuadsForLines = useQuadsForLines; } public boolean isUseSWMouseCursors() { return useSWMouseCursors; } /** * Controls if the mouse cursor is rendered via SW or HW cursors. HW cursors have reduced support * for transparency and cursor size. * * <p>This must be set before loading a theme ! * * @param useSWMouseCursors */ public void setUseSWMouseCursors(boolean useSWMouseCursors) { this.useSWMouseCursors = useSWMouseCursors; } public CacheContext createNewCacheContext() { return new LWJGLCacheContext(this); } private LWJGLCacheContext activeCacheContext() { if (cacheContext == null) { setActiveCacheContext(createNewCacheContext()); } return cacheContext; } public CacheContext getActiveCacheContext() { return activeCacheContext(); } public void setActiveCacheContext(CacheContext cc) throws IllegalStateException { if (cc == null) { throw new NullPointerException(); } if (!cc.isValid()) { throw new IllegalStateException("CacheContext is invalid"); } if (!(cc instanceof LWJGLCacheContext)) { throw new IllegalArgumentException("CacheContext object not from this renderer"); } LWJGLCacheContext lwjglCC = (LWJGLCacheContext) cc; if (lwjglCC.renderer != this) { throw new IllegalArgumentException("CacheContext object not from this renderer"); } this.cacheContext = lwjglCC; try { for (TextureArea ta : textureAreas) { ta.destroyRepeatCache(); } for (TextureAreaRotated tar : rotatedTextureAreas) { tar.destroyRepeatCache(); } } finally { textureAreas.clear(); rotatedTextureAreas.clear(); } } /** * Queries the current view port size & position and updates all related internal state. * * <p>It is important that the internal state matches the OpenGL viewport or clipping won't work * correctly. * * <p>This method should only be called when the viewport size has changed. It can have negative * impact on performance to call every frame. * * @see #getWidth() * @see #getHeight() */ public void syncViewportSize() { ib16.clear(); GL11.glGetInteger(GL11.GL_VIEWPORT, ib16); viewportX = ib16.get(0); width = ib16.get(2); height = ib16.get(3); viewportBottom = ib16.get(1) + height; } /** * Sets the viewport size & position. * * <p>This method is preferred over {@link #syncViewportSize() } as it avoids calling {@link * GL11#glGetInteger(int, java.nio.IntBuffer) }. * * @param x the X position (GL_VIEWPORT index 0) * @param y the Y position (GL_VIEWPORT index 1) * @param width the width (GL_VIEWPORT index 2) * @param height the height (GL_VIEWPORT index 3) */ public void setViewport(int x, int y, int width, int height) { this.viewportX = x; this.viewportBottom = y + height; this.width = width; this.height = height; } public long getTimeMillis() { long res = Sys.getTimerResolution(); long time = Sys.getTime(); if (res != 1000) { time = (time * 1000) / res; } return time; } protected void setupGLState() { GL11.glPushAttrib( GL11.GL_ENABLE_BIT | GL11.GL_TRANSFORM_BIT | GL11.GL_HINT_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_SCISSOR_BIT | GL11.GL_LINE_BIT | GL11.GL_TEXTURE_BIT); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPushMatrix(); GL11.glLoadIdentity(); GL11.glOrtho(0, width, height, 0, -1.0, 1.0); GL11.glMatrixMode(GL11.GL_MODELVIEW); GL11.glPushMatrix(); GL11.glLoadIdentity(); GL11.glEnable(GL11.GL_TEXTURE_2D); GL11.glEnable(GL11.GL_BLEND); GL11.glEnable(GL11.GL_LINE_SMOOTH); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); GL11.glDisable(GL11.GL_SCISSOR_TEST); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST); } protected void revertGLState() { GL11.glPopMatrix(); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPopMatrix(); GL11.glPopAttrib(); } /** Setup GL to start rendering the GUI. It assumes default GL state. */ public boolean startRendering() { if (width <= 0 || height <= 0) { return false; } prepareForRendering(); setupGLState(); RenderScale.doscale(); return true; } public void endRendering() { renderSWCursor(); RenderScale.descale(); revertGLState(); } /** * Call to revert the GL state to the state before calling {@link #startRendering()}. * * @see #resumeRendering() */ public void pauseRendering() { RenderScale.descale(); revertGLState(); } /** Resume rendering after a call to {@link #pauseRendering()}. */ public void resumeRendering() { hasScissor = false; setupGLState(); RenderScale.doscale(); setClipRect(); } public int getHeight() { return height; } public int getWidth() { return width; } /** * Retrieves the X position of the OpenGL viewport (index 0 of GL_VIEWPORT) * * @return the X position of the OpenGL viewport */ public int getViewportX() { return viewportX; } /** * Retrieves the Y position of the OpenGL viewport (index 1 of GL_VIEWPORT) * * @return the Y position of the OpenGL viewport */ public int getViewportY() { return viewportBottom - height; } public Font loadFont(URL url, StateSelect select, FontParameter... parameterList) throws IOException { if (url == null) { throw new NullPointerException("url"); } if (select == null) { throw new NullPointerException("select"); } if (parameterList == null) { throw new NullPointerException("parameterList"); } if (select.getNumExpressions() + 1 != parameterList.length) { throw new IllegalArgumentException("select.getNumExpressions() + 1 != parameterList.length"); } BitmapFont bmFont = activeCacheContext().loadBitmapFont(url); return new LWJGLFont(this, bmFont, select, parameterList); } public Texture loadTexture(URL url, String formatStr, String filterStr) throws IOException { LWJGLTexture.Format format = LWJGLTexture.Format.COLOR; LWJGLTexture.Filter filter = LWJGLTexture.Filter.NEAREST; if (formatStr != null) { try { format = LWJGLTexture.Format.valueOf(formatStr.toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException ex) { getLogger().log(Level.WARNING, "Unknown texture format: {0}", formatStr); } } if (filterStr != null) { try { filter = LWJGLTexture.Filter.valueOf(filterStr.toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException ex) { getLogger().log(Level.WARNING, "Unknown texture filter: {0}", filterStr); } } return load(url, format, filter); } public LineRenderer getLineRenderer() { return this; } public OffscreenRenderer getOffscreenRenderer() { return null; } public FontMapper getFontMapper() { return fontMapper; } /** * Installs a font mapper. It is the responsibility of the font mapper to manage the OpenGL state * correctly so that normal rendering by LWJGLRenderer is not disturbed. * * @param fontMapper the font mapper object - can be null. */ public void setFontMapper(FontMapper fontMapper) { this.fontMapper = fontMapper; } public DynamicImage createDynamicImage(int width, int height) { if (width <= 0) { throw new IllegalArgumentException("width"); } if (height <= 0) { throw new IllegalArgumentException("height"); } if (width > maxTextureSize || height > maxTextureSize) { getLogger() .log( Level.WARNING, "requested size {0} x {1} exceeds maximum texture size {3}", new Object[] {width, height, maxTextureSize}); return null; } int texWidth = width; int texHeight = height; ContextCapabilities caps = GLContext.getCapabilities(); boolean useTextureRectangle = caps.GL_EXT_texture_rectangle || caps.GL_ARB_texture_rectangle; if (!useTextureRectangle && !caps.GL_ARB_texture_non_power_of_two) { texWidth = nextPowerOf2(width); texHeight = nextPowerOf2(height); } // ARB and EXT versions use the same enum ! int proxyTarget = useTextureRectangle ? EXTTextureRectangle.GL_PROXY_TEXTURE_RECTANGLE_EXT : GL11.GL_PROXY_TEXTURE_2D; GL11.glTexImage2D( proxyTarget, 0, GL11.GL_RGBA, texWidth, texHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); ib16.clear(); GL11.glGetTexLevelParameter(proxyTarget, 0, GL11.GL_TEXTURE_WIDTH, ib16); if (ib16.get(0) != texWidth) { getLogger() .log( Level.WARNING, "requested size {0} x {1} failed proxy texture test", new Object[] {texWidth, texHeight}); return null; } // ARB and EXT versions use the same enum ! int target = useTextureRectangle ? EXTTextureRectangle.GL_TEXTURE_RECTANGLE_EXT : GL11.GL_TEXTURE_2D; int id = GL11.glGenTextures(); GL11.glBindTexture(target, id); GL11.glTexImage2D( target, 0, GL11.GL_RGBA, texWidth, texHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); GL11.glTexParameteri(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); GL11.glTexParameteri(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); LWJGLDynamicImage image = new LWJGLDynamicImage(this, target, id, width, height, texWidth, texHeight, Color.WHITE); dynamicImages.add(image); return image; } public Image createGradient(Gradient gradient) { return new GradientImage(this, gradient); } public void clipEnter(int x, int y, int w, int h) { clipStack.push(x, y, w, h); setClipRect(); } public void clipEnter(Rect rect) { clipStack.push(rect); setClipRect(); } public void clipLeave() { clipStack.pop(); setClipRect(); } public boolean clipIsEmpty() { return clipStack.isClipEmpty(); } public void setCursor(MouseCursor cursor) { try { swCursor = null; if (isMouseInsideWindow()) { if (cursor instanceof LWJGLCursor) { setNativeCursor(((LWJGLCursor) cursor).cursor); } else if (cursor instanceof SWCursor) { setNativeCursor(emptyCursor); swCursor = (SWCursor) cursor; } else { setNativeCursor(null); } } } catch (LWJGLException ex) { getLogger().log(Level.WARNING, "Could not set native cursor", ex); } } public void setMousePosition(int mouseX, int mouseY) { this.mouseX = mouseX; this.mouseY = mouseY; } public void setMouseButton(int button, boolean state) { swCursorAnimState.setAnimationState(button, state); } public LWJGLTexture load(URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter) throws IOException { return load(textureUrl, fmt, filter, null); } public LWJGLTexture load( URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter, TexturePostProcessing tpp) throws IOException { if (textureUrl == null) { throw new NullPointerException("textureUrl"); } LWJGLCacheContext cc = activeCacheContext(); if (tpp != null) { return cc.createTexture(textureUrl, fmt, filter, tpp); } else { return cc.loadTexture(textureUrl, fmt, filter); } } public void pushGlobalTintColor(float r, float g, float b, float a) { tintStack = tintStack.push(r, g, b, a); } public void popGlobalTintColor() { tintStack = tintStack.pop(); } /** * Pushes a white entry on the tint stack which ignores the previous tint color. It must be * removed by calling {@link #popGlobalTintColor()}. * * <p>This is useful when rendering to texture */ public void pushGlobalTintColorReset() { tintStack = tintStack.pushReset(); } /** * Calls GL11.glColor4f() with the specified color multiplied by the current global tint color. * * @param color the color to set */ public void setColor(Color color) { tintStack.setColor(color); } public void drawLine(float[] pts, int numPts, float width, Color color, boolean drawAsLoop) { if (numPts * 2 > pts.length) { throw new ArrayIndexOutOfBoundsException(numPts * 2); } if (numPts >= 2) { tintStack.setColor(color); GL11.glDisable(GL11.GL_TEXTURE_2D); if (useQuadsForLines) { drawLinesAsQuads(numPts, pts, width, drawAsLoop); } else { drawLinesAsLines(numPts, pts, width, drawAsLoop); } GL11.glEnable(GL11.GL_TEXTURE_2D); } } private void drawLinesAsLines(int numPts, float[] pts, float width, boolean drawAsLoop) { GL11.glLineWidth(width); GL11.glBegin(drawAsLoop ? GL11.GL_LINE_LOOP : GL11.GL_LINE_STRIP); for (int i = 0; i < numPts; i++) { GL11.glVertex2f(pts[i * 2 + 0], pts[i * 2 + 1]); } GL11.glEnd(); } private void drawLinesAsQuads(int numPts, float[] pts, float width, boolean drawAsLoop) { width *= 0.5f; GL11.glBegin(GL11.GL_QUADS); for (int i = 1; i < numPts; i++) { drawLineAsQuad(pts[i * 2 - 2], pts[i * 2 - 1], pts[i * 2 + 0], pts[i * 2 + 1], width); } if (drawAsLoop) { int idx = numPts * 2; drawLineAsQuad(pts[idx], pts[idx + 1], pts[0], pts[1], width); } GL11.glEnd(); } private static void drawLineAsQuad(float x0, float y0, float x1, float y1, float w) { float dx = x1 - x0; float dy = y1 - y0; float l = (float) Math.sqrt(dx * dx + dy * dy) / w; dx /= l; dy /= l; GL11.glVertex2f(x0 - dx + dy, y0 - dy - dx); GL11.glVertex2f(x0 - dx - dy, y0 - dy + dx); GL11.glVertex2f(x1 + dx - dy, y1 + dy + dx); GL11.glVertex2f(x1 + dx + dy, y1 + dy - dx); } protected void prepareForRendering() { hasScissor = false; tintStack = tintStateRoot; clipStack.clearStack(); } protected void renderSWCursor() { if (swCursor != null) { tintStack = tintStateRoot; swCursor.render(mouseX, mouseY); } } protected void setNativeCursor(Cursor cursor) throws LWJGLException { Mouse.setNativeCursor(cursor); } protected boolean isMouseInsideWindow() { return Mouse.isInsideWindow(); } protected void getTintedColor(Color color, float[] result) { result[0] = tintStack.r * color.getRed(); result[1] = tintStack.g * color.getGreen(); result[2] = tintStack.b * color.getBlue(); result[3] = tintStack.a * color.getAlpha(); } /** * Computes the tinted color from the given color. * * @param color the input color in RGBA order, value range is 0.0 (black) to 255.0 (white). * @param result the tinted color in RGBA order, can be the same array as color. */ protected void getTintedColor(float[] color, float[] result) { result[0] = tintStack.r * color[0]; result[1] = tintStack.g * color[1]; result[2] = tintStack.b * color[2]; result[3] = tintStack.a * color[3]; } public void setClipRect() { final Rect rect = clipRectTemp; if (clipStack.getClipRect(rect)) { GL11.glScissor( viewportX + rect.getX() * RenderScale.scale, viewportBottom - rect.getBottom() * RenderScale.scale, rect.getWidth() * RenderScale.scale, rect.getHeight() * RenderScale.scale); if (!hasScissor) { GL11.glEnable(GL11.GL_SCISSOR_TEST); hasScissor = true; } } else if (hasScissor) { GL11.glDisable(GL11.GL_SCISSOR_TEST); hasScissor = false; } } /** * Retrieves the active clip region from the top of the stack * * @param rect the rect coordinates - may not be updated when clipping is disabled * @return true if clipping is active, false if clipping is disabled */ public boolean getClipRect(Rect rect) { return clipStack.getClipRect(rect); } Logger getLogger() { return Logger.getLogger(LWJGLRenderer.class.getName()); } /** * If the passed value is not a power of 2 then return the next highest power of 2 otherwise the * value is returned unchanged. * * <p>Warren Jr., Henry S. (2002). Hacker's Delight. Addison Wesley. pp. 48. ISBN 978-0201914658 * * @param i a non negative number <= 2^31 * @return the smallest power of 2 which is >= i */ private static int nextPowerOf2(int i) { i--; i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i + 1; } private static class SWCursorAnimState implements AnimationState { private final long[] lastTime; private final boolean[] active; SWCursorAnimState() { lastTime = new long[3]; active = new boolean[3]; } void setAnimationState(int idx, boolean isActive) { if (idx >= 0 && idx < 3 && active[idx] != isActive) { lastTime[idx] = Sys.getTime(); active[idx] = isActive; } } public int getAnimationTime(StateKey state) { long curTime = Sys.getTime(); int idx = getMouseButton(state); if (idx >= 0) { curTime -= lastTime[idx]; } return (int) curTime & Integer.MAX_VALUE; } public boolean getAnimationState(StateKey state) { int idx = getMouseButton(state); if (idx >= 0) { return active[idx]; } return false; } public boolean getShouldAnimateState(StateKey state) { return true; } private int getMouseButton(StateKey key) { if (key == STATE_LEFT_MOUSE_BUTTON) { return Event.MOUSE_LBUTTON; } if (key == STATE_MIDDLE_MOUSE_BUTTON) { return Event.MOUSE_MBUTTON; } if (key == STATE_RIGHT_MOUSE_BUTTON) { return Event.MOUSE_RBUTTON; } return -1; } } }