package com.replaymod.pathing.gui; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Utils; import com.replaymod.core.versions.MCVer; import com.replaymod.render.gui.GuiRenderQueue; import com.replaymod.render.gui.GuiRenderSettings; import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; import com.replaymod.replaystudio.replay.ReplayFile; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiVerticalList; import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiTextField; import de.johni0702.minecraft.gui.function.Closeable; import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import net.minecraft.util.crash.CrashReport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; import java.util.ArrayDeque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static de.johni0702.minecraft.gui.versions.MCVer.getClipboardString; import static de.johni0702.minecraft.gui.versions.MCVer.setClipboardString; /** * Gui for loading and saving {@link Timeline Timelines}. */ public class GuiKeyframeRepository extends GuiScreen implements Closeable, Typeable { private static final Logger LOGGER = LogManager.getLogger(); public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT); public final GuiLabel title = new GuiLabel(contentPanel).setI18nText("replaymod.gui.keyframerepository.title"); public final GuiVerticalList list = new GuiVerticalList(contentPanel).setDrawShadow(true).setDrawSlider(true); public final GuiButton overwriteButton = new GuiButton().onClick(new Runnable() { @Override public void run() { GuiYesNoPopup.open(GuiKeyframeRepository.this, new GuiLabel().setI18nText("replaymod.gui.keyframerepo.overwrite").setColor(Colors.BLACK) ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no").onAccept(() -> { for (Entry entry : selectedEntries) { timelines.put(entry.name, currentTimeline); } overwriteButton.setDisabled(); save(); }); } }).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled(); public final GuiButton saveAsButton = new GuiButton().onClick(new Runnable() { @Override public void run() { final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true); final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, new GuiLabel().setI18nText("replaymod.gui.saveas").setColor(Colors.BLACK), nameField ).setYesI18nLabel("replaymod.gui.save").setNoI18nLabel("replaymod.gui.cancel"); popup.getYesButton().setDisabled(); ((VerticalLayout) popup.getInfo().getLayout()).setSpacing(7); nameField.onEnter(new Runnable() { @Override public void run() { if (popup.getYesButton().isEnabled()) { popup.getYesButton().onClick(); } } }).onTextChanged(new Consumer() { @Override public void consume(String obj) { popup.getYesButton().setEnabled(!nameField.getText().isEmpty() && !timelines.containsKey(nameField.getText())); } }); popup.onAccept(() -> { String name = nameField.getText(); timelines.put(name, currentTimeline); list.getListPanel().addElements(null, new Entry(name)); save(); }); } }).setSize(75, 20).setI18nLabel("replaymod.gui.saveas"); public final GuiButton loadButton = new GuiButton().onClick(new Runnable() { @Override public void run() { getMinecraft().openScreen(null); try { Timeline timeline = timelines.get(selectedEntries.iterator().next().name); for (Path path : timeline.getPaths()) { path.updateAll(); } future.set(timeline); } catch (Throwable t) { future.setException(t); } } }).setSize(75, 20).setI18nLabel("replaymod.gui.load").setDisabled(); public final GuiButton renameButton = new GuiButton().onClick(new Runnable() { @Override public void run() { Entry selectedEntry = selectedEntries.iterator().next(); final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(selectedEntry.name); final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, new GuiLabel().setI18nText("replaymod.gui.rename").setColor(Colors.BLACK), nameField ).setYesI18nLabel("replaymod.gui.done").setNoI18nLabel("replaymod.gui.cancel"); popup.getYesButton().setDisabled(); ((VerticalLayout) popup.getInfo().getLayout()).setSpacing(7); nameField.onEnter(new Runnable() { @Override public void run() { if (popup.getYesButton().isEnabled()) { popup.getYesButton().onClick(); } } }).onTextChanged(new Consumer() { @Override public void consume(String obj) { popup.getYesButton().setEnabled(!nameField.getText().isEmpty() && !timelines.containsKey(nameField.getText())); } }); popup.onAccept(() -> { String name = nameField.getText(); timelines.put(name, timelines.remove(selectedEntry.name)); selectedEntry.name = name; selectedEntry.label.setText(name); save(); }); } }).setSize(75, 20).setI18nLabel("replaymod.gui.rename").setDisabled(); public final GuiButton removeButton = new GuiButton().onClick(new Runnable() { @Override public void run() { GuiYesNoPopup.open(GuiKeyframeRepository.this, new GuiLabel().setI18nText("replaymod.gui.keyframerepo.delete").setColor(Colors.BLACK) ).setYesI18nLabel("replaymod.gui.delete").setNoI18nLabel("replaymod.gui.cancel").onAccept(() -> { for (Entry entry : selectedEntries) { timelines.remove(entry.name); list.getListPanel().removeElement(entry); } selectedEntries.clear(); updateButtons(); save(); }); } }).setSize(75, 20).setI18nLabel("replaymod.gui.remove").setDisabled(); public final GuiButton copyButton = new GuiButton().onClick(new Runnable() { @Override public void run() { Map toBeSerialized = new HashMap<>(); for (Entry entry : selectedEntries) { toBeSerialized.put(entry.name, timelines.get(entry.name)); } try { TimelineSerialization serialization = new TimelineSerialization(registry, null); setClipboardString(serialization.serialize(toBeSerialized)); } catch (Throwable t) { t.printStackTrace(); CrashReport report = CrashReport.create(t, "Copying timeline(s)"); Utils.error(LOGGER, GuiKeyframeRepository.this, report, () -> {}); } } }).setSize(75, 20).setI18nLabel("replaymod.gui.copy").setDisabled(); public final GuiButton pasteButton = new GuiButton().onClick(new Runnable() { @Override public void run() { try { TimelineSerialization serialization = new TimelineSerialization(registry, null); for (Map.Entry entry : serialization.deserialize(getClipboardString()).entrySet()) { String name = entry.getKey(); while (timelines.containsKey(name)) { name += " (Copy)"; } timelines.put(name, entry.getValue()); list.getListPanel().addElements(null, new Entry(name)); } save(); } catch (Throwable t) { // Intentionally not making a fuzz about it cause this will likely happen when they have anything // else in their clipboard. // If it's actually not working, they'll go complain and we'll just check the log. t.printStackTrace(); } } }).setSize(75, 20).setI18nLabel("replaymod.gui.paste"); public final GuiButton addToQueueButton = new GuiButton().onClick(new Runnable() { @Override public void run() { ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); GuiRenderQueue queue = new GuiRenderQueue(GuiKeyframeRepository.this, replayHandler, null); queue.open(); Set selected = selectedEntries.stream().map(e -> e.name).collect(Collectors.toSet()); ArrayDeque> toBeAdded = new ArrayDeque<>(); // Iterating over timelines to get consistent ordering (cause selectedEntries is an unordered set) // and we need to get the Timelines anyway. for (Map.Entry entry : timelines.entrySet()) { if (selected.contains(entry.getKey())) { toBeAdded.offerLast(entry); } } new Runnable() { @Override public void run() { Map.Entry entry = toBeAdded.pollFirst(); if (entry == null) { return; } String name = entry.getKey(); Timeline timeline = entry.getValue(); GuiRenderSettings settingsGui = queue.addJob(timeline); settingsGui.buttonPanel.removeElement(settingsGui.renderButton); settingsGui.setOutputFileBaseName(name); Runnable orgOnClick = settingsGui.queueButton.getOnClick(); settingsGui.queueButton.onClick(() -> { orgOnClick.run(); this.run(); }); settingsGui.open(); } }.run(); } }).setSize(75, 20).setI18nLabel("replaymod.gui.rendersettings.addtoqueue"); public final GuiPanel buttonPanel = new GuiPanel(contentPanel) .setLayout(new GridLayout() .setColumns(4) .setSpacingX(5) .setSpacingY(5)) .addElements(null, overwriteButton, saveAsButton, renameButton, removeButton, loadButton, addToQueueButton, copyButton, pasteButton); private final Map timelines = new LinkedHashMap<>(); private final Timeline currentTimeline; private final SettableFuture future = SettableFuture.create(); private final PathingRegistry registry; private final ReplayFile replayFile; private final Set selectedEntries = new HashSet<>(); { setBackground(Background.NONE); setLayout(new CustomLayout() { @Override protected void layout(GuiScreen container, int width, int height) { pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2); } }); contentPanel.setLayout(new CustomLayout() { @Override protected void layout(GuiPanel container, int width, int height) { pos(title, width / 2 - width(title) / 2, 5); size(list, width, height - 10 - height(buttonPanel) - 10 - y(title) - height(title) - 5); pos(list, width / 2 - width(list) / 2, y(title) + height(title) + 5); pos(buttonPanel, width / 2 - width(buttonPanel) / 2, y(list) + height(list) + 10); } @Override public ReadableDimension calcMinSize(GuiContainer container) { ReadableDimension screenSize = getMinSize(); return new Dimension(screenSize.getWidth() - 10, screenSize.getHeight() - 10); } }); } public GuiKeyframeRepository(PathingRegistry registry, ReplayFile replayFile, Timeline currentTimeline) throws IOException { this.registry = registry; this.replayFile = replayFile; this.currentTimeline = currentTimeline; timelines.putAll(replayFile.getTimelines(registry)); for (Map.Entry entry : timelines.entrySet()) { if (entry.getKey().isEmpty()) continue; // don't show auto-save slot list.getListPanel().addElements(null, new Entry(entry.getKey())); } updateButtons(); } private void updateButtons() { int selected = selectedEntries.size(); overwriteButton.setEnabled(selected >= 1); loadButton.setEnabled(selected == 1); renameButton.setEnabled(selected == 1); removeButton.setEnabled(selected >= 1); copyButton.setEnabled(selected >= 1); addToQueueButton.setEnabled(selected >= 1); } @Override public void display() { super.display(); ReplayModReplay.instance.getReplayHandler().getOverlay().setVisible(false); } @Override public void close() { ReplayModReplay.instance.getReplayHandler().getOverlay().setVisible(true); } public SettableFuture getFuture() { return future; } public void save() { try { replayFile.writeTimelines(registry, timelines); } catch (IOException e) { e.printStackTrace(); ReplayMod.instance.printWarningToChat("Error saving timelines: " + e.getMessage()); } } @Override public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { if (MCVer.Keyboard.hasControlDown()) { switch (keyCode) { case MCVer.Keyboard.KEY_A: if (selectedEntries.size() < timelines.size()) { for (GuiElement child : list.getListPanel().getChildren()) { if (child instanceof Entry) { selectedEntries.add((Entry) child); } } } else { selectedEntries.clear(); } updateButtons(); return true; case MCVer.Keyboard.KEY_C: copyButton.onClick(); return true; case MCVer.Keyboard.KEY_V: pasteButton.onClick(); return true; } } return false; } public class Entry extends AbstractGuiClickableContainer { public final GuiLabel label = new GuiLabel(this); private String name; public Entry(String name) { this.name = name; setLayout(new CustomLayout() { @Override protected void layout(Entry container, int width, int height) { pos(label, 5, height / 2 - height(label) / 2); } @Override public ReadableDimension calcMinSize(GuiContainer container) { return new Dimension(buttonPanel.calcMinSize().getWidth(), 16); } }); label.setText(name); } @Override protected void onClick() { if (!MCVer.Keyboard.hasControlDown()) { selectedEntries.clear(); } if (selectedEntries.contains(this)) { selectedEntries.remove(this); } else { selectedEntries.add(this); } updateButtons(); } @Override public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { if (selectedEntries.contains(this)) { renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK); renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE); } super.draw(renderer, size, renderInfo); } @Override protected Entry getThis() { return this; } } }