From 2d19b176260d4d7618c6961c3d09469f0d6beb1f Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Wed, 8 Jul 2015 16:54:14 +0200 Subject: [PATCH] Finished Layout of GuiObjectManager --- .../replaymod/entities/CameraEntity.java | 2 +- .../replaymod/gui/GuiObjectManager.java | 358 +++++++++++++++++- .../gui/elements/ComposedElement.java | 15 +- .../gui/elements/GuiNumberInput.java | 12 +- .../gui/overlay/GuiReplayOverlay.java | 4 +- .../assets/replaymod/lang/en_US.lang | 7 + .../resources/assets/replaymod/replay_gui.png | Bin 6852 -> 7260 bytes 7 files changed, 375 insertions(+), 23 deletions(-) diff --git a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java b/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java index 2795da85..3bc3e742 100755 --- a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java +++ b/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java @@ -34,7 +34,7 @@ public class CameraEntity extends EntityPlayer { if(maxSpeed < LOWER_SPEED || maxSpeed > UPPER_SPEED) return; MAX_SPEED = maxSpeed; THRESHOLD = MAX_SPEED / 20; - DECAY = MAX_SPEED/3; + DECAY = 5; } public static double getCameraMaximumSpeed() { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java index 16dd6065..59d8681b 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java @@ -1,20 +1,99 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.gui.elements.ComposedElement; -import eu.crushedpixel.replaymod.gui.elements.GuiDraggingNumberInput; +import eu.crushedpixel.replaymod.assets.CustomImageObject; +import eu.crushedpixel.replaymod.assets.ReplayAsset; +import eu.crushedpixel.replaymod.gui.elements.*; +import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; +import eu.crushedpixel.replaymod.interpolation.KeyframeList; +import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.MouseUtils; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; import org.lwjgl.util.Point; +import java.awt.*; import java.io.IOException; public class GuiObjectManager extends GuiScreen { private boolean initialized = false; - private GuiDraggingNumberInput anchorXInput, anchorYInput, anchorZInput; + private KeyframeList[] keyframeLists; - private ComposedElement numberInputs; + private GuiEntryList objectList; + private GuiAdvancedButton addButton, removeButton; + + private GuiAdvancedTextField nameInput; + + private GuiString dropdownLabel; + private GuiDropdown assetDropdown; + + private GuiDraggingNumberInput anchorXInput, anchorYInput, anchorZInput; + private GuiDraggingNumberInput positionXInput, positionYInput, positionZInput; + private GuiDraggingNumberInput scaleXInput, scaleYInput, scaleZInput; + private GuiDraggingNumberInput orientationXInput, orientationYInput, orientationZInput; + private GuiDraggingNumberInput opacityInput; + + private GuiObjectKeyframeTimeline objectKeyframeTimeline; + private GuiScrollbar timelineScrollbar; + + private GuiTexturedButton zoomInButton, zoomOutButton; + + private ComposedElement anchorInputs, positionInputs, scaleInputs, orientationInputs, opacityInputs, numberInputs; + private ComposedElement allElements; + + private static final int KEYFRAME_BUTTON_X = 80; + private static final int KEYFRAME_BUTTON_Y = 40; + + private static final float ZOOM_STEPS = 0.05f; + + private DelegatingElement keyframeButton(final int x, final int y, final int line) { + return new DelegatingElement() { + + private GuiTexturedButton normal = new GuiTexturedButton(0, x, y, 20, 20, + GuiReplayOverlay.replay_gui, KEYFRAME_BUTTON_X, KEYFRAME_BUTTON_Y, + GuiReplayOverlay.TEXTURE_SIZE, GuiReplayOverlay.TEXTURE_SIZE, new Runnable() { + @Override + public void run() { + addKeyframe(line); + } + }, + null); + + private GuiTexturedButton selected = new GuiTexturedButton(0, x, y, 20, 20, + GuiReplayOverlay.replay_gui, KEYFRAME_BUTTON_X, KEYFRAME_BUTTON_Y+20, + GuiReplayOverlay.TEXTURE_SIZE, GuiReplayOverlay.TEXTURE_SIZE, new Runnable() { + @Override + public void run() { + removeKeyframe(line); + } + }, + null); + + @Override + public GuiElement delegate() { + boolean sel = false; //TODO + + if(sel) { + return selected; + } else { + return normal; + } + } + }; + } + + private void addKeyframe(int line) { + + } + + private void removeKeyframe(int line) { + + } @Override public void initGui() { @@ -23,15 +102,170 @@ public class GuiObjectManager extends GuiScreen { anchorYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); anchorZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - numberInputs = new ComposedElement(anchorXInput, anchorYInput, anchorZInput); + positionXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); + positionYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); + positionZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); + + scaleXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%"); + scaleYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%"); + scaleZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%"); + + orientationXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); + orientationYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); + orientationZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); + + opacityInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, 0d, 100d, 100d, true, "%"); + + dropdownLabel = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.assets.filechooser")+": "); + assetDropdown = new GuiDropdown(fontRendererObj, 0, 0, 0, 5); + if(ReplayHandler.getAssetRepository().getCopyOfReplayAssets().isEmpty()) { + assetDropdown.addElement(I18n.format("replaymod.gui.assets.emptylist")); + } else { + for(ReplayAsset asset : ReplayHandler.getAssetRepository().getCopyOfReplayAssets()) { + assetDropdown.addElement(asset.getDisplayString()); + } + } + + objectList = new GuiEntryList(fontRendererObj, 0, 0, 0, 0); + objectList.setEmptyMessage(I18n.format("replaymod.gui.objects.empty")); + + addButton = new GuiAdvancedButton(0, 0, 0, 20, I18n.format("replaymod.gui.add"), new Runnable() { + @Override + public void run() { + + } + }, null); + + removeButton = new GuiAdvancedButton(0, 0, 0, 20, I18n.format("replaymod.gui.remove"), new Runnable() { + @Override + public void run() { + + } + }, null); + + nameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 0, 20); + nameInput.hint = I18n.format("replaymod.gui.objects.properties.name"); + + keyframeLists = new KeyframeList[5]; } - anchorXInput.width = anchorYInput.width = anchorZInput.width = 45; - anchorXInput.yPosition = anchorYInput.yPosition = anchorZInput.yPosition = 50; + String[] labelStrings = new String[] { + I18n.format("replaymod.gui.objects.properties.anchor"), + I18n.format("replaymod.gui.objects.properties.position"), + I18n.format("replaymod.gui.objects.properties.scale"), + I18n.format("replaymod.gui.objects.properties.orientation"), + I18n.format("replaymod.gui.objects.properties.opacity"), + }; - anchorXInput.xPosition = 20; - anchorYInput.xPosition = anchorXInput.xPosition+anchorXInput.width + 5; - anchorZInput.xPosition = anchorYInput.xPosition+anchorYInput.width + 5; + int maxStringWidth = 0; + for(String label : labelStrings) { + int stringWidth = fontRendererObj.getStringWidth(label); + if(stringWidth > maxStringWidth) { + maxStringWidth = stringWidth; + } + } + + allElements = new ComposedElement(); + + int inputWidth = 40; + + anchorInputs = new ComposedElement(anchorXInput, anchorYInput, anchorZInput); + positionInputs = new ComposedElement(positionXInput, positionYInput, positionZInput); + scaleInputs = new ComposedElement(scaleXInput, scaleYInput, scaleZInput); + orientationInputs = new ComposedElement(orientationXInput, orientationYInput, orientationZInput); + opacityInputs = new ComposedElement(opacityInput); + numberInputs = new ComposedElement(anchorInputs, positionInputs, scaleInputs, orientationInputs, opacityInputs); + + for(int i = numberInputs.getParts().length-1; i >= 0; i--) { + int yPos = this.height-5-10-(25*(numberInputs.getParts().length-i)); + DelegatingElement button = keyframeButton(10, yPos, i); + GuiString label = new GuiString(35, yPos + 6, Color.WHITE, labelStrings[i]); + + ComposedElement child = (ComposedElement)numberInputs.getParts()[i]; + int x = 0; + for(GuiElement el : child.getParts()) { + GuiDraggingNumberInput dni = (GuiDraggingNumberInput)el; + dni.width = inputWidth; + dni.xPosition = 35+maxStringWidth+5 + (x*(inputWidth+5)); + dni.yPosition = yPos; + x++; + } + + child.addPart(button); + child.addPart(label); + + allElements.addPart(child); + } + + int timelineX = anchorZInput.xPosition + anchorZInput.width + 5; + int timelineY = anchorZInput.yPosition; + + int timelineWidth = this.width-10-timelineX; + int timelineHeight = this.height-10-10-timelineY; + + objectKeyframeTimeline = new GuiObjectKeyframeTimeline(timelineX, timelineY, timelineWidth, timelineHeight, keyframeLists); + allElements.addPart(objectKeyframeTimeline); + + timelineScrollbar = new GuiScrollbar(timelineX, this.height-15, timelineWidth-19) { + @Override + public void dragged() { + objectKeyframeTimeline.setLeftPosition((float)sliderPosition); + } + }; + allElements.addPart(timelineScrollbar); + + zoomInButton = GuiReplayOverlay.texturedButton(width - 28, this.height-15, 40, 20, 9, new Runnable() { + @Override + public void run() { + objectKeyframeTimeline.setZoomScale(Math.max(0.025f, objectKeyframeTimeline.getZoomScale() - ZOOM_STEPS)); + } + }, "replaymod.gui.ingame.menu.zoomin"); + + zoomOutButton = GuiReplayOverlay.texturedButton(width - 18, this.height-15, 40, 30, 9, new Runnable() { + + @Override + public void run() { + objectKeyframeTimeline.setZoomScale(Math.min(1f, objectKeyframeTimeline.getZoomScale() + ZOOM_STEPS)); + objectKeyframeTimeline.setLeftPosition(Math.min(objectKeyframeTimeline.getLeftPos(), 1f - objectKeyframeTimeline.getZoomScale())); + } + }, "replaymod.gui.ingame.menu.zoomout"); + + allElements.addPart(zoomInButton); + allElements.addPart(zoomOutButton); + + objectList.xPosition = 11; + objectList.yPosition = 11; + objectList.width = width/2 - 11 - 5; + + int visibleElements = (int)((anchorXInput.yPosition-5 - objectList.yPosition)/14f); + objectList.setVisibleElements(visibleElements); + + allElements.addPart(objectList); + + nameInput.xPosition = width/2 + 5; + nameInput.yPosition = objectList.yPosition; + nameInput.width = objectList.width; + + allElements.addPart(nameInput); + + dropdownLabel.positionX = (width/2)+5; + int strWidth = fontRendererObj.getStringWidth(I18n.format("replaymod.gui.assets.filechooser")+": "); + + assetDropdown.xPosition = dropdownLabel.positionX+strWidth+5; + assetDropdown.yPosition = objectList.yPosition+25; + dropdownLabel.positionY = assetDropdown.yPosition + 6; + assetDropdown.width = (objectList.width-strWidth-5); + + allElements.addPart(dropdownLabel); + allElements.addPart(assetDropdown); + + addButton.xPosition = nameInput.xPosition; + addButton.width = removeButton.width = nameInput.width/2 - 2; + removeButton.xPosition = addButton.xPosition + nameInput.width/2 + 2; + addButton.yPosition = removeButton.yPosition = objectList.yPosition+objectList.height-20; + + allElements.addPart(addButton); + allElements.addPart(removeButton); initialized = true; } @@ -40,7 +274,9 @@ public class GuiObjectManager extends GuiScreen { public void drawScreen(int mouseX, int mouseY, float partialTicks) { this.drawDefaultBackground(); - numberInputs.draw(mc, mouseX, mouseY); + allElements.draw(mc, mouseX, mouseY); + + GlStateManager.enableBlend(); super.drawScreen(mouseX, mouseY, partialTicks); } @@ -48,32 +284,122 @@ public class GuiObjectManager extends GuiScreen { @Override protected void keyTyped(char typedChar, int keyCode) throws IOException { Point mousePos = MouseUtils.getMousePos(); - numberInputs.buttonPressed(mc, mousePos.getX(), mousePos.getY(), typedChar, keyCode); + allElements.buttonPressed(mc, mousePos.getX(), mousePos.getY(), typedChar, keyCode); super.keyTyped(typedChar, keyCode); } @Override public void updateScreen() { - numberInputs.tick(mc); + allElements.tick(mc); super.updateScreen(); } @Override protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { - numberInputs.mouseClick(mc, mouseX, mouseY, mouseButton); + allElements.mouseClick(mc, mouseX, mouseY, mouseButton); super.mouseClicked(mouseX, mouseY, mouseButton); } @Override protected void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) { - numberInputs.mouseDrag(mc, mouseX, mouseY, mouseButton); + allElements.mouseDrag(mc, mouseX, mouseY, mouseButton); super.mouseClickMove(mouseX, mouseY, mouseButton, timeSinceLastClick); } @Override protected void mouseReleased(int mouseX, int mouseY, int mouseButton) { - numberInputs.mouseRelease(mc, mouseX, mouseY, mouseButton); + allElements.mouseRelease(mc, mouseX, mouseY, mouseButton); super.mouseReleased(mouseX, mouseY, mouseButton); } + + public class GuiObjectKeyframeTimeline implements GuiElement { + + private KeyframeList[] keyframeLists; + + public int x, y, width, height; + + @Getter @Setter + private int timestamp; + + @Getter @Setter + private float zoomScale; + + @Getter @Setter + private float leftPos; + + private int timelineLength = 10 * 60 * 1000; + + public GuiObjectKeyframeTimeline(int x, int y, int width, int height, KeyframeList... keyframeLists) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.keyframeLists = keyframeLists; + } + + public void setLeftPosition(float leftPos) { + this.leftPos = leftPos; + } + + private boolean enabled = true; + + @Override + public void draw(Minecraft mc, int mouseX, int mouseY) { + drawRect(this.x - 1, this.y - 1, this.x + this.width + 1, this.y + this.height + 1, -6250336); + drawRect(this.x, this.y, this.x + this.width, this.y + this.height, -16777216); + + int count = keyframeLists.length; + int i = 0; + for(KeyframeList list : keyframeLists) { + int yPos = (int)(((float)i/count)*(height+2)); + int h = (int)(1f/count)*(height+2); + + if(i < count) + drawHorizontalLine(x, x+width, y+yPos+h-1, -6250336); + i++; + } + } + + @Override + public void drawOverlay(Minecraft mc, int mouseX, int mouseY) { + + } + + @Override + public boolean isHovering(int mouseX, int mouseY) { + return false; + } + + @Override + public void mouseClick(Minecraft mc, int mouseX, int mouseY, int button) { + + } + + @Override + public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) { + + } + + @Override + public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { + + } + + @Override + public void buttonPressed(Minecraft mc, int mouseX, int mouseY, char key, int keyCode) { + + } + + @Override + public void tick(Minecraft mc) { + + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/ComposedElement.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/ComposedElement.java index 3794bda6..4dada1d0 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/ComposedElement.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/ComposedElement.java @@ -1,14 +1,27 @@ package eu.crushedpixel.replaymod.gui.elements; +import lombok.Getter; import net.minecraft.client.Minecraft; public class ComposedElement implements GuiElement { - private final GuiElement[] parts; + @Getter + private GuiElement[] parts; public ComposedElement(GuiElement...parts) { this.parts = parts; } + public void addPart(GuiElement part) { + GuiElement[] newParts = new GuiElement[parts.length+1]; + int i = 0; + for(GuiElement e : parts) { + newParts[i] = e; + i++; + } + newParts[i] = part; + this.parts = newParts; + } + @Override public void draw(Minecraft mc, int mouseX, int mouseY) { for (GuiElement part : parts) { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiNumberInput.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiNumberInput.java index e3f71008..a26cc5c5 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiNumberInput.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiNumberInput.java @@ -26,10 +26,16 @@ public class GuiNumberInput extends GuiAdvancedTextField { } public void setValue(double value) { - if(acceptFloats) { - setText("" + value); + if(minimum != null && value < minimum) { + setText(acceptFloats ? minimum.toString() : Integer.toString((int) Math.round(minimum))); + } else if(maximum != null && value > maximum) { + setText(acceptFloats ? maximum.toString() : Integer.toString((int) Math.round(maximum))); } else { - setText("" + (int)Math.round(value)); + if(acceptFloats) { + setText("" + value); + } else { + setText("" + (int) Math.round(value)); + } } setCursorPositionZero(); } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java index 5af6fc2a..86f23825 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java @@ -46,7 +46,7 @@ public class GuiReplayOverlay extends Gui { public static final int TEXTURE_SIZE = 128; private static final float ZOOM_STEPS = 0.05f; - private static GuiTexturedButton texturedButton(int x, int y, int u, int v, int size, Runnable action, String hoverText) { + public static GuiTexturedButton texturedButton(int x, int y, int u, int v, int size, Runnable action, String hoverText) { return new GuiTexturedButton(0, x, y, size, size, replay_gui, u, v, TEXTURE_SIZE, TEXTURE_SIZE, action, I18n.format(hoverText)); } @@ -217,7 +217,6 @@ public class GuiReplayOverlay extends Gui { return ReplayHandler.getSelectedKeyframe() != null && ReplayHandler.getSelectedKeyframe().getValue() instanceof TimestampValue ? buttonSelected : buttonNotSelected; } }; - private final GuiElement buttonZoomIn = texturedButton(WIDTH - 14 - 9, BOTTOM_ROW, 40, 20, 9, new Runnable() { @Override public void run() { @@ -234,6 +233,7 @@ public class GuiReplayOverlay extends Gui { } }, "replaymod.gui.ingame.menu.zoomout"); + private final GuiMarkerTimeline timeline = new GuiMarkerTimeline(TIMELINE_X, TOP_ROW - 1, WIDTH - 14 - TIMELINE_X, false); private final GuiKeyframeTimeline timelineReal = new GuiKeyframeTimeline(TIMELINE_REAL_X, BOTTOM_ROW - 1, TIMELINE_REAL_WIDTH, true, true, true); diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index a59a2914..c0f4d28a 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -367,3 +367,10 @@ replaymod.gui.assets.namehint=Asset Name replaymod.gui.assets.changefile=Change Asset File #Object Manager Gui +replaymod.gui.objects.properties.anchor=Anchor Point +replaymod.gui.objects.properties.position=Position +replaymod.gui.objects.properties.scale=Scale +replaymod.gui.objects.properties.orientation=Orientation +replaymod.gui.objects.properties.opacity=Opacity +replaymod.gui.objects.properties.name=Object Name +replaymod.gui.objects.empty=No Objects added \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/replay_gui.png b/src/main/resources/assets/replaymod/replay_gui.png index 04dbf78be398d56ae03012fa0d201a604a07e8f7..ece10558ea861d38324e7dd6e60e00cd8667c1d2 100755 GIT binary patch literal 7260 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 W5QoVh3lshW0<_ij)k;-tKKu{!)Az;z literal 6852 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