package gminers.glasspane.component; import static lombok.AccessLevel.NONE; // i generally think static imports are bad, but this is used in clear context and shortens lines import gminers.glasspane.GlassPane; import gminers.glasspane.GlassPaneMirror; import gminers.glasspane.PaneBB; import gminers.glasspane.component.button.PaneButton; import gminers.glasspane.event.ComponentActivateEvent; import gminers.glasspane.event.ComponentTickEvent; import gminers.glasspane.event.KeyTypedEvent; import gminers.glasspane.event.MouseDownEvent; import gminers.glasspane.event.MouseUpEvent; import gminers.glasspane.event.MouseWheelEvent; import gminers.glasspane.event.PaneComponentPostRenderEvent; import gminers.glasspane.event.PaneComponentPreRenderEvent; import gminers.glasspane.event.PaneEvent; import gminers.glasspane.event.PaneEventListenerRegisterEvent; import gminers.glasspane.event.PaneEventListenerUnregisterEvent; import gminers.glasspane.event.WinchEvent; import gminers.glasspane.exception.PaneCantContinueError; import gminers.glasspane.listener.PaneEventHandler; import gminers.kitchensink.Rendering; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.FieldDefaults; import lombok.experimental.PackagePrivate; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; import com.gameminers.glasspane.internal.GlassPaneMod; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * Root class for all components that can be used in a PaneContainer, and make up the general UI of a GlassPane.<br/> * Yes, it extends PaneBB. Deal with it. * * @author Aesen Vismea * */ @FieldDefaults(level = AccessLevel.PRIVATE) @ToString(exclude = { "listeners", "parent" }) @Getter @Setter // TODO: this class is a bit oversized, needs refactoring public abstract class PaneComponent extends PaneBB { /** * The Z index of this component. Components with higher Z indexes render in front of components with lower. */ protected int zIndex = 0; /** * The angle of this component, in degrees. Only listened to if rotationAllowed is true. */ float angle = 0f; /** * The X multiplier for rotation */ float xRot = 0f; /** * The Y multiplier for rotation */ float yRot = 0f; /** * The Z multiplier for rotation (this points into the screen and is probably what you want) */ float zRot = 0f; /** * Whether or not rotation will be applied to this component. */ boolean rotationAllowed = false; /** * For debugging - if true, renders a solid color box encompassing this entire component, with it's color being this component's * identityHashCode. */ boolean drawBoundingBox = false; /** * Whether or not this component will render. */ boolean visible = true; /** * Whether or not to clip rendering of this component to it's bounding box.<br/> * * @deprecated Clip To Size is very buggy and not worth the effort required to fix it properly. It is still used and maintained as some * PaneComponents rely on it, but as such Clip To Size functionality may contain PaneComponent-specific workarounds and * other such quirks, and it should not be depended upon for third-party components. */ @Deprecated boolean clipToSize = false; /** * Whether or not to listen to the relative size set by relativeWidth. If this is true, width will be updated whenever the parent * container is resized. */ boolean autoResizeWidth = false; /** * Whether or not to listen to the relative size set by relativeHeight. If this is true, height will be updated whenever the parent * container is resized. */ boolean autoResizeHeight = false; /** * The multiplier to be applied to the parent container's width when autoResize is enabled and a WinchEvent is fired. */ double relativeWidth = 1.0; /** * The multiplier to be applied to the parent container's height when autoResize is enabled and a WinchEvent is fired. */ double relativeHeight = 1.0; /** * An offset to be applied to this component's width when it is resized due to autoResize. */ int relativeWidthOffset = 0; /** * An offset to be applied to this component's height when it is resized due to autoResize. */ int relativeHeightOffset = 0; /** * Whether or not to listen to the relative position set by relativeX. If this is true, X will be updated whenever the parent * container is resized. */ boolean autoPositionX = false; /** * Whether or not to listen to the relative position set by relativeY. If this is true, Y will be updated whenever the parent * container is resized. */ boolean autoPositionY = false; /** * The multiplier to be applied to the parent container's width when autoResize is enabled and a WinchEvent is fired. */ double relativeX = 1.0; /** * The multiplier to be applied to the parent container's height when autoResize is enabled and a WinchEvent is fired. */ double relativeY = 1.0; /** * An offset to be applied to this component's width when it is resized due to autoResize. */ int relativeXOffset = 0; /** * An offset to be applied to this component's height when it is resized due to autoResize. */ int relativeYOffset = 0; /** * Whether or not a ComponentActivateEvent should be fired when this component is clicked. */ boolean activatedOnClick = true; /** * The tooltip to show when the mouse hovers over this component for a while.<br/> * Null is acceptable, and suppresses the tooltip. Newlines are allowed. */ @Setter(NONE) String tooltip = null; /** * The font renderer to use for the tooltip. */ FontRenderer tooltipFontRenderer = Minecraft.getMinecraft().fontRendererObj; /** * The distance to translate the position of this component on the X axis, in 'big' pixels. */ protected float translateX = 0f; /** * The distance to translate the position of this component on the Y axis, in 'big' pixels. */ protected float translateY = 0f; @Getter(NONE) @Setter(NONE) protected static final ResourceLocation RESOURCE = new ResourceLocation("glasspane", "wadjets.png"); /** * The parent of this component. */ @PackagePrivate @Setter(NONE) PaneContainer parent = null; @Getter(NONE) @Setter(NONE) protected int mouseX; @Getter(NONE) @Setter(NONE) protected int mouseY; @Getter(NONE) @Setter(NONE) private List<String> tooltipSplit = null; @Getter(NONE) @Setter(NONE) protected Map<String, String> metadata = Maps.newHashMap(); @Getter(NONE) @Setter(NONE) protected Map<Class<? extends PaneEvent>, Map<Object, List<Method>>> listeners = new HashMap<Class<? extends PaneEvent>, Map<Object, List<Method>>>(); public PaneComponent() { registerListeners(this); } public String getName() { return getMetadata("name"); } public void setName(String name) { putMetadata("name", name); } public void putMetadata(final String k, final String v) { metadata.put(k, v); } public void removeMetadata(final String k) { metadata.remove(k); } public String getMetadata(final String k) { return metadata.get(k); } public boolean hasMetadata(final String k) { return metadata.containsKey(k); } /** * Shortcut to set autoResizeWidth and autoResizeHeight at the same time. */ public void setAutoResize(final boolean autoResize) { autoResizeHeight = autoResizeWidth = autoResize; } /** * Shortcut to set autoPositionX and autoPositionY at the same time. */ public void setAutoPosition(final boolean autoPosition) { autoPositionX = autoPositionY = autoPosition; } /** * The tooltip to show when the mouse hovers over this component for a while.<br/> * Null is acceptable, and suppresses the tooltip. Newlines are allowed. */ public void setTooltip(final String tooltip) { this.tooltip = tooltip; if (tooltip == null) { tooltipSplit = null; } else { tooltipSplit = Lists.newArrayList(tooltip.split("\n")); } } protected final boolean isListeningForEvent(final Class<? extends PaneEvent> eventClass) { return listeners.containsKey(eventClass) && listeners.get(eventClass).size() > 0; } /** * Creates a new PaneBB that copies this component's x, y, width, and height. If you do not need to modify the returned PaneBB, it is * more efficient to use PaneComponent as if it were a PaneBB, as it avoids unnecessary object creation. * * @return A strictly PaneBB copy of this PaneComponent's bounds. */ public PaneBB getBounds() { return new PaneBB(this); } /** * Renders this Component, applying all needed transformations, and then calling doRender. This method will revert all transformations * it does afterward.<br/> * * @param mouseX * The X coordinate of the mouse, in 'big' pixels. * @param mouseY * The Y coordinate of the mouse, in 'big' pixels. * @param partialTicks * The amount of the way into the next tick we are, since frames do not align with ticks. */ public final void render(final int mouseX, final int mouseY, final float partialTicks) { // just return if we aren't visible if (!visible) return; // push a matrix so we can easily revert GL11.glPushMatrix(); // draw a bounding box if asked, for debugging purposes if (drawBoundingBox) { Rendering.drawRect(x, y, x + width, y + height, ~(System.identityHashCode(this) | 0xFF000000)); Rendering.drawRect(x + 1, y + 1, x + (width - 2), y + (height - 2), System.identityHashCode(this) | 0xFF000000); } // only do position transformations if we're a component if (!(this instanceof GlassPane)) { // clip to this component's size if (clipToSize) { final GlassPane pane = getGlassPane(); GL11.glScissor(getAbsoluteX(pane.getWidth()), getAbsoluteY(pane.getHeight()), getAbsoluteWidth(pane.getWidth()), getAbsoluteHeight(pane.getHeight())); GL11.glEnable(GL11.GL_SCISSOR_TEST); } // translate to this component's coordinates GL11.glTranslatef(x, y, zIndex); } else if (((GlassPane) this).isScreenClearedBeforeDrawing() && currentScreenIsThis()) { GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); } GL11.glTranslatef(translateX, translateY, 0); // if we're a pane and have a shadowbox, this will be true final boolean renderShadowbox = this instanceof GlassPane && !((GlassPane) this).getScreenMirror().isModal() && ((GlassPane) this).getShadowbox() != null && (currentScreenIsThis() || ((GlassPane) this).isTakingOver()); // if we don't want shadowboxes to be rotated, render it here if (renderShadowbox && !((GlassPane) this).isShadowboxRotationAllowed()) { ((GlassPane) this).getShadowbox().render(mouseX, mouseY, partialTicks); } // apply rotation, if wanted if (angle != 0 && rotationAllowed) { GL11.glRotatef(angle, xRot, yRot, zRot); } // if we do want shadowboxes to be rotated, render it here if (renderShadowbox && ((GlassPane) this).isShadowboxRotationAllowed()) { ((GlassPane) this).getShadowbox().render(mouseX, mouseY, partialTicks); } // set the fields, for doTick logic this.mouseX = mouseX; this.mouseY = mouseY; // perform the render performRender(partialTicks); // pop the matrix to revert to the previous state, and disable scissor test if (clipToSize) { GL11.glDisable(GL11.GL_SCISSOR_TEST); } GL11.glPopMatrix(); GL11.glPushMatrix(); if (hoverTime >= 30 && tooltip != null) { // render a tooltip if we should GL11.glTranslatef(0, 0, 5f); Rendering.drawHoveringText(tooltipSplit, mouseX, mouseY, tooltipFontRenderer); } GL11.glPopMatrix(); } /** * Gets the GlassPane ancestor of this Component. * * @return The GlassPane at the root of the hierarchy this component is within, or null if this component is orphaned (or one of it's * parents is orphaned) */ public GlassPane getGlassPane() { PaneComponent work = this; while (!(work instanceof GlassPane)) { work = work.getParent(); if (work == null) { break; } } if (work instanceof GlassPane) return (GlassPane) work; else return null; } public int getChainX() { return getX() + (getParent() == null ? 0 : getParent().getPX()); } public int getChainY() { return getY() + (getParent() == null ? 0 : getParent().getPY()); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected int getAbsoluteX(final int scaledWidth) { return getAbsoluteX(getChainX(), scaledWidth); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected static int getAbsoluteX(final int x, final int scaledWidth) { return (int) Math.floor(((double) x / ((double) scaledWidth)) * Minecraft.getMinecraft().displayWidth); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected int getAbsoluteY(final int scaledHeight) { return getAbsoluteY(getChainY(), height, scaledHeight); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected static int getAbsoluteY(final int y, final int height, final int scaledHeight) { return (Minecraft.getMinecraft().displayHeight - getAbsoluteHeight(height, scaledHeight)) - (int) Math.ceil(((double) y / ((double) scaledHeight)) * Minecraft.getMinecraft().displayHeight); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected int getAbsoluteEdgeY(final int scaledHeight) { return getAbsoluteY(getEdgeY(), height, scaledHeight) - (getParent() == null ? 0 : getParent().getAbsoluteEdgeY(scaledHeight)); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected int getAbsoluteWidth(final int scaledWidth) { return getAbsoluteWidth(width, scaledWidth); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected static int getAbsoluteWidth(final int width, final int scaledWidth) { return (int) Math.ceil(((double) width / ((double) scaledWidth)) * Minecraft.getMinecraft().displayWidth) + 1; } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected int getAbsoluteHeight(final int scaledHeight) { return getAbsoluteHeight(height, scaledHeight); } /** * Internal utility method for transforming GlassPane/Minecraft coords to GL coords */ protected static int getAbsoluteHeight(final int height, final int scaledHeight) { return (int) Math.ceil(((double) height / ((double) scaledHeight)) * Minecraft.getMinecraft().displayHeight); } private boolean currentScreenIsThis() { if (((GlassPane) this).isTakingOver()) return true; final Minecraft mc = Minecraft.getMinecraft(); if (mc.currentScreen != null) { if (mc.currentScreen instanceof GlassPaneMirror) return ((GlassPaneMirror) mc.currentScreen).getMirrored() == this; } return false; } private void performRender(final float partialTicks) { if (clipToSize) { // save the scissor box so we can revert it in case the component's render method changes it (e.g. containers) GL11.glPushAttrib(GL11.GL_SCISSOR_BIT); } GL11.glPushMatrix(); // fire a pre-render event fireEvent(PaneComponentPreRenderEvent.class, this, mouseX, mouseY, partialTicks); // render the component doRender(mouseX, mouseY, partialTicks); // and fire a post-render event. fireEvent(PaneComponentPostRenderEvent.class, this, mouseX, mouseY, partialTicks); GL11.glPopMatrix(); if (clipToSize) { // restore the scissor box GL11.glPopAttrib(); } } /** * Renders this Component. When this method is called, a clip and transform have already been applied to the GL context and the receiver * does not have to worry about applying transforms or going out of bounds. * * @param mouseX * The X coordinate of the mouse, in 'big' pixels. * @param mouseY * The Y coordinate of the mouse, in 'big' pixels. * @param partialTicks * The amount of the way into the next tick we are, since frames do not align with ticks. */ protected abstract void doRender(final int mouseX, final int mouseY, final float partialTicks); /** * Registers all methods annotated with {@link PaneEventHandler} as event listeners for their * argument. * * @param o * The object to register */ public final void registerListeners(final Object o) { for (final Method m : o.getClass().getMethods()) { // first check if we have the @PaneEventHandler annotation if (m.getAnnotation(PaneEventHandler.class) != null) { // if we do, see if the method has only one parameter if (m.getParameterTypes().length == 1) { // and that that parameter can be cast to a PaneEvent if (PaneEvent.class.isAssignableFrom(m.getParameterTypes()[0])) { // first we'll cast the parameter class, which should be safe given the above check @SuppressWarnings("unchecked") final Class<? extends PaneEvent> eventClass = (Class<? extends PaneEvent>) m .getParameterTypes()[0]; // now we'll grab the objects map Map<Object, List<Method>> objects; if (listeners.containsKey(eventClass) && listeners.get(eventClass) != null) { // if it exists, use it objects = listeners.get(eventClass); } else { // otherwise make a new one objects = Maps.newHashMap(); listeners.put(eventClass, objects); } // now grab the methods list List<Method> methodList; if (objects.containsKey(o) && objects.get(o) != null) { // use it if it exists methodList = objects.get(o); } else { // or make a new one. methodList = Lists.newArrayList(); objects.put(o, methodList); } // now add the current method to the list methodList.add(m); // and fire an event for the registration fireEvent(PaneEventListenerRegisterEvent.class, this, o, m); } else { // not a PaneEvent, print an error and continue System.err .println("[GlassPane] [EventSystem] Found a method with incorrect parameter types when registering listener " + o.getClass().getName()); } } else { // method has more than one parameter, or no parameters. print an error and continue System.err .println("[GlassPane] [EventSystem] Found a method with an incorrect number of parameters when registering listener " + o.getClass().getName()); } } } } /** * Unregisters all methods associated with the passed object. * * @param o * The object to unregister */ public final void unregisterListeners(final Object o) { List<Class<? extends PaneEvent>> needsRemoval = null; boolean didSomething = false; for (final Entry<Class<? extends PaneEvent>, Map<Object, List<Method>>> en : listeners.entrySet()) { // do we have a map in this? if (en.getValue() != null) { // we do. does it contain our object? if (en.getValue().containsKey(o)) { // it does. en.getValue().remove(o); didSomething = true; // do we still have some entries in here? if (en.getValue().isEmpty()) { // no, we don't. if (needsRemoval == null) { needsRemoval = Lists.newArrayList(); } // add this event class to the removal list to prevent having extra maps needsRemoval.add(en.getKey()); } } } } // are some entries pending removal? if (needsRemoval != null) { // yes, so let's remove them for (final Class<? extends PaneEvent> clazz : needsRemoval) { listeners.remove(clazz); } } // fire an event for the unregistration, if we actually did something. if (didSomething) { // we only call this once per object instead of once per method like in register because we would get a concurrent modification // exception if we did it in the above for loop. fireEvent(PaneEventListenerUnregisterEvent.class, o); } } /** * Fires an event of type <code>eventClass</code> to all listeners listening for that event type. Does not create an event object if * there are no listeners for this event. * * @param eventClass * The class of the event to fire. * @param constructorArgs * The arguments to pass to the event's constructor. * @return The instantiated event, or <code>null</code> if an error occurred or an event did not need to be instantiated. */ public <T extends PaneEvent> T fireEvent(final @NonNull Class<T> eventClass, final Object... constructorArgs) { // this is a bit hacky, but the only good way to forward into protected from outside the package w/o reflecting if (eventClass == KeyTypedEvent.class) { keyPressed((Character) constructorArgs[1], (Integer) constructorArgs[2]); } else if (eventClass == MouseDownEvent.class) { mouseDown((Integer) constructorArgs[1], (Integer) constructorArgs[2], (Integer) constructorArgs[3]); } else if (eventClass == MouseUpEvent.class) { mouseUp((Integer) constructorArgs[1], (Integer) constructorArgs[2], (Integer) constructorArgs[3]); } else if (eventClass == MouseWheelEvent.class) { mouseWheel((Integer) constructorArgs[1], (Integer) constructorArgs[2], (Integer) constructorArgs[3]); } else if (eventClass == WinchEvent.class) { winch((Integer) constructorArgs[1], (Integer) constructorArgs[2], (Integer) constructorArgs[3], (Integer) constructorArgs[4]); } // first of all, to save objects, we're going to check if this event is being listened for on this object. if (!isListeningForEvent(eventClass)) return null; // if not, just return and don't create any event objects. this is good for // high-frequency events. // now we'll create an instance... i hate how many lines all the exception garbage takes. Class<?>[] constructorTypes; try { constructorTypes = (Class<?>[]) eventClass.getField("SIGNATURE").get(null); } catch (final SecurityException e) { e.printStackTrace(); if (System.getSecurityManager() == null) { GlassPaneMod.inst .getLog() .error("[GlassPane] [EventSystem] A SecurityException was thrown, but there's no SecurityManager registered..."); } else throw new PaneCantContinueError("Security manager (" + System.getSecurityManager().getClass().getName() + ") prevents proper operation of the GlassPane event system!", e); return null; } catch (final IllegalAccessException e) { e.printStackTrace(); GlassPaneMod.inst.getLog().error( "[GlassPane] [EventSystem] Event class " + eventClass.getName() + "'s SIGNATURE field is non-public!"); return null; } catch (final NoSuchFieldException e) { e.printStackTrace(); GlassPaneMod.inst.getLog().error( "[GlassPane] [EventSystem] Event class " + eventClass.getName() + " does not declare static field SIGNATURE!"); return null; } T event; try { event = eventClass.getConstructor(constructorTypes).newInstance(constructorArgs); } catch (final SecurityException e) { e.printStackTrace(); if (System.getSecurityManager() == null) { GlassPaneMod.inst .getLog() .error("[GlassPane] [EventSystem] A SecurityException was thrown, but there's no SecurityManager registered..."); return null; } else throw new PaneCantContinueError("Security manager (" + System.getSecurityManager().getClass().getName() + ") prevents proper operation of the GlassPane event system!", e); } catch (final NoSuchMethodException e) { e.printStackTrace(); final StringBuilder types = new StringBuilder(); for (int i = 0; i < constructorTypes.length; i++) { types.append(constructorTypes[i].getName()); if (i == constructorTypes.length - 2) { types.append(" and "); } else if (i < constructorTypes.length - 2) { types.append(", "); } } GlassPaneMod.inst.getLog().error( "[GlassPane] [EventSystem] No constructor for event class " + eventClass.getName() + " matching the call spec of " + types + "!"); return null; } catch (Exception e) { e.printStackTrace(); return null; } // now let's fire it for (final Entry<Object, List<Method>> en : listeners.get(eventClass).entrySet()) { for (final Method method : en.getValue()) { if (method.getAnnotation(PaneEventHandler.class).ignoreConsumed() && event.isConsumed()) { continue; } try { method.setAccessible(true); // you're fired method.invoke(en.getKey(), event); } catch (final IllegalArgumentException e) { e.printStackTrace(); GlassPaneMod.inst.getLog().error( "[GlassPane] [EventSystem] Cannot properly invoke method for event class " + eventClass.getName() + " and listener class " + en.getKey().getClass().getName() + "!"); } catch (final IllegalAccessException e) { e.printStackTrace(); GlassPaneMod.inst.getLog().error( "[GlassPane] [EventSystem] No permission to invoke method for event class " + eventClass.getName() + " and listener class " + en.getKey().getClass().getName() + "!"); } catch (final InvocationTargetException e) { e.printStackTrace(); GlassPaneMod.inst.getLog().error( "[GlassPane] [EventSystem] Invocation of method for event class " + eventClass.getName() + " and listener class " + en.getKey().getClass().getName() + " threw an exception!"); } catch (final SecurityException e) { e.printStackTrace(); if (System.getSecurityManager() == null) { GlassPaneMod.inst .getLog() .error("[GlassPane] [EventSystem] A SecurityException was thrown, but there's no SecurityManager registered..."); return null; } else throw new PaneCantContinueError("Security manager (" + System.getSecurityManager().getClass().getName() + ") prevents proper operation of the GlassPane event system!", e); } } } // and finally return it return event; } @Override public void setHeight(final int height) { if (height != this.height) { fireEvent(WinchEvent.class, this, this.width, this.height, this.width, height); } super.setHeight(height); } @Override public void setWidth(final int width) { if (width != this.width) { fireEvent(WinchEvent.class, this, this.width, this.height, width, this.height); } super.setWidth(width); } /** * Activates this component. Equivalent to clicking on the component or pressing Enter while it has the focus. */ public void activate() { fireEvent(ComponentActivateEvent.class, this); } @Getter(NONE) @Setter(NONE) private int hoverTime = 0; public final void tick() { fireEvent(ComponentTickEvent.class, this); if (this instanceof PaneContainer) { for (final PaneComponent c : ((PaneContainer) this).components) { c.tick(); } } if (Display.isActive() && ((this instanceof PaneButton) ? ((PaneButton) this).isEnabled() : true) && withinBounds(mouseX, mouseY)) { hoverTime++; } else { hoverTime = 0; } doTick(); } /** * Shortcut for components to be able to get certain events without needing to create objects. */ protected void mouseDown(final int mouseX, final int mouseY, final int button) {} /** * Shortcut for components to be able to get certain events without needing to create objects. */ protected void mouseUp(final int mouseX, final int mouseY, final int button) {} /** * Shortcut for components to be able to get certain events without needing to create objects. */ protected void mouseWheel(final int mouseX, final int mouseY, final int distance) {} /** * Shortcut for components to be able to get certain events without needing to create objects. */ protected void keyPressed(final char keyChar, final int keyCode) {} /** * Shortcut for components to be able to get certain events without needing to create objects. */ protected void winch(final int oldWidth, final int oldHeight, final int newWidth, final int newHeight) {} /** * Called every tick. Good for doing animation. */ protected void doTick() {} }