From d791eedae97149c76e88b34998515576db114487 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 17 Dec 2018 13:28:28 +0100 Subject: [PATCH] Replace dropdown replay selection with popup in ReplayEditor (fixes #132) --- jGui | 2 +- .../replaymod/editor/gui/GuiTrimPanel.java | 68 +++--- .../replay/gui/screen/GuiReplayViewer.java | 201 +++++++++++++----- 3 files changed, 188 insertions(+), 83 deletions(-) diff --git a/jGui b/jGui index 213ac136..43a58160 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 213ac13627178f49ce9671b29b6a542cb0e4c922 +Subproject commit 43a581607508c42d2ad7f67deaffb49cf620cfbe diff --git a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java index 3947214d..4b15de62 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java +++ b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java @@ -1,7 +1,10 @@ package com.replaymod.editor.gui; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.gson.JsonObject; import com.replaymod.core.utils.Utils; +import com.replaymod.replay.gui.screen.GuiReplayViewer; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.filter.ChangeTimestampFilter; import com.replaymod.replaystudio.filter.RemoveFilter; @@ -12,6 +15,7 @@ import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.stream.PacketStream; import com.replaymod.replaystudio.studio.ReplayStudio; import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiNumberField; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; @@ -20,13 +24,11 @@ import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import net.minecraft.client.resources.I18n; import net.minecraft.crash.CrashReport; -import org.apache.commons.io.IOCase; -import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.lang3.tuple.Pair; import org.lwjgl.util.Dimension; +import javax.annotation.Nullable; import java.io.File; -import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -50,9 +52,10 @@ public class GuiTrimPanel extends GuiPanel { private final GuiReplayEditor gui; - public final GuiDropdownMenu inputReplays = new GuiDropdownMenu(this) - .setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay()) - .setToString(f -> f == NO_REPLAY ? "" : Utils.fileNameToReplayName(f.getName())); + private final GuiButton inputReplayButton = new GuiButton(this) + .setMinSize(new Dimension(200, 20)) + .setMaxSize(new Dimension(350, 20)); + private File inputReplay = NO_REPLAY; public final GuiNumberField startHour = newGuiNumberField(); public final GuiNumberField startMin = newGuiNumberField(); @@ -104,24 +107,31 @@ public class GuiTrimPanel extends GuiPanel { endMarker.setToString(toString).setMinSize(new Dimension(100, 20)).onSelection(i -> onSelectedMarkerChanged(endMarker, endHour, endMin, endSec, endMilli)); - File[] files = null; - try { - File folder = gui.getMod().getReplayFolder(); - files = folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE)); - } catch (IOException e) { - Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Listing available replays"), gui.backButton::onClick); - } - if (files == null) { - LOGGER.warn("Replay file list is null, has the replay folder been deleted?"); - files = new File[0]; - } else { - LOGGER.debug("Found {} replays in the replay folder", files.length); - } - if (files.length == 0) { - inputReplays.setValues(NO_REPLAY); - } else { - inputReplays.setValues(files); - } + inputReplayButton.onClick(() -> { + File folder; + try { + folder = gui.getMod().getReplayFolder(); + } catch (IOException e) { + Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Getting replay folder"), gui.backButton::onClick); + return; + } + GuiReplayViewer.GuiSelectReplayPopup popup = GuiReplayViewer.GuiSelectReplayPopup.openGui(gui, folder); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(@Nullable File result) { + if (result != null) { + inputReplay = result; + updateSelectedReplay(); + updateReadyState(); + } + } + + @Override + public void onFailure(Throwable t) { + Utils.error(LOGGER, gui, CrashReport.makeCrashReport(t, "Selecting replay"), gui.backButton::onClick); + } + }); + }); updateSelectedReplay(); updateReadyState(); @@ -135,7 +145,7 @@ public class GuiTrimPanel extends GuiPanel { JsonObject config = new JsonObject(); config.addProperty("offset", -start); // Pass filters to save dialog - gui.save(inputReplays.getSelectedValue(), + gui.save(inputReplay, Pair.of(new PacketStream.FilterInfo(new SquashFilter(), -1, start), new JsonObject()), Pair.of(new PacketStream.FilterInfo(ctf, start, end), config), Pair.of(new PacketStream.FilterInfo(new RemoveFilter(), end, -1), new JsonObject()) @@ -161,7 +171,11 @@ public class GuiTrimPanel extends GuiPanel { } private void updateSelectedReplay() { - File file = inputReplays.getSelectedValue(); + File file = inputReplay; + + // Update input button label + inputReplayButton.setLabel(file == NO_REPLAY ? I18n.format("gui.none") : Utils.fileNameToReplayName(file.getName())); + // Load markers and meta data from replay file ReplayMetaData metaData; Set markers; @@ -196,6 +210,6 @@ public class GuiTrimPanel extends GuiPanel { } private void updateReadyState() { - gui.saveButton.setEnabled(inputReplays.getSelectedValue() != NO_REPLAY); + gui.saveButton.setEnabled(inputReplay != NO_REPLAY); } } diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index cd83f75a..388714ed 100755 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -4,6 +4,7 @@ import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; import com.mojang.realmsclient.gui.ChatFormatting; import com.replaymod.core.ReplayMod; import com.replaymod.core.SettingsRegistry; @@ -20,17 +21,21 @@ 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.element.*; -import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList; +import de.johni0702.minecraft.gui.element.advanced.AbstractGuiResourceLoadingList; 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.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; +import lombok.Getter; import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.client.resources.I18n; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; import net.minecraft.util.Util; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOCase; @@ -53,10 +58,10 @@ import java.util.Date; import static com.replaymod.replay.ReplayModReplay.LOGGER; -public class GuiReplayViewer extends GuiScreen implements Typeable { +public class GuiReplayViewer extends GuiScreen { private final ReplayModReplay mod; - public final GuiResourceLoadingList list = new GuiResourceLoadingList(this).onSelectionChanged(new Runnable() { + public final GuiReplayList list = new GuiReplayList(this).onSelectionChanged(new Runnable() { @Override public void run() { replayButtonPanel.forEach(IGuiButton.class).setEnabled(list.getSelected() != null); @@ -64,53 +69,13 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { loadButton.setDisabled(); } } - }).onLoad(new Consumer>> () { - @Override - public void consume(Consumer> obj) { - try { - File folder = mod.getCore().getReplayFolder(); - for (final File file : folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE))) { - if (Thread.interrupted()) break; - try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) { - - Optional thumb = replayFile.getThumb(); - // Make sure that to int[] conversion doesn't have to occur in main thread - final BufferedImage theThumb; - if (thumb.isPresent()) { - BufferedImage buf = thumb.get(); - // This is the same way minecraft calls this method, we cache the result and hand - // minecraft a BufferedImage with way simpler logic using the precomputed values - final int[] theIntArray = buf.getRGB(0, 0, buf.getWidth(), buf.getHeight(), null, 0, buf.getWidth()); - theThumb = new BufferedImage(buf.getWidth(), buf.getHeight(), BufferedImage.TYPE_INT_ARGB) { - @Override - public int[] getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { - System.arraycopy(theIntArray, 0, rgbArray, 0, theIntArray.length); - return null; // Minecraft doesn't use the return value - } - }; - } else { - theThumb = null; - } - final ReplayMetaData metaData = replayFile.getMetaData(); - - if (metaData != null) { - obj.consume(() -> new GuiReplayEntry(file, metaData, theThumb)); - } - } catch (Exception e) { - LOGGER.error("Could not load Replay File {}", file.getName(), e); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } }).onSelectionDoubleClicked(() -> { if (this.loadButton.isEnabled()) { this.loadButton.onClick(); // Disable load button to prevent the player from opening the replay twice at the same time this.loadButton.setDisabled(); } - }).setDrawShadow(true).setDrawSlider(true); + }); public final GuiButton loadButton = new GuiButton().onClick(new Runnable() { @Override @@ -264,6 +229,12 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { public GuiReplayViewer(ReplayModReplay mod) { this.mod = mod; + try { + list.setFolder(mod.getCore().getReplayFolder()); + } catch (IOException e) { + throw new ReportedException(CrashReport.makeCrashReport(e, "Getting replay folder")); + } + setTitle(new GuiLabel().setI18nText("replaymod.gui.replayviewer")); setLayout(new CustomLayout() { @@ -277,20 +248,140 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { }); } - private final GuiImage defaultThumbnail = new GuiImage().setTexture(Utils.DEFAULT_THUMBNAIL); + private static final GuiImage DEFAULT_THUMBNAIL = new GuiImage().setTexture(Utils.DEFAULT_THUMBNAIL); - @Override - public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { - if (keyCode == Keyboard.KEY_F1) { - SettingsRegistry reg = ReplayMod.instance.getSettingsRegistry(); - reg.set(Setting.SHOW_SERVER_IPS, !reg.get(Setting.SHOW_SERVER_IPS)); - reg.save(); - list.load(); + public static class GuiSelectReplayPopup extends AbstractGuiPopup { + public static GuiSelectReplayPopup openGui(GuiContainer container, File folder) { + GuiSelectReplayPopup popup = new GuiSelectReplayPopup(container, folder); + popup.list.load(); + popup.open(); + return popup; + } + + @Getter + private final SettableFuture future = SettableFuture.create(); + + @Getter + private final GuiReplayList list = new GuiReplayList(popup); + + @Getter + private final GuiButton acceptButton = new GuiButton(popup).setI18nLabel("gui.done").setSize(50, 20).setDisabled(); + + @Getter + private final GuiButton cancelButton = new GuiButton(popup).setI18nLabel("gui.cancel").setSize(50, 20); + + + public GuiSelectReplayPopup(GuiContainer container, File folder) { + super(container); + + list.setFolder(folder); + + list.onSelectionChanged(() -> { + acceptButton.setEnabled(list.getSelected() != null); + }).onSelectionDoubleClicked(() -> { + close(); + future.set(list.getSelected().file); + }); + acceptButton.onClick(() -> { + future.set(list.getSelected().file); + close(); + }); + cancelButton.onClick(() -> { + future.set(null); + close(); + }); + + popup.setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(cancelButton, width - width(cancelButton), height - height(cancelButton)); + pos(acceptButton, x(cancelButton) - 5 - width(acceptButton), y(cancelButton)); + pos(list, 0, 5); + size(list, width, height - height(cancelButton) - 10); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + return new Dimension(330, 200); + } + }); + } + + @Override + protected GuiSelectReplayPopup getThis() { + return this; } - return false; } - public class GuiReplayEntry extends AbstractGuiContainer implements Comparable { + public static class GuiReplayList extends AbstractGuiResourceLoadingList implements Typeable { + private File folder = null; + + public GuiReplayList(GuiContainer container) { + super(container); + } + + { + onLoad((Consumer> results) -> { + File[] files = folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE)); + if (files == null) { + LOGGER.warn("Failed to list files in {}", folder); + return; + } + for (final File file : files) { + if (Thread.interrupted()) break; + try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) { + Optional thumb = replayFile.getThumb(); + // Make sure that to int[] conversion doesn't have to occur in main thread + final BufferedImage theThumb; + if (thumb.isPresent()) { + BufferedImage buf = thumb.get(); + // This is the same way minecraft calls this method, we cache the result and hand + // minecraft a BufferedImage with way simpler logic using the precomputed values + final int[] theIntArray = buf.getRGB(0, 0, buf.getWidth(), buf.getHeight(), null, 0, buf.getWidth()); + theThumb = new BufferedImage(buf.getWidth(), buf.getHeight(), BufferedImage.TYPE_INT_ARGB) { + @Override + public int[] getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { + System.arraycopy(theIntArray, 0, rgbArray, 0, theIntArray.length); + return null; // Minecraft doesn't use the return value + } + }; + } else { + theThumb = null; + } + final ReplayMetaData metaData = replayFile.getMetaData(); + + if (metaData != null) { + results.consume(() -> new GuiReplayEntry(file, metaData, theThumb)); + } + } catch (Exception e) { + LOGGER.error("Could not load Replay File {}", file.getName(), e); + } + } + }).setDrawShadow(true).setDrawSlider(true); + } + + public void setFolder(File folder) { + this.folder = folder; + } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_F1) { + SettingsRegistry reg = ReplayMod.instance.getSettingsRegistry(); + reg.set(Setting.SHOW_SERVER_IPS, !reg.get(Setting.SHOW_SERVER_IPS)); + reg.save(); + load(); + } + return false; + } + + @Override + protected GuiReplayList getThis() { + return this; + } + } + + public static class GuiReplayEntry extends AbstractGuiContainer implements Comparable { public final File file; public final GuiLabel name = new GuiLabel(); public final GuiLabel server = new GuiLabel().setColor(Colors.LIGHT_GRAY); @@ -334,7 +425,7 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { dateMillis = metaData.getDate(); date.setText(new SimpleDateFormat().format(new Date(dateMillis))); if (thumbImage == null) { - thumbnail = new GuiImage(defaultThumbnail).setSize(30 * 16 / 9, 30); + thumbnail = new GuiImage(DEFAULT_THUMBNAIL).setSize(30 * 16 / 9, 30); addElements(null, thumbnail); } else { thumbnail = new GuiImage(this).setTexture(thumbImage).setSize(30 * 16 / 9, 30);