diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/DeleteOnlyPageException.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/DeleteOnlyPageException.java new file mode 100644 index 0000000000..7fdf1aadc7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/DeleteOnlyPageException.java @@ -0,0 +1,9 @@ +package net.runelite.client.plugins.notes; + +class DeleteOnlyPageException extends RuntimeException +{ + DeleteOnlyPageException() + { + super("Cannot delete the only page"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NoteTab.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NoteTab.java new file mode 100644 index 0000000000..d66026ff24 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NoteTab.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019, whs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.notes; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import javax.swing.AbstractAction; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; +import javax.swing.border.EmptyBorder; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoManager; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.ui.ColorScheme; + +@Slf4j +class NoteTab extends JPanel +{ + private final NotesManager manager; + private final JTextArea notesEditor = new JTextArea(); + private final UndoManager undoRedo = new UndoManager(); + + private int index; + + NoteTab(NotesManager mManager, int mIndex) + { + manager = mManager; + index = mIndex; + + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARKER_GRAY_COLOR); + + notesEditor.setTabSize(2); + notesEditor.setLineWrap(true); + notesEditor.setWrapStyleWord(true); + + notesEditor.setOpaque(false); + + notesEditor.setText(manager.getNotes().get(mIndex)); + + // setting the limit to a 500 as UndoManager registers every key press, + // which means that be default we would be able to undo only a sentence. + // note: the default limit is 100 + undoRedo.setLimit(500); + notesEditor.getDocument().addUndoableEditListener(e -> undoRedo.addEdit(e.getEdit())); + + notesEditor.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo"); + notesEditor.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo"); + + notesEditor.getActionMap().put("Undo", new AbstractAction("Undo") + { + @Override + public void actionPerformed(ActionEvent e) + { + try + { + if (undoRedo.canUndo()) + { + undoRedo.undo(); + } + } + catch (CannotUndoException ex) + { + log.warn("Notes Document Unable To Undo: " + ex); + } + } + }); + + notesEditor.getActionMap().put("Redo", new AbstractAction("Redo") + { + @Override + public void actionPerformed(ActionEvent e) + { + try + { + if (undoRedo.canRedo()) + { + undoRedo.redo(); + } + } + catch (CannotUndoException ex) + { + log.warn("Notes Document Unable To Redo: " + ex); + } + } + }); + + notesEditor.addFocusListener(new FocusListener() + { + @Override + public void focusGained(FocusEvent e) + { + + } + + @Override + public void focusLost(FocusEvent e) + { + notesChanged(notesEditor.getDocument()); + } + + private void notesChanged(Document doc) + { + try + { + // get document text and save to config whenever editor is changed + String data = doc.getText(0, doc.getLength()); + manager.updateNote(index, data); + } + catch (BadLocationException ex) + { + log.warn("Notes Document Bad Location: " + ex); + } + } + }); + add(notesEditor, BorderLayout.CENTER); + setBorder(new EmptyBorder(10, 10, 10, 10)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesConfig.java index d8834bed5c..409e125ffc 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesConfig.java @@ -27,10 +27,14 @@ package net.runelite.client.plugins.notes; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Range; @ConfigGroup("notes") public interface NotesConfig extends Config { + String CONFIG_GROUP = "notes"; + String NOTES = "notes"; + @ConfigItem( keyName = "notesData", name = "", @@ -48,4 +52,18 @@ public interface NotesConfig extends Config description = "" ) void notesData(String str); + + @Range( + min = 1, + max = 5 + ) + @ConfigItem( + keyName = "maxNotes", + name = "Max Notes", + description = "Desired maximum amount of notes" + ) + default int maxNotes() + { + return 5; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesManager.java new file mode 100644 index 0000000000..bb77880b71 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesManager.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2019, whs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.notes; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; +import joptsimple.internal.Strings; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.plugins.notes.events.PageAdded; +import net.runelite.client.plugins.notes.events.PageDeleted; + +@Singleton +@Slf4j +public class NotesManager +{ + @Inject + private ConfigManager configManager; + + @Inject + private NotesConfig config; + + @Inject + private EventBus eventBus; + + @Getter + private List notes = new ArrayList<>(); + + void loadNotes() + { + final String configJson = configManager.getConfiguration(NotesConfig.CONFIG_GROUP, NotesConfig.NOTES); + + notes = null; + if (!Strings.isNullOrEmpty(configJson)) + { + final Gson gson = new Gson(); + notes = gson.fromJson(configJson, new TypeToken>() + { + }.getType()); + } + + if (notes == null) + { + notes = new ArrayList<>(); + notes.add(""); + } + + // migrate from legacy single tab notes + if (!config.notesData().isEmpty()) + { + log.info("Adding tab for legacy note data"); + notes.add(0, config.notesData()); + + if (notes.size() == 2 && notes.get(1).equals("")) + { + // remove the default empty note page + notes.remove(1); + } + } + } + + void updateNote(int index, String data) + { + notes.set(index, data); + save(); + } + + void save() + { + final Gson gson = new Gson(); + final String json = gson.toJson(notes); + configManager.setConfiguration(NotesConfig.CONFIG_GROUP, NotesConfig.NOTES, json); + + // Remove legacy notes + if (!config.notesData().isEmpty()) + { + log.info("Removing legacy note data"); + config.notesData(""); + } + } + + void addPage() + { + notes.add(""); + eventBus.post(PageAdded.class, new PageAdded(notes.size() - 1)); + save(); + } + + void deletePage(int index) + { + if (notes.size() <= 1) + { + throw new DeleteOnlyPageException(); + } + + notes.remove(index); + eventBus.post(PageDeleted.class, new PageDeleted(index)); + save(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPanel.java index 4092f2c15b..3bfb882b51 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPanel.java @@ -1,6 +1,7 @@ /* * Copyright (c) 2018 Charlie Waters * Copyright (c) 2018, Psikoi + * Copyright (c) 2019, whs * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,139 +27,180 @@ package net.runelite.client.plugins.notes; import java.awt.BorderLayout; -import java.awt.event.ActionEvent; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import javax.inject.Inject; import javax.inject.Singleton; -import javax.swing.AbstractAction; -import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import static javax.swing.JOptionPane.ERROR_MESSAGE; +import static javax.swing.JOptionPane.YES_NO_OPTION; +import static javax.swing.JOptionPane.YES_OPTION; +import static javax.swing.JOptionPane.getRootFrame; import javax.swing.JPanel; -import javax.swing.JTextArea; -import javax.swing.KeyStroke; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.undo.CannotUndoException; -import javax.swing.undo.UndoManager; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.events.ConfigChanged; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.plugins.notes.events.PageAdded; +import net.runelite.client.plugins.notes.events.PageDeleted; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.materialtabs.MaterialTab; +import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; +import net.runelite.client.util.ImageUtil; @Slf4j @Singleton class NotesPanel extends PluginPanel { - private final JTextArea notesEditor = new JTextArea(); - private final UndoManager undoRedo = new UndoManager(); + @Inject + private NotesManager notesManager; - void init(final NotesConfig config) + @Inject + private EventBus eventBus; + + private final JPanel display = new JPanel(); + private final MaterialTabGroup tabGroup = new MaterialTabGroup(display); + private final ImageIcon addIcon = new ImageIcon(ImageUtil.getResourceStreamFromClass(getClass(), "add_icon.png")); + private MaterialTab addTab; + private List tabs = new ArrayList<>(); + private NotesConfig config; + + void init(final NotesConfig mConfig) { + config = mConfig; + + eventBus.subscribe(PageAdded.class, this, this::onPageAdded); + eventBus.subscribe(PageDeleted.class, this, this::onPageDeleted); + eventBus.subscribe(ConfigChanged.class, this, this::onConfigChanged); + // this may or may not qualify as a hack // but this lets the editor pane expand to fill the whole parent panel getParent().setLayout(new BorderLayout()); getParent().add(this, BorderLayout.CENTER); setLayout(new BorderLayout()); - setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); setBackground(ColorScheme.DARK_GRAY_COLOR); - notesEditor.setTabSize(2); - notesEditor.setLineWrap(true); - notesEditor.setWrapStyleWord(true); + tabGroup.setBorder(new EmptyBorder(0, 0, 10, 0)); - JPanel notesContainer = new JPanel(); - notesContainer.setLayout(new BorderLayout()); - notesContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + buildAddTab(); - notesEditor.setOpaque(false); - - // load note text - String data = config.notesData(); - notesEditor.setText(data); - - // setting the limit to a 500 as UndoManager registers every key press, - // which means that be default we would be able to undo only a sentence. - // note: the default limit is 100 - undoRedo.setLimit(500); - notesEditor.getDocument().addUndoableEditListener(e -> undoRedo.addEdit(e.getEdit())); - - notesEditor.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo"); - notesEditor.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo"); - - notesEditor.getActionMap().put("Undo", new AbstractAction("Undo") - { - @Override - public void actionPerformed(ActionEvent e) - { - try - { - if (undoRedo.canUndo()) - { - undoRedo.undo(); - } - } - catch (CannotUndoException ex) - { - log.warn("Notes Document Unable To Undo: " + ex); - } - } - }); - - notesEditor.getActionMap().put("Redo", new AbstractAction("Redo") - { - @Override - public void actionPerformed(ActionEvent e) - { - try - { - if (undoRedo.canRedo()) - { - undoRedo.redo(); - } - } - catch (CannotUndoException ex) - { - log.warn("Notes Document Unable To Redo: " + ex); - } - } - }); - - notesEditor.addFocusListener(new FocusListener() - { - @Override - public void focusGained(FocusEvent e) - { - - } - - @Override - public void focusLost(FocusEvent e) - { - notesChanged(notesEditor.getDocument()); - } - - private void notesChanged(Document doc) - { - try - { - // get document text and save to config whenever editor is changed - String data = doc.getText(0, doc.getLength()); - config.notesData(data); - } - catch (BadLocationException ex) - { - log.warn("Notes Document Bad Location: " + ex); - } - } - }); - notesContainer.add(notesEditor, BorderLayout.CENTER); - notesContainer.setBorder(new EmptyBorder(10, 10, 10, 10)); - - add(notesContainer, BorderLayout.CENTER); + add(tabGroup, BorderLayout.NORTH); + add(display, BorderLayout.CENTER); } - void setNotes(String data) + private void buildAddTab() { - notesEditor.setText(data); + addTab = new MaterialTab(addIcon, tabGroup, new JPanel()); + addTab.setOnSelectEvent(() -> { + notesManager.addPage(); + return false; + }); + } + + void rebuild() + { + tabs = new LinkedList<>(); + tabGroup.removeAll(); + + int totalNotes = notesManager.getNotes().size(); + + for (int i = 0; i < totalNotes; i++) + { + MaterialTab tab = buildTab(i); + tabs.add(tab); + tabGroup.addTab(tab); + } + + if (totalNotes < config.maxNotes()) + { + tabGroup.addTab(addTab); + } + + if (tabs.size() > 0) + { + // select the first tab + tabGroup.select(tabGroup.getTab(0)); + } + + revalidate(); + repaint(); + } + + private void onConfigChanged(ConfigChanged e) + { + if (!e.getGroup().equals(NotesConfig.CONFIG_GROUP)) + { + return; + } + + rebuild(); + } + + private void onPageAdded(PageAdded e) + { + MaterialTab tab = buildTab(e.getIndex()); + tabs.add(tab); + tabGroup.addTab(tab); + + // re-add add button to make it last + tabGroup.removeTab(addTab); + if (notesManager.getNotes().size() < config.maxNotes()) + { + tabGroup.addTab(addTab); + } + + revalidate(); + repaint(); + } + + private void onPageDeleted(PageDeleted e) + { + rebuild(); + } + + private MaterialTab buildTab(int index) + { + String name = String.valueOf(index + 1); + NoteTab noteTab = new NoteTab(notesManager, index); + + MaterialTab materialTab = new MaterialTab(name, tabGroup, noteTab); + materialTab.setPreferredSize(new Dimension(30, 27)); + materialTab.setName(name); + + final JMenuItem deleteMenuItem = new JMenuItem(); + deleteMenuItem.setText(String.format("Delete note %s", name)); + + deleteMenuItem.addActionListener(e -> { + if (JOptionPane.showConfirmDialog(getRootFrame(), String.format("Delete note page %s?", name), "Notes", YES_NO_OPTION) != YES_OPTION) + { + return; + } + try + { + notesManager.deletePage(index); + } + catch (DeleteOnlyPageException err) + { + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(getRootFrame(), + "Cannot delete the last page", + "Notes", ERROR_MESSAGE)); + } + }); + + final JPopupMenu contextMenu = new JPopupMenu(); + contextMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + contextMenu.add(deleteMenuItem); + + materialTab.setComponentPopupMenu(contextMenu); + + return materialTab; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPlugin.java index 4b5777c8fc..7f5a5f4162 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesPlugin.java @@ -52,6 +52,9 @@ public class NotesPlugin extends Plugin @Inject private NotesConfig config; + @Inject + private NotesManager notesManager; + @Inject private EventBus eventBus; @@ -82,6 +85,9 @@ public class NotesPlugin extends Plugin .build(); clientToolbar.addNavigation(navButton); + + notesManager.loadNotes(); + panel.rebuild(); } @Override @@ -94,8 +100,7 @@ public class NotesPlugin extends Plugin private void onSessionOpen(SessionOpen event) { - // update notes - String data = config.notesData(); - panel.setNotes(data); + notesManager.loadNotes(); + panel.rebuild(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageAdded.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageAdded.java new file mode 100644 index 0000000000..710d1b5fd4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageAdded.java @@ -0,0 +1,14 @@ +package net.runelite.client.plugins.notes.events; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.events.Event; + +@AllArgsConstructor +public class PageAdded implements Event +{ + @Getter + @Setter + private int index; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageDeleted.java b/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageDeleted.java new file mode 100644 index 0000000000..060c65cd45 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageDeleted.java @@ -0,0 +1,14 @@ +package net.runelite.client.plugins.notes.events; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.events.Event; + +@AllArgsConstructor +public class PageDeleted implements Event +{ + @Getter + @Setter + private int index; +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/materialtabs/MaterialTabGroup.java b/runelite-client/src/main/java/net/runelite/client/ui/components/materialtabs/MaterialTabGroup.java index d6ef646b50..b106428a96 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/components/materialtabs/MaterialTabGroup.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/materialtabs/MaterialTabGroup.java @@ -87,6 +87,18 @@ public class MaterialTabGroup extends JPanel { tabs.add(tab); add(tab, BorderLayout.NORTH); + + invalidate(); + repaint(); + } + + public void removeTab(MaterialTab tab) + { + tabs.remove(tab); + remove(tab); + + invalidate(); + repaint(); } /*** diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/notes/add_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/notes/add_icon.png new file mode 100644 index 0000000000..343c3dce0c Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/notes/add_icon.png differ