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 super E> listChangeListener) {
+ eventList.addListEventListener(listChangeListener);
+ }
+
+ @Override
+ public void removeListEventListener(ListEventListener super E> 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 extends E> 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 extends E> 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