package hunternif.mc.atlas.client.gui.core;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.RenderHelper;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.GL11;
import java.io.IOException;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Core visual component class, which facilitates hierarchy. You can add child
* GuiComponent's to it, and they will be rendered, notified about mouse and
* keyboard events, window resize and will be moved around together with the
* parent component.
*/
@SideOnly(Side.CLIENT)
public class GuiComponent extends GuiScreen {
private GuiComponent parent = null;
private final List<GuiComponent> children = new CopyOnWriteArrayList<>();
/** The component's own size. */
int properWidth;
int properHeight;
/** The component's total calculated size, including itself and its children. */
int contentWidth;
int contentHeight;
/** If true, content size will be validated on the next update. */
private boolean sizeIsInvalid = false;
/** If true, this GUI will not be rendered. */
private boolean isClipped = false;
/** This flag is updated on every mouse event. */
protected boolean isMouseOver = false;
/** If true, mouse actions will only affect this GUI and its children,
* else they will only affect the in-game controller. */
private boolean interceptsMouse = true;
/** If true, pressing keyboard keys will affect this GUI, it's children,
* and the in-game controller. */
private boolean interceptsKeyboard = true;
/** These flags are set temporarily when this GUI has finished handling
* input and won't let the parent handle it, after which they are reset.
* This won't prevent the sibling children of this GUI from handling input.*/
private boolean hasHandledKeyboard = false, hasHandledMouse = false;
/** If true, no input is handled by the parent or any sibling GUIs. */
private boolean blocksScreen = false;
/** guiX and guiY are absolute coordinates on the screen. */
private int guiX = 0, guiY = 0;
/** Set absolute coordinates of the top left corner of this component on
* the screen. If this GUI has a parent, its size will be invalidated. */
public void setGuiCoords(int x, int y) {
int dx = x - guiX;
int dy = y - guiY;
this.guiX = x;
this.guiY = y;
for (GuiComponent child : children) {
child.offsetGuiCoords(dx, dy);
}
if (parent != null && (dx != 0 || dy != 0)) {
parent.invalidateSize();
}
}
/** Set coordinates relative to the parent's (or to the screen, if none)
* top left corner. */
public final void setRelativeCoords(int x, int y) {
if (parent != null) {
setGuiCoords(parent.getGuiX() + x, parent.getGuiY() + y);
} else {
setGuiCoords(x, y);
}
}
/** Set x coordinate relative to the parent's (or the screen, if none) left. */
public final void setRelativeX(int x) {
if (parent != null) {
setGuiCoords(parent.getGuiX() + x, guiY);
} else {
setGuiCoords(x, guiY);
}
}
/** Set y coordinate relative to the parent's (or the screen, if none) top. */
public final void setRelativeY(int y) {
if (parent != null) {
setGuiCoords(guiX, parent.getGuiY() + y);
} else {
setGuiCoords(guiX, y);
}
}
/** Offset the component's coordinates by the given values. If the component
* has only just been added to a parent component, the result will be the
* same as setRelativeGuiCoords(). */
public final void offsetGuiCoords(int dx, int dy) {
setGuiCoords(guiX + dx, guiY + dy);
}
/** Position this component in the center of its parent. */
protected final void setCentered() {
validateSize();
if (parent == null) {
setGuiCoords((this.width - getWidth()) / 2, (this.height - getHeight()) / 2);
} else {
setRelativeCoords((parent.getWidth() - getWidth())/2, (parent.getHeight() - getHeight())/2);
}
}
/** Absolute X coordinate on the screen. */
public int getGuiX() {
return guiX;
}
/** Absolute Y coordinate on the screen. */
public int getGuiY() {
return guiY;
}
/** X coordinate relative to the parent's top left corner. */
int getRelativeX() {
return parent == null ? guiX : (guiX - parent.guiX);
}
/** Y coordinate relative to the parent's top left corner. */
int getRelativeY() {
return parent == null ? guiY : (guiY - parent.guiY);
}
/** Set this component's own size. This shouldn't affect the size or position of the children. */
protected void setSize(int width, int height) {
this.properWidth = width;
this.properHeight = height;
this.contentWidth = width;
this.contentHeight = height;
invalidateSize();
}
/** Adds the child component to this GUI's content and initializes it.
* The child is placed at the top left corner of this component.
* @return the child added. */
protected GuiComponent addChild(GuiComponent child) {
doAddChild(null, child, null);
return child;
}
/** Adds the child component to this GUI's content and initializes it.
* The child is placed in the list immediately after the specified child,
* which is equivalent to putting it in front of that child in Z-order.
* The child is placed at the top left corner of this component.
* @return the child added. */
public GuiComponent addChildInfrontOf(GuiComponent inFrontOf, GuiComponent child) {
doAddChild(inFrontOf, child, null);
return child;
}
/** Adds the child component to this GUI's content and initializes it.
* The child is placed in the list immediately before the specified child,
* which is equivalent to putting it behind that child in Z-order.
* The child is placed at the top left corner of this component.
* @return the child added. */
protected GuiComponent addChildBehind(GuiComponent behind, GuiComponent child) {
doAddChild(null, child, behind);
return child;
}
private void doAddChild(GuiComponent inFrontOf, GuiComponent child, GuiComponent behind) {
if (child == null || children.contains(child) || parent == child) {
return;
}
int i = children.indexOf(inFrontOf);
if (i == -1) {
int j = children.indexOf(behind);
if (j == -1) {
children.add(child);
} else {
children.add(j, child);
}
} else {
children.add(i + 1, child);
}
child.parent = this;
child.setGuiCoords(guiX, guiY);
if (mc != null) {
child.setWorldAndResolution(mc, width, height);
}
invalidateSize();
}
/** @return the child removed. */
protected GuiComponent removeChild(GuiComponent child) {
if (child != null && children.contains(child)) {
child.parent = null;
children.remove(child);
invalidateSize();
onChildClosed(child);
}
return child;
}
void removeAllChildren() {
children.clear();
invalidateSize();
}
/** Null if this is a top-level GUI. */
public GuiComponent getParent() {
return parent;
}
List<GuiComponent> getChildren() {
return children;
}
/** If true, mouse actions will only affect this GUI and its children,
* else they will only affect the in-game controller. */
public void setInterceptMouse(boolean value) {
this.interceptsMouse = value;
this.allowUserInput = !interceptsMouse | !interceptsKeyboard;
}
/** If true, pressing keyboard keys will affect this GUI, it's children,
* and the in-game controller. */
protected void setInterceptKeyboard(boolean value) {
this.interceptsKeyboard = value;
this.allowUserInput = !interceptsMouse | !interceptsKeyboard;
}
@Override
public void handleInput() throws IOException {
// Traverse children backwards, because the topmost child should be the
// first to process input:
ListIterator<GuiComponent> iter = children.listIterator(children.size());
while(iter.hasPrevious()) {
GuiComponent child = iter.previous();
if (child.blocksScreen) {
child.handleInput();
isMouseOver = false;
return;
}
}
if (interceptsMouse) {
while (Mouse.next()) {
this.handleMouseInput();
}
}
if (interceptsKeyboard) {
while (Keyboard.next()) {
this.handleKeyboardInput();
}
}
}
/** Call this method from within {@link #handleMouseInput()} (or other
* mouse-processing methods) if the input has been handled by this GUI
* andshouldn't be handled by its parents.
* This won't prevent the sibling children of this GUI from handling input. */
void mouseHasBeenHandled() {
this.hasHandledMouse = true;
}
/** Call this method from within {@link #handleKeyboardInput()} if the input
* has been handled by this GUI and shouldn't be handled by its parents.
* This won't prevent the sibling children of this GUI from handling input. */
protected void keyboardHasBeenHandled() {
this.hasHandledKeyboard = true;
}
/** If true, no input is handled by the parent or any sibling GUIs. */
protected void setBlocksScreen(boolean value) {
this.blocksScreen = value;
}
/** Handle mouse input for this GUI and its children. */
@Override
public void handleMouseInput() throws IOException {
boolean handled = false;
isMouseOver = false;
// Traverse children backwards, because the topmost child should be the
// first to process input:
ListIterator<GuiComponent> iter = children.listIterator(children.size());
while(iter.hasPrevious()) {
GuiComponent child = iter.previous();
child.handleMouseInput();
if (child.hasHandledMouse) {
child.hasHandledMouse = false;
handled = true;
}
}
if (!handled) {
isMouseOver = isMouseInRegion(getGuiX(), getGuiY(), getWidth(), getHeight());
super.handleMouseInput();
}
}
/** Handle keyboard input for this GUI and its children. */
@Override
public void handleKeyboardInput() throws IOException {
boolean handled = false;
// Traverse children backwards, because the topmost child should be the
// first to process input:
ListIterator<GuiComponent> iter = children.listIterator(children.size());
while(iter.hasPrevious()) {
GuiComponent child = iter.previous();
child.handleKeyboardInput();
if (child.hasHandledKeyboard) {
child.hasHandledKeyboard = false;
handled = true;
}
}
if (!handled) {
if (Keyboard.getEventKeyState()) {
this.keyTyped(Keyboard.getEventCharacter(), Keyboard.getEventKey());
}
}
}
@Override
protected void keyTyped(char typedChar, int keyCode) throws IOException {
if (keyCode == 1 && mc.currentScreen != null)
{
this.mc.displayGuiScreen((GuiScreen)null);
if (this.mc.currentScreen == null)
{
this.mc.setIngameFocus();
}
}
}
/** Render this GUI and its children. */
@Override
public void drawScreen(int mouseX, int mouseY, float partialTick) {
super.drawScreen(mouseX, mouseY, partialTick);
for (GuiComponent child : children) {
if (!child.isClipped) {
child.drawScreen(mouseX, mouseY, partialTick);
}
}
// Draw any hovering text requested by child components:
if (hoveringTextInfo.shouldDraw) {
drawHoveringText2(hoveringTextInfo.lines, hoveringTextInfo.x, hoveringTextInfo.y, hoveringTextInfo.font);
hoveringTextInfo.shouldDraw = false;
}
}
/** Called when the GUI is unloaded, called for each child as well. */
@Override
public void onGuiClosed() {
for (GuiComponent child : children) {
child.onGuiClosed();
}
super.onGuiClosed();
}
/** Called each in-game tick for this GUI and its children. If this GUI's
* size has been invalidated, it will be validated on the next update. */
@Override
public void updateScreen() {
for (GuiComponent child : children) {
child.updateScreen();
}
super.updateScreen();
if (sizeIsInvalid) {
validateSize();
}
}
@Override
public void setWorldAndResolution(Minecraft mc, int width, int height) {
super.setWorldAndResolution(mc, width, height);
for (GuiComponent child : children) {
child.setWorldAndResolution(mc, width, height);
}
}
/** Width of the GUI or its contents. This method may be called often so it
* should be fast. */
protected int getWidth() {
return contentWidth;
}
/** Height of the GUI or its contents. This method may be called often so it
* should be fast. */
protected int getHeight() {
return contentHeight;
}
/** If set to true, the parent of this GUI will not render it. */
void setClipped(boolean value) {
this.isClipped = value;
}
/** Cause the size of the component to be recalculate on the next update
* tick. If this GUI has a parent, the parent's size will be invalidated too. */
private void invalidateSize() {
sizeIsInvalid = true;
if (parent != null) {
parent.invalidateSize();
}
}
/** Recalculate the dimensions of the contents (children) of this GUI. */
void validateSize() {
int leftmost = Integer.MAX_VALUE;
int rightmost = Integer.MIN_VALUE;
int topmost = Integer.MAX_VALUE;
int bottommost = Integer.MIN_VALUE;
for (GuiComponent child : children) {
int x = child.getGuiX();
if (x < leftmost) {
leftmost = x;
}
int childWidth = child.getWidth();
if (x + childWidth > rightmost) {
rightmost = x + childWidth;
}
int y = child.getGuiY();
if (y < topmost) {
topmost = y;
}
int childHeight = child.getHeight();
if (y + childHeight > bottommost) {
bottommost = y + childHeight;
}
}
contentWidth = Math.max(properWidth, rightmost - leftmost);
contentHeight = Math.max(properHeight, bottommost - topmost);
sizeIsInvalid = false;
}
/** Returns true, if the mouse cursor is within the specified bounds.
* Note: left and top are absolute. */
boolean isMouseInRegion(int left, int top, int width, int height) {
int mouseX = getMouseX();
int mouseY = getMouseY();
return mouseX >= left && mouseX < left + width && mouseY >= top && mouseY < top + height;
}
/**
* Returns true if the mouse cursor is within a rectangular box of the specified
* size with its center at the specified point.
* @param x center of the box, absolute
* @param y center of the box, absolute
* @param radius half the side of the box
*/
protected boolean isMouseInRadius(int x, int y, int radius) {
int mouseX = getMouseX();
int mouseY = getMouseY();
return mouseX >= x - radius && mouseX < x + radius && mouseY >= y - radius && mouseY < y + radius;
}
/** Draws a standard Minecraft hovering text window, constrained by this
* component's dimensions (i.e. if it won't fit in when drawn to the left
* of the cursor, it will be drawn to the right instead). */
private void drawHoveringText2(List<String> lines, int x, int y, FontRenderer font) {
if (!lines.isEmpty()) {
// Stencil test is used by VScrollingComponent to hide the content
// that is currently outside the viewport; that shouldn't affect
// hovering text though.
boolean stencilEnabled = GL11.glIsEnabled(GL11.GL_STENCIL_TEST);
if (stencilEnabled) GL11.glDisable(GL11.GL_STENCIL_TEST);
RenderHelper.disableStandardItemLighting();
int k = 0;
for (String s : lines) {
int l = font.getStringWidth(s);
if (l > k) {
k = l;
}
}
int i1 = x + 12;
int j1 = y - 12;
int k1 = 8;
if (lines.size() > 1) {
k1 += 2 + (lines.size() - 1) * 10;
}
if (i1 + k > width) {
i1 -= 28 + k;
}
if (j1 + k1 + 6 > height) {
j1 = height - k1 - 6;
}
int l1 = -267386864;
this.drawGradientRect(i1 - 3, j1 - 4, i1 + k + 3, j1 - 3, l1, l1);
this.drawGradientRect(i1 - 3, j1 + k1 + 3, i1 + k + 3, j1 + k1 + 4, l1, l1);
this.drawGradientRect(i1 - 3, j1 - 3, i1 + k + 3, j1 + k1 + 3, l1, l1);
this.drawGradientRect(i1 - 4, j1 - 3, i1 - 3, j1 + k1 + 3, l1, l1);
this.drawGradientRect(i1 + k + 3, j1 - 3, i1 + k + 4, j1 + k1 + 3, l1, l1);
int i2 = 1347420415;
int j2 = (i2 & 16711422) >> 1 | i2 & -16777216;
this.drawGradientRect(i1 - 3, j1 - 3 + 1, i1 - 3 + 1, j1 + k1 + 3 - 1, i2, j2);
this.drawGradientRect(i1 + k + 2, j1 - 3 + 1, i1 + k + 3, j1 + k1 + 3 - 1, i2, j2);
this.drawGradientRect(i1 - 3, j1 - 3, i1 + k + 3, j1 - 3 + 1, i2, i2);
this.drawGradientRect(i1 - 3, j1 + k1 + 2, i1 + k + 3, j1 + k1 + 3, j2, j2);
for (int k2 = 0; k2 < lines.size(); ++k2) {
String s1 = lines.get(k2);
font.drawStringWithShadow(s1, i1, j1, -1);
if (k2 == 0) {
j1 += 2;
}
j1 += 10;
}
if (stencilEnabled) GL11.glEnable(GL11.GL_STENCIL_TEST);
RenderHelper.enableStandardItemLighting();
GlStateManager.enableBlend();
}
}
/** Returns the top level parent of this component, or itself if it has no
* parent. Useful for correctly drawing hovering text. */
private GuiComponent getTopLevelParent() {
GuiComponent component = this;
while (component.parent != null) {
component = component.parent;
}
return component;
}
/**
* Draws a text tooltip at mouse coordinates.
* <p>
* Same as {@link #drawHoveringText2(List, int, int, FontRenderer)}, but
* the text is drawn on the top level parent component, after all its child
* components have finished drawing. This allows the hovering text to be
* unobscured by other components.
* </p>
* <p>
* Only one instance of hovering text can be drawn via this method, i.e.
* from several components which occupy the same position on the screen.
* </p>
* */
protected void drawTooltip(List<String> lines, FontRenderer font) {
GuiComponent topLevel = getTopLevelParent();
topLevel.hoveringTextInfo.lines = lines;
topLevel.hoveringTextInfo.x = getMouseX();
topLevel.hoveringTextInfo.y = getMouseY();
topLevel.hoveringTextInfo.font = font;
topLevel.hoveringTextInfo.shouldDraw = true;
}
/** Wrapper for data used to draw hovering text at the end of rendering
* current frame. It is used by child components that wish to draw hovering
* text unobscured by their neighboring components. */
private final HoveringTextInfo hoveringTextInfo = new HoveringTextInfo();
private static class HoveringTextInfo {
List<String> lines;
int x, y;
FontRenderer font;
/** Whether to draw this hovering text during rendering current frame.
* This flag is reset to false after rendering finishes. */
boolean shouldDraw = false;
}
/** Remove itself from its parent component (if any), notifying it. */
public void close() {
if (parent != null) {
parent.removeChild(this); // This sets parent to null
} else {
Minecraft.getMinecraft().displayGuiScreen(null);
}
}
/** Called when a child removes itself from this component. */
protected void onChildClosed(GuiComponent child) {}
/** Draw a text string centered horizontally, using this GUI's FontRenderer. */
protected void drawCenteredString(String text, int y, int color, boolean dropShadow) {
int length = fontRenderer.getStringWidth(text);
fontRenderer.drawString(text, (this.width - length)/2, y, color, dropShadow);
}
protected int getMouseX() {
return Mouse.getX() * width / mc.displayWidth;
}
protected int getMouseY() {
return height - Mouse.getY() * height / mc.displayHeight - 1;
}
}