diff --git a/pom.xml b/pom.xml index 2efd8210b..4d0089839 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,16 @@ + + com.google.guava + guava + 27.0.1-jre + + + org.dishevelled + dsh-eventlist-view + 2.2 + com.google.code.gson gson @@ -115,4 +125,4 @@ maven-plugin - \ No newline at end of file + diff --git a/src/main/java/amidst/AmidstSettings.java b/src/main/java/amidst/AmidstSettings.java index 8fdf0e830..ad48aee31 100644 --- a/src/main/java/amidst/AmidstSettings.java +++ b/src/main/java/amidst/AmidstSettings.java @@ -28,6 +28,7 @@ public class AmidstSettings { public final Setting showOceanFeatures; public final Setting showNetherFortresses; public final Setting showEndCities; + public final Setting showBookmarks; public final Setting smoothScrolling; public final Setting fragmentFading; @@ -65,6 +66,7 @@ public AmidstSettings(Preferences preferences) { showOceanFeatures = Setting.createBoolean( preferences, "oceanFeaturesIcons", true); showNetherFortresses = Setting.createBoolean( preferences, "netherFortressIcons", false); showEndCities = Setting.createBoolean( preferences, "endCityIcons", false); + showBookmarks = Setting.createBoolean( preferences, "bookmarkIcons", true); smoothScrolling = Setting.createBoolean( preferences, "mapFlicking", true); fragmentFading = Setting.createBoolean( preferences, "mapFading", true); diff --git a/src/main/java/amidst/fragment/layer/LayerBuilder.java b/src/main/java/amidst/fragment/layer/LayerBuilder.java index 98ef5082f..5cd761692 100644 --- a/src/main/java/amidst/fragment/layer/LayerBuilder.java +++ b/src/main/java/amidst/fragment/layer/LayerBuilder.java @@ -99,6 +99,7 @@ private List createDeclarations(AmidstSettings settings, List< declare(settings, declarations, enabledLayers, LayerIds.OCEAN_FEATURES, Dimension.OVERWORLD, false, settings.showOceanFeatures); declare(settings, declarations, enabledLayers, LayerIds.NETHER_FORTRESS, Dimension.OVERWORLD, false, settings.showNetherFortresses); declare(settings, declarations, enabledLayers, LayerIds.END_CITY, Dimension.END, false, settings.showEndCities); + declare(settings, declarations, enabledLayers, LayerIds.BOOKMARKS, Dimension.OVERWORLD, false, settings.showBookmarks); // @formatter:on return Collections.unmodifiableList(Arrays.asList(declarations)); } @@ -144,7 +145,8 @@ private Iterable createLoaders( new WorldIconLoader<>(declarations.get(LayerIds.WOODLAND_MANSION),world.getWoodlandMansionProducer()), new WorldIconLoader<>(declarations.get(LayerIds.OCEAN_FEATURES), world.getOceanFeaturesProducer()), new WorldIconLoader<>(declarations.get(LayerIds.NETHER_FORTRESS), world.getNetherFortressProducer()), - new WorldIconLoader<>(declarations.get(LayerIds.END_CITY), world.getEndCityProducer(), Fragment::getEndIslands) + new WorldIconLoader<>(declarations.get(LayerIds.END_CITY), world.getEndCityProducer(), Fragment::getEndIslands), + new WorldIconLoader<>(declarations.get(LayerIds.BOOKMARKS), world.getBookmarkProducer()) )); // @formatter:on } @@ -173,7 +175,8 @@ private Iterable createDrawers( new WorldIconDrawer(declarations.get(LayerIds.WOODLAND_MANSION),zoom, worldIconSelection), new WorldIconDrawer(declarations.get(LayerIds.OCEAN_FEATURES), zoom, worldIconSelection), new WorldIconDrawer(declarations.get(LayerIds.NETHER_FORTRESS), zoom, worldIconSelection), - new WorldIconDrawer(declarations.get(LayerIds.END_CITY), zoom, worldIconSelection) + new WorldIconDrawer(declarations.get(LayerIds.END_CITY), zoom, worldIconSelection), + new WorldIconDrawer(declarations.get(LayerIds.BOOKMARKS), zoom, worldIconSelection) )); // @formatter:on } diff --git a/src/main/java/amidst/fragment/layer/LayerIds.java b/src/main/java/amidst/fragment/layer/LayerIds.java index 51d527a48..233fbe028 100644 --- a/src/main/java/amidst/fragment/layer/LayerIds.java +++ b/src/main/java/amidst/fragment/layer/LayerIds.java @@ -26,6 +26,7 @@ public class LayerIds { public static final int OCEAN_FEATURES = 14; public static final int NETHER_FORTRESS = 15; public static final int END_CITY = 16; - public static final int NUMBER_OF_LAYERS = 17; + public static final int BOOKMARKS = 17; + public static final int NUMBER_OF_LAYERS = 18; // @formatter:on } diff --git a/src/main/java/amidst/gui/main/Actions.java b/src/main/java/amidst/gui/main/Actions.java index c69332d04..1ed41bd54 100644 --- a/src/main/java/amidst/gui/main/Actions.java +++ b/src/main/java/amidst/gui/main/Actions.java @@ -121,6 +121,11 @@ public void switchProfile() { application.displayProfileSelectWindow(); } + @CalledOnlyBy(AmidstThread.EDT) + public void displayBookmarks() { + dialogs.displayBookmarks(); + } + @CalledOnlyBy(AmidstThread.EDT) public void exit() { if (BiomeExporter.isExporterRunning()) { diff --git a/src/main/java/amidst/gui/main/MainWindowDialogs.java b/src/main/java/amidst/gui/main/MainWindowDialogs.java index b291d230b..58a766f17 100644 --- a/src/main/java/amidst/gui/main/MainWindowDialogs.java +++ b/src/main/java/amidst/gui/main/MainWindowDialogs.java @@ -12,6 +12,7 @@ import amidst.documentation.CalledOnlyBy; import amidst.documentation.NotThreadSafe; import amidst.logging.AmidstMessageBox; +import amidst.gui.main.bookmarks.BookmarkDialog; import amidst.mojangapi.RunningLauncherProfile; import amidst.mojangapi.world.WorldSeed; import amidst.mojangapi.world.WorldType; @@ -90,8 +91,13 @@ private Path showSaveDialogAndGetSelectedFileOrNull(JFileChooser fileChooser) { return null; } } + + @CalledOnlyBy(AmidstThread.EDT) + public void displayBookmarks() { + new BookmarkDialog(frame).setVisible(true); + } - @CalledOnlyBy(AmidstThread.EDT) + @CalledOnlyBy(AmidstThread.EDT) public void displayInfo(String title, String message) { AmidstMessageBox.displayInfo(frame, title, message); } diff --git a/src/main/java/amidst/gui/main/bookmarks/Bookmark.java b/src/main/java/amidst/gui/main/bookmarks/Bookmark.java new file mode 100644 index 000000000..21d2cfe9a --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/Bookmark.java @@ -0,0 +1,46 @@ +package amidst.gui.main.bookmarks; + +import java.io.Serializable; + +import java.util.Objects; + +/** + * Bookmark. + */ +public final class Bookmark implements Serializable { + private final long x; + private final long z; + private final String label; + private final int hashCode; + + private Bookmark(final long x, final long z, final String label) { + if (label == null) { + throw new NullPointerException("label must not be null"); + } + this.x = x; + this.z = z; + this.label = label; + this.hashCode = Objects.hash(this.x, this.z, this.label); + } + + public long getX() { + return x; + } + + public long getZ() { + return z; + } + + public String getLabel() { + return label; + } + + @Override + public int hashCode() { + return hashCode; + } + + public static Bookmark valueOf(final long x, final long z, final String label) { + return new Bookmark(x, z, label); + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/BookmarkChooser.java b/src/main/java/amidst/gui/main/bookmarks/BookmarkChooser.java new file mode 100644 index 000000000..4f116ad38 --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/BookmarkChooser.java @@ -0,0 +1,99 @@ +package amidst.gui.main.bookmarks; + +import java.awt.Component; +import java.awt.Dialog; +import java.awt.Frame; +import java.awt.Window; + +import javax.swing.JDialog; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +import javax.swing.border.EmptyBorder; + +import org.dishevelled.layout.LabelFieldPanel; + +final class BookmarkChooser extends LabelFieldPanel { + private final JTextField x; + private final JTextField z; + private final JTextField label; + + BookmarkChooser(final int x, final int z) { + super(); + this.x = new JTextField(String.valueOf(x)); + this.z = new JTextField(String.valueOf(z)); + label = new JTextField(); + label.requestFocus(); + + layoutComponents(); + } + + private void layoutComponents() { + setBorder(new EmptyBorder(12, 12, 0, 12)); + addField("X", x); + addField("Z", z); + addField("Label", label); + addFinalSpacing(12); + } + + JTextField x() { + return x; + } + + JTextField z() { + return z; + } + + JTextField label() { + return label; + } + + boolean ready() { + // todo validate + return true; + } + + Bookmark getBookmark() { + if (ready()) { + return Bookmark.valueOf(Integer.parseInt(x.getText()), Integer.parseInt(z.getText()), label.getText()); + } + return null; + } + + public static Bookmark showDialog(final Component component, final String title, final int x, final int z) { + if (component == null) { + throw new IllegalArgumentException("component must not be null"); + } + if (title == null) { + throw new IllegalArgumentException("title must not be null"); + } + BookmarkChooser chooserPane = new BookmarkChooser(x, z); + BookmarkChooserDialog dialog = createDialog(component, title, true, chooserPane); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.setLocationRelativeTo(component); + dialog.show(); + return dialog.getBookmark(); + } + + static BookmarkChooserDialog createDialog(final Component component, final String title, final boolean modal, final BookmarkChooser chooserPane) { + if (component == null) { + throw new IllegalArgumentException("component must not be null"); + } + if (title == null) { + throw new IllegalArgumentException("title must not be null"); + } + if (chooserPane == null) { + throw new IllegalArgumentException("chooserPane must not be null"); + } + Window window = SwingUtilities.windowForComponent(component); + BookmarkChooserDialog dialog; + if (window instanceof Frame) { + dialog = new BookmarkChooserDialog((Frame) window, title, modal, chooserPane); + } + else { //if (window instanceof Dialog) + dialog = new BookmarkChooserDialog((Dialog) window, title, modal, chooserPane); + } + dialog.getAccessibleContext().setAccessibleDescription(title); + return dialog; + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/BookmarkChooserDialog.java b/src/main/java/amidst/gui/main/bookmarks/BookmarkChooserDialog.java new file mode 100644 index 000000000..d79175833 --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/BookmarkChooserDialog.java @@ -0,0 +1,133 @@ +package amidst.gui.main.bookmarks; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dialog; +import java.awt.Frame; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; + +import javax.swing.border.EmptyBorder; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.dishevelled.layout.ButtonPanel; + +class BookmarkChooserDialog extends JDialog { + private boolean canceled = true; + + private final AbstractAction cancel = new AbstractAction("Cancel") { + @Override + public void actionPerformed(final ActionEvent event) + { + cancel(); + } + }; + private final AbstractAction ok = new AbstractAction("OK") { + @Override + public void actionPerformed(final ActionEvent event) + { + ok(); + } + }; + + private final JButton okButton; + private final BookmarkChooser chooser; + + BookmarkChooserDialog(final Dialog owner, final String title, final boolean modal, final BookmarkChooser chooser) + { + super(owner, title, modal); + + ok.setEnabled(false); + okButton = new JButton(ok); + this.chooser = chooser; + + initialize(); + } + + BookmarkChooserDialog(final Frame owner, final String title, final boolean modal, final BookmarkChooser chooser) + { + super(owner, title, modal); + + ok.setEnabled(false); + okButton = new JButton(ok); + this.chooser = chooser; + + initialize(); + } + + private void initialize() + { + getRootPane().setDefaultButton(okButton); + createListeners(); + layoutComponents(); + setSize(320, 200); + } + + private void createListeners() + { + DocumentListener l = new DocumentListener() { + @Override + public void changedUpdate(final DocumentEvent e) { + ok.setEnabled(chooser.ready()); + } + + @Override + public void insertUpdate(final DocumentEvent e) { + ok.setEnabled(chooser.ready()); + } + + @Override + public void removeUpdate(final DocumentEvent e) { + ok.setEnabled(chooser.ready()); + } + }; + + chooser.x().getDocument().addDocumentListener(l); + chooser.z().getDocument().addDocumentListener(l); + chooser.label().getDocument().addDocumentListener(l); + } + + private void layoutComponents() + { + ButtonPanel buttonPanel = new ButtonPanel(); + buttonPanel.setBorder(new EmptyBorder(0, 12, 12, 12)); + buttonPanel.add(cancel); + buttonPanel.add(okButton); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout()); + mainPanel.add("Center", chooser); + mainPanel.add("South", buttonPanel); + + setContentPane(mainPanel); + } + + private void cancel() + { + canceled = true; + hide(); + } + + private void ok() + { + canceled = false; + hide(); + } + + boolean wasCanceled() + { + return canceled; + } + + Bookmark getBookmark() + { + return wasCanceled() ? null : chooser.getBookmark(); + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/BookmarkDialog.java b/src/main/java/amidst/gui/main/bookmarks/BookmarkDialog.java new file mode 100644 index 000000000..0fc643072 --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/BookmarkDialog.java @@ -0,0 +1,216 @@ +package amidst.gui.main.bookmarks; + +import java.awt.BorderLayout; + +import java.awt.event.ActionEvent; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTabbedPane; + +import javax.swing.border.EmptyBorder; + +import ca.odell.glazedlists.event.ListEvent; +import ca.odell.glazedlists.event.ListEventListener; + +import com.google.common.io.Files; + +import org.dishevelled.iconbundle.IconSize; + +import org.dishevelled.iconbundle.tango.TangoProject; + +import org.dishevelled.identify.ContextMenuListener; +import org.dishevelled.identify.IdentifiableAction; +import org.dishevelled.identify.IdButton; +import org.dishevelled.identify.IdMenuItem; +import org.dishevelled.identify.IdToolBar; + +import org.dishevelled.layout.LabelFieldPanel; + +public final class BookmarkDialog extends JDialog { + private boolean dirty = false; + private final Bookmarks bookmarks; + private final BookmarkList bookmarkList; + private final BookmarkTable bookmarkTable; + + /** Open action. */ + private final IdentifiableAction open = new IdentifiableAction("Open bookmarks...", TangoProject.DOCUMENT_OPEN) + { + @Override + public void actionPerformed(final ActionEvent event) + { + open(); + } + }; + + /** Save action. */ + private final IdentifiableAction save = new IdentifiableAction("Save bookmarks...", TangoProject.DOCUMENT_SAVE) + { + @Override + public void actionPerformed(final ActionEvent event) + { + save(); + } + }; + + + public BookmarkDialog(final JFrame frame) { + super(frame, "Bookmarks"); + bookmarks = Bookmarks.getInstance(); + bookmarkList = new BookmarkList(bookmarks); + bookmarkTable = new BookmarkTable(bookmarks); + + bookmarks.addListEventListener(new ListEventListener() { + @Override + public void listChanged(final ListEvent e) { + dirty = true; + save.setEnabled(true); + } + }); + + open.setEnabled(true); + save.setEnabled(false); + + layoutComponents(); + setSize(450, 640); + setLocationRelativeTo(frame); + } + + private void layoutComponents() { + JPopupMenu contextMenu = new JPopupMenu(); + contextMenu.add(open); + contextMenu.add(save); + bookmarkList.addMouseListener(new ContextMenuListener(contextMenu)); + bookmarkTable.addMouseListener(new ContextMenuListener(contextMenu)); + + IdToolBar toolBar = new IdToolBar(); + IdButton openButton = toolBar.add(open); + openButton.setBorderPainted(false); + openButton.setFocusPainted(false); + IdButton saveButton = toolBar.add(save); + saveButton.setBorderPainted(false); + saveButton.setFocusPainted(false); + + toolBar.displayIcons(); + toolBar.setIconSize(TangoProject.SMALL); + + JPopupMenu toolBarContextMenu = new JPopupMenu(); + for (Object menuItem : toolBar.getDisplayMenuItems()) + { + toolBarContextMenu.add((JCheckBoxMenuItem) menuItem); + } + toolBarContextMenu.addSeparator(); + for (Object iconSize : TangoProject.SIZES) + { + toolBarContextMenu.add(toolBar.createIconSizeMenuItem((IconSize) iconSize)); + } + toolBar.addMouseListener(new ContextMenuListener(toolBarContextMenu)); + + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.addTab("As bookmark list", createListPanel()); + tabbedPane.addTab("As bookmark table", createTablePanel()); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout()); + mainPanel.add("North", toolBar); + mainPanel.add("Center", tabbedPane); + setContentPane(mainPanel); + } + + private JPanel createListPanel() { + LabelFieldPanel panel = new LabelFieldPanel(); + panel.setBorder(12); + panel.addFinalField(bookmarkList); + panel.setOpaque(false); + return panel; + } + + private JPanel createTablePanel() { + LabelFieldPanel panel = new LabelFieldPanel(); + panel.setBorder(12); + panel.addFinalField(bookmarkTable); + panel.setOpaque(false); + return panel; + } + + private void open() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(true); + int returnVal = fileChooser.showOpenDialog(getContentPane()); + + if (returnVal == JFileChooser.APPROVE_OPTION) + { + for (File file: fileChooser.getSelectedFiles()) + { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String ext = Files.getFileExtension(file.getName()); + if ("csv".equals(ext)) { + bookmarks.readCsvFrom(reader); + } + else if ("txt".equals(ext) || "tab".equals(ext) || "tsv".equals(ext)) { + bookmarks.readTsvFrom(reader); + } + else if ("json".equals(ext) || "js".equals(ext)) { + bookmarks.readJsonFrom(reader); + } + else { + // default to .tsv + bookmarks.readTsvFrom(reader); + } + } + catch (Exception e) { + // ignore + } + } + } + } + + private void save() { + if (dirty) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + int returnVal = fileChooser.showSaveDialog(getContentPane()); + + if (returnVal == JFileChooser.APPROVE_OPTION) + { + File file = fileChooser.getSelectedFile(); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + String ext = Files.getFileExtension(file.getName()); + if ("csv".equals(ext)) { + bookmarks.writeCsvTo(writer); + } + else if ("txt".equals(ext) || "tab".equals(ext) || "tsv".equals(ext)) { + bookmarks.writeTsvTo(writer); + } + else if ("json".equals(ext) || "js".equals(ext)) { + bookmarks.writeJsonTo(writer); + } + else { + // default to .tsv + bookmarks.writeTsvTo(writer); + } + } + catch (Exception e) { + // ignore + } + dirty = false; + save.setEnabled(false); + } + } + } + + public Bookmarks getBookmarks() { + return bookmarks; + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/BookmarkList.java b/src/main/java/amidst/gui/main/bookmarks/BookmarkList.java new file mode 100644 index 000000000..cacd40bc7 --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/BookmarkList.java @@ -0,0 +1,22 @@ +package amidst.gui.main.bookmarks; + +import org.dishevelled.eventlist.view.ElementsList; + +/** + * Bookmark list. + */ +public final class BookmarkList extends ElementsList { + + public BookmarkList(final Bookmarks bookmarks) { + super("Bookmarks", bookmarks); + getList().setCellRenderer(new BookmarkListCellRenderer()); + } + + @Override + public void add() { + Bookmark bookmark = BookmarkChooser.showDialog(this, "Create a new bookmark", 0, 0); + if (bookmark != null) { + getModel().add(bookmark); + } + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/BookmarkListCellRenderer.java b/src/main/java/amidst/gui/main/bookmarks/BookmarkListCellRenderer.java new file mode 100644 index 000000000..aa6e8234c --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/BookmarkListCellRenderer.java @@ -0,0 +1,22 @@ +package amidst.gui.main.bookmarks; + +import java.awt.Component; + +import javax.swing.JLabel; +import javax.swing.JList; + +import org.dishevelled.identify.StripeListCellRenderer; + +final class BookmarkListCellRenderer extends StripeListCellRenderer { + + @Override + public final Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean hasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, hasFocus); + + if (value instanceof Bookmark) { + Bookmark bookmark = (Bookmark) value; + label.setText(bookmark.getLabel() + " [" + bookmark.getX() + ", " + bookmark.getZ() + "]"); + } + return label; + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/BookmarkTable.java b/src/main/java/amidst/gui/main/bookmarks/BookmarkTable.java new file mode 100644 index 000000000..ff11df62d --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/BookmarkTable.java @@ -0,0 +1,36 @@ +package amidst.gui.main.bookmarks; + +import ca.odell.glazedlists.GlazedLists; + +import ca.odell.glazedlists.gui.TableFormat; + +import org.dishevelled.eventlist.view.ElementsTable; + +/** + * Bookmark table. + */ +public final class BookmarkTable extends ElementsTable { + + /** Table property names. */ + private static final String[] PROPERTY_NAMES = { "x", "z", "label" }; + + /** Table column labels. */ + private static final String[] COLUMN_LABELS = { "X", "Z", "Label" }; + + /** Table format. */ + private static final TableFormat TABLE_FORMAT = GlazedLists.tableFormat(Bookmark.class, PROPERTY_NAMES, COLUMN_LABELS); + + + public BookmarkTable(final Bookmarks bookmarks) { + super("Bookmarks", bookmarks, TABLE_FORMAT); + } + + + @Override + public void add() { + Bookmark bookmark = BookmarkChooser.showDialog(this, "Create a new bookmark", 0, 0); + if (bookmark != null) { + getModel().add(bookmark); + } + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/Bookmarks.java b/src/main/java/amidst/gui/main/bookmarks/Bookmarks.java new file mode 100644 index 000000000..6a24b80f0 --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/Bookmarks.java @@ -0,0 +1,102 @@ +package amidst.gui.main.bookmarks; + +import java.io.BufferedReader; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Iterator; + +import ca.odell.glazedlists.GlazedLists; + +/** + * Bookmarks. + */ +public final class Bookmarks extends ForwardingEventList { + + private Bookmarks() { + super(GlazedLists.eventList(new ArrayList())); + } + + public void readCsvFrom(final BufferedReader reader) throws IOException { + while (reader.ready()) { + String line = reader.readLine() + .replace("\"", "") + .replace(",", "\t"); + String[] tokens = line.split("\t"); + if (tokens.length == 3) { + add(Bookmark.valueOf(Long.parseLong(tokens[0]), Long.parseLong(tokens[1]), tokens[2])); + } + } + } + + public void readTsvFrom(final BufferedReader reader) throws IOException { + while (reader.ready()) { + String line = reader.readLine(); + String[] tokens = line.split("\t"); + if (tokens.length == 3) { + add(Bookmark.valueOf(Long.parseLong(tokens[0]), Long.parseLong(tokens[1]), tokens[2])); + } + } + } + + public void readJsonFrom(final BufferedReader reader) throws IOException { + while (reader.ready()) { + String line = reader.readLine() + .replace("{ \"x\":", "") + .replace(", \"z\":", "\t") + .replace(", \"label\":\"", "\t") + .replace("\" },", "") + .replace("\" }", ""); + String[] tokens = line.split("\t"); + if (tokens.length == 3) { + add(Bookmark.valueOf(Long.parseLong(tokens[0]), Long.parseLong(tokens[1]), tokens[2])); + } + } + } + + public void writeCsvTo(final Appendable a) throws IOException { + for (Bookmark b : this) { + a.append(String.valueOf(b.getX())); + a.append(","); + a.append(String.valueOf(b.getZ())); + a.append(",\""); + a.append(b.getLabel()); + a.append("\"\n"); + } + } + + public void writeTsvTo(final Appendable a) throws IOException { + for (Bookmark b : this) { + a.append(String.valueOf(b.getX())); + a.append("\t"); + a.append(String.valueOf(b.getZ())); + a.append("\t"); + a.append(b.getLabel()); + a.append("\n"); + } + } + + public void writeJsonTo(final Appendable a) throws IOException { + for (Iterator i = iterator(); i.hasNext(); ) { + Bookmark b = i.next(); + a.append("{ \"x\":"); + a.append(String.valueOf(b.getX())); + a.append(", \"z\":"); + a.append(String.valueOf(b.getZ())); + a.append(", \"label\":\""); + a.append(b.getLabel()); + a.append("\" }"); + if (i.hasNext()) { + a.append(","); + } + a.append("\n"); + } + } + + + // todo: is there a better place to instantiate this? + private static final Bookmarks INSTANCE = new Bookmarks(); + public static final Bookmarks getInstance() { + return INSTANCE; + } +} diff --git a/src/main/java/amidst/gui/main/bookmarks/ForwardingEventList.java b/src/main/java/amidst/gui/main/bookmarks/ForwardingEventList.java new file mode 100644 index 000000000..2778b87fe --- /dev/null +++ b/src/main/java/amidst/gui/main/bookmarks/ForwardingEventList.java @@ -0,0 +1,178 @@ +package amidst.gui.main.bookmarks; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import java.util.function.Consumer; +import java.util.function.Function; + +import ca.odell.glazedlists.EventList; + +import ca.odell.glazedlists.event.ListEvent; +import ca.odell.glazedlists.event.ListEventListener; +import ca.odell.glazedlists.event.ListEventPublisher; + +import ca.odell.glazedlists.util.concurrent.ReadWriteLock; + +class ForwardingEventList implements EventList { + private final EventList eventList; + + protected ForwardingEventList(final EventList eventList) { + if (eventList == null) { + throw new NullPointerException("eventList must not be null"); + } + this.eventList = eventList; + } + + @Override + public void addListEventListener(ListEventListener listChangeListener) { + eventList.addListEventListener(listChangeListener); + } + + @Override + public void removeListEventListener(ListEventListener listChangeListener) { + eventList.removeListEventListener(listChangeListener); + } + + @Override + public ReadWriteLock getReadWriteLock() { + return eventList.getReadWriteLock(); + } + + @Override + public ListEventPublisher getPublisher() { + return eventList.getPublisher(); + } + + @Override + public void dispose() { + eventList.dispose(); + } + + @Override + public Iterator iterator() { + return eventList.iterator(); + } + + @Override + public int size() { + return eventList.size(); + } + + @Override + public boolean removeAll(Collection collection) { + return eventList.removeAll(collection); + } + + @Override + public boolean isEmpty() { + return eventList.isEmpty(); + } + + @Override + public boolean contains(Object object) { + return eventList.contains(object); + } + + @Override + public boolean add(E element) { + return eventList.add(element); + } + + @Override + public boolean remove(Object object) { + return eventList.remove(object); + } + + @Override + public boolean containsAll(Collection collection) { + return eventList.containsAll(collection); + } + + @Override + public boolean addAll(Collection collection) { + return eventList.addAll(collection); + } + + @Override + public boolean retainAll(Collection collection) { + return eventList.retainAll(collection); + } + + @Override + public void clear() { + eventList.clear(); + } + + @Override + public Object[] toArray() { + return eventList.toArray(); + } + + @Override + public T[] toArray(T[] array) { + return eventList.toArray(array); + } + + @Override + public void add(int index, E element) { + eventList.add(index, element); + } + + @Override + public boolean addAll(int index, Collection elements) { + return eventList.addAll(index, elements); + } + + @Override + public E get(int index) { + return eventList.get(index); + } + + @Override + public int indexOf(Object element) { + return eventList.indexOf(element); + } + + @Override + public int lastIndexOf(Object element) { + return eventList.lastIndexOf(element); + } + + @Override + public ListIterator listIterator() { + return eventList.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return eventList.listIterator(index); + } + + @Override + public E remove(int index) { + return eventList.remove(index); + } + + @Override + public E set(int index, E element) { + return eventList.set(index, element); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return eventList.subList(fromIndex, toIndex); + } + + @Override + public boolean equals(Object object) { + return object == this || eventList.equals(object); + } + + @Override + public int hashCode() { + return eventList.hashCode(); + } +} diff --git a/src/main/java/amidst/gui/main/menu/AmidstMenuBuilder.java b/src/main/java/amidst/gui/main/menu/AmidstMenuBuilder.java index 7c791e9b6..98b22a1c8 100644 --- a/src/main/java/amidst/gui/main/menu/AmidstMenuBuilder.java +++ b/src/main/java/amidst/gui/main/menu/AmidstMenuBuilder.java @@ -77,10 +77,12 @@ private JMenu create_World() { result.setEnabled(false); result.setMnemonic(KeyEvent.VK_W); // @formatter:off + Menus.item(result, actions::displayBookmarks, "Display bookmarks", KeyEvent.VK_B); Menus.item(result, actions::goToCoordinate, "Go to Coordinate ...", KeyEvent.VK_C, MenuShortcuts.GO_TO_COORDINATE); Menus.item(result, actions::goToSpawn, "Go to World Spawn", KeyEvent.VK_S, MenuShortcuts.GO_TO_WORLD_SPAWN); Menus.item(result, actions::goToStronghold, "Go to Stronghold ...", KeyEvent.VK_H, MenuShortcuts.GO_TO_STRONGHOLD); Menus.item(result, actions::goToPlayer, "Go to Player ...", KeyEvent.VK_P, MenuShortcuts.GO_TO_PLAYER); + result.addSeparator(); Menus.item(result, actions::zoomIn, "Zoom In", KeyEvent.VK_I, MenuShortcuts.ZOOM_IN); Menus.item(result, actions::zoomOut, "Zoom Out", KeyEvent.VK_O, MenuShortcuts.ZOOM_OUT); diff --git a/src/main/java/amidst/gui/main/menu/LayersMenu.java b/src/main/java/amidst/gui/main/menu/LayersMenu.java index d9fb678b3..4837557a7 100644 --- a/src/main/java/amidst/gui/main/menu/LayersMenu.java +++ b/src/main/java/amidst/gui/main/menu/LayersMenu.java @@ -85,6 +85,7 @@ private void createOverworldAndEndLayers(Dimension dimension) { @CalledOnlyBy(AmidstThread.EDT) private void createOverworldLayers(Dimension dimension) { // @formatter:off + overworldLayer(settings.showBookmarks, "Bookmarks", getIcon("bookmark-menu.png"), MenuShortcuts.SHOW_BOOKMARKS, dimension, LayerIds.BOOKMARKS); overworldLayer(settings.showSlimeChunks, "Slime Chunks", getIcon("slime.png"), MenuShortcuts.SHOW_SLIME_CHUNKS, dimension, LayerIds.SLIME); overworldLayer(settings.showSpawn, "Spawn Location Icon", getIcon("spawn.png"), MenuShortcuts.SHOW_WORLD_SPAWN, dimension, LayerIds.SPAWN); overworldLayer(settings.showStrongholds, "Stronghold Icons", getIcon("stronghold.png"), MenuShortcuts.SHOW_STRONGHOLDS, dimension, LayerIds.STRONGHOLD); diff --git a/src/main/java/amidst/gui/main/menu/MenuShortcuts.java b/src/main/java/amidst/gui/main/menu/MenuShortcuts.java index bb096924a..1f209fbd6 100644 --- a/src/main/java/amidst/gui/main/menu/MenuShortcuts.java +++ b/src/main/java/amidst/gui/main/menu/MenuShortcuts.java @@ -44,6 +44,7 @@ public enum MenuShortcuts implements MenuShortcut { SHOW_WOODLAND_MANSIONS("menu 8"), SHOW_OCEAN_FEATURES("menu 9"), SHOW_NETHER_FORTRESSES("menu 0"), + SHOW_BOOKMARKS("menu M"), // It's okay to duplicate the Overworld layers shortcuts here, because // the End layers will never be active at the same time. diff --git a/src/main/java/amidst/mojangapi/world/World.java b/src/main/java/amidst/mojangapi/world/World.java index ae0195b18..99fef7e01 100644 --- a/src/main/java/amidst/mojangapi/world/World.java +++ b/src/main/java/amidst/mojangapi/world/World.java @@ -35,7 +35,8 @@ public class World { private final WorldIconProducer woodlandMansionProducer; private final WorldIconProducer oceanFeaturesProducer; private final WorldIconProducer netherFortressProducer; - private final WorldIconProducer> endCityProducer; + private final WorldIconProducer> endCityProducer; + private final WorldIconProducer bookmarkProducer; public World( WorldOptions worldOptions, @@ -56,7 +57,8 @@ public World( WorldIconProducer woodlandMansionProducer, WorldIconProducer oceanFeaturesProducer, WorldIconProducer netherFortressProducer, - WorldIconProducer> endCityProducer) { + WorldIconProducer> endCityProducer, + WorldIconProducer bookmarkProducer) { this.worldOptions = worldOptions; this.movablePlayerList = movablePlayerList; this.recognisedVersion = recognisedVersion; @@ -76,6 +78,7 @@ public World( this.oceanFeaturesProducer = oceanFeaturesProducer; this.netherFortressProducer = netherFortressProducer; this.endCityProducer = endCityProducer; + this.bookmarkProducer = bookmarkProducer; } public WorldOptions getWorldOptions() { @@ -154,6 +157,10 @@ public WorldIconProducer getOceanFeaturesProducer() { return oceanFeaturesProducer; } + public WorldIconProducer getBookmarkProducer() { + return bookmarkProducer; + } + public WorldIcon getSpawnWorldIcon() { return spawnProducer.getFirstWorldIcon(); } diff --git a/src/main/java/amidst/mojangapi/world/WorldBuilder.java b/src/main/java/amidst/mojangapi/world/WorldBuilder.java index 42608070e..3e766ef15 100644 --- a/src/main/java/amidst/mojangapi/world/WorldBuilder.java +++ b/src/main/java/amidst/mojangapi/world/WorldBuilder.java @@ -3,6 +3,7 @@ import java.io.IOException; import amidst.documentation.Immutable; +import amidst.gui.main.bookmarks.Bookmarks; import amidst.mojangapi.file.ImmutablePlayerInformationProvider; import amidst.mojangapi.file.PlayerInformationProvider; import amidst.mojangapi.file.SaveGame; @@ -10,6 +11,7 @@ import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; import amidst.mojangapi.minecraftinterface.RecognisedVersion; import amidst.mojangapi.world.coordinates.Resolution; +import amidst.mojangapi.world.icon.producer.BookmarkProducer; import amidst.mojangapi.world.icon.producer.MultiProducer; import amidst.mojangapi.world.icon.producer.PlayerProducer; import amidst.mojangapi.world.icon.producer.SpawnProducer; @@ -204,6 +206,7 @@ private World create( versionFeatures.get(FeatureKey.END_ISLAND_LOCATION_CHECKER), new EndCityWorldIconTypeProvider(), Dimension.END, - false)); + false), + new BookmarkProducer(Bookmarks.getInstance())); } } diff --git a/src/main/java/amidst/mojangapi/world/icon/producer/BookmarkProducer.java b/src/main/java/amidst/mojangapi/world/icon/producer/BookmarkProducer.java new file mode 100644 index 000000000..3a5a20453 --- /dev/null +++ b/src/main/java/amidst/mojangapi/world/icon/producer/BookmarkProducer.java @@ -0,0 +1,43 @@ +package amidst.mojangapi.world.icon.producer; + +import java.util.LinkedList; +import java.util.List; + +import amidst.documentation.ThreadSafe; +import amidst.gui.main.bookmarks.Bookmark; +import amidst.gui.main.bookmarks.Bookmarks; +import amidst.logging.AmidstLogger; +import amidst.mojangapi.world.Dimension; +import amidst.mojangapi.world.coordinates.CoordinatesInWorld; +import amidst.mojangapi.world.icon.WorldIcon; +import amidst.mojangapi.world.icon.type.DefaultWorldIconTypes; + +import ca.odell.glazedlists.event.ListEvent; +import ca.odell.glazedlists.event.ListEventListener; + +@ThreadSafe +public class BookmarkProducer extends CachedWorldIconProducer { + private final Bookmarks bookmarks; + + public BookmarkProducer(final Bookmarks bookmarks) { + this.bookmarks = bookmarks; + this.bookmarks.addListEventListener(new ListEventListener() { + @Override + public void listChanged(final ListEvent e) { + AmidstLogger.info("Resetting cache on list event " + e + "..."); + resetCache(); + } + }); + } + + @Override + protected List doCreateCache() { + AmidstLogger.info("Creating bookmark world icon cache..."); + List result = new LinkedList<>(); + for (Bookmark b : bookmarks) { + AmidstLogger.info("Creating bookmark world icon at [" + b.getX() + ", " + b.getZ() + "] in world coordinates " + CoordinatesInWorld.from((long) b.getX(), (long) b.getZ()) + "..."); + result.add(new WorldIcon(CoordinatesInWorld.from((long) b.getX(), (long) b.getZ()), b.getLabel(), DefaultWorldIconTypes.BOOKMARK.getImage(), Dimension.OVERWORLD, true)); + } + return result; + } +} diff --git a/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java b/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java index 67decc6e6..05ad3de5d 100644 --- a/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java +++ b/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java @@ -29,6 +29,7 @@ public enum DefaultWorldIconTypes { MINESHAFT ("mineshaft", "Mineshaft"), WOODLAND_MANSION ("woodland_mansion", "Woodland Mansion"), END_CITY ("end_city", "Likely End City"), + BOOKMARK ("bookmark", "Bookmark"), POSSIBLE_END_CITY ("possible_end_city", "Possible End City"), OCEAN_RUINS ("ocean_ruins", "Ocean Ruins"), SHIPWRECK ("shipwreck", "Shipwreck"), diff --git a/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java b/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java index 9964ef857..f4469cc29 100644 --- a/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java +++ b/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java @@ -93,6 +93,7 @@ public static VersionFeatures.Builder builder(WorldOptions worldOptions, Minecra LayerIds.ALPHA, LayerIds.BIOME_DATA, LayerIds.BACKGROUND, + LayerIds.BOOKMARKS, LayerIds.SLIME, LayerIds.GRID, LayerIds.SPAWN, diff --git a/src/main/resources/amidst/gui/main/icon/bookmark-menu.png b/src/main/resources/amidst/gui/main/icon/bookmark-menu.png new file mode 100644 index 000000000..dd830525a Binary files /dev/null and b/src/main/resources/amidst/gui/main/icon/bookmark-menu.png differ diff --git a/src/main/resources/amidst/gui/main/icon/bookmark.png b/src/main/resources/amidst/gui/main/icon/bookmark.png new file mode 100644 index 000000000..efc4cbb70 Binary files /dev/null and b/src/main/resources/amidst/gui/main/icon/bookmark.png differ