From c089dc9cb4f8ce33668eb40f9aab684defa553f9 Mon Sep 17 00:00:00 2001 From: Manatsawin Hanmongkolchai Date: Wed, 2 Oct 2019 00:28:28 +0700 Subject: [PATCH] Add support for multiple note tabs --- .../notes/DeleteOnlyPageException.java | 7 + .../client/plugins/notes/NoteTab.java | 123 +++++++++ .../client/plugins/notes/NotesConfig.java | 5 + .../client/plugins/notes/NotesManager.java | 110 +++++++++ .../client/plugins/notes/NotesPanel.java | 233 +++++++++--------- .../client/plugins/notes/NotesPlugin.java | 18 +- .../plugins/notes/events/PageAdded.java | 13 + .../plugins/notes/events/PageDeleted.java | 13 + .../materialtabs/MaterialTabGroup.java | 19 +- .../client/plugins/notes/add_icon.png | Bin 0 -> 121 bytes 10 files changed, 416 insertions(+), 125 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/notes/DeleteOnlyPageException.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/notes/NoteTab.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesManager.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageAdded.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageDeleted.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/notes/add_icon.png 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..31604cb5e5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/DeleteOnlyPageException.java @@ -0,0 +1,7 @@ +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..1aad52881f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NoteTab.java @@ -0,0 +1,123 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; +import net.runelite.client.ui.ColorScheme; + +import javax.swing.*; +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 java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; + +@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..82ac2ef3c6 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 @@ -31,6 +31,11 @@ import net.runelite.client.config.ConfigItem; @ConfigGroup("notes") public interface NotesConfig extends Config { + String CONFIG_GROUP = "notes"; + String NOTES = "notes"; + + int MAX_NOTES = 5; + @ConfigItem( keyName = "notesData", name = "", 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..0bb1aa9fc7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/NotesManager.java @@ -0,0 +1,110 @@ +/* + * 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 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; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; + +@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<>(); + } + + // migrate from legacy single tab notes + if (!config.notesData().isEmpty()) { + log.info("Adding tab for legacy note data"); + notes.add(0, config.notesData()); + } + } + + 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..0e7121f6d6 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 @@ -25,140 +26,144 @@ */ 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.inject.Singleton; -import javax.swing.AbstractAction; -import javax.swing.BorderFactory; -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.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; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static javax.swing.JOptionPane.*; @Slf4j @Singleton -class NotesPanel extends PluginPanel -{ - private final JTextArea notesEditor = new JTextArea(); - private final UndoManager undoRedo = new UndoManager(); +class NotesPanel extends PluginPanel { + @Inject + private NotesManager notesManager; - void init(final NotesConfig config) - { - // 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); + @Inject + private EventBus eventBus; - setLayout(new BorderLayout()); - setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - setBackground(ColorScheme.DARK_GRAY_COLOR); + 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<>(); - notesEditor.setTabSize(2); - notesEditor.setLineWrap(true); - notesEditor.setWrapStyleWord(true); + void init(final NotesConfig config) { + eventBus.subscribe(PageAdded.class, this, this::onPageAdded); + eventBus.subscribe(PageDeleted.class, this, this::onPageDeleted); - JPanel notesContainer = new JPanel(); - notesContainer.setLayout(new BorderLayout()); - notesContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + // 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); - notesEditor.setOpaque(false); + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); - // load note text - String data = config.notesData(); - notesEditor.setText(data); + tabGroup.setBorder(new EmptyBorder(0, 0, 10, 0)); - // 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())); + buildAddTab(); - notesEditor.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo"); - notesEditor.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo"); + add(tabGroup, BorderLayout.NORTH); + add(display, BorderLayout.CENTER); + } - 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); - } - } - }); + private void buildAddTab() { + addTab = new MaterialTab(addIcon, tabGroup, new JPanel()); + addTab.setOnSelectEvent(() -> { + notesManager.addPage(); + return false; + }); + } - 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); - } - } - }); + void rebuild() { + tabs = new LinkedList<>(); + tabGroup.removeAll(); - notesEditor.addFocusListener(new FocusListener() - { - @Override - public void focusGained(FocusEvent e) - { + int totalNotes = notesManager.getNotes().size(); - } + for (int i = 0; i < totalNotes; i++) { + MaterialTab tab = buildTab(i); + tabs.add(tab); + tabGroup.addTab(tab); + } - @Override - public void focusLost(FocusEvent e) - { - notesChanged(notesEditor.getDocument()); - } + if (totalNotes < NotesConfig.MAX_NOTES) { + tabGroup.addTab(addTab); + } - 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)); + if (tabs.size() > 0) { + // select the first tab + tabGroup.select(tabGroup.getTab(0)); + } - add(notesContainer, BorderLayout.CENTER); - } + revalidate(); + repaint(); + } - void setNotes(String data) - { - notesEditor.setText(data); - } + 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() < NotesConfig.MAX_NOTES) { + 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..6896fb36d0 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 @@ -25,9 +25,6 @@ package net.runelite.client.plugins.notes; import com.google.inject.Provides; -import java.awt.image.BufferedImage; -import javax.inject.Inject; -import javax.inject.Singleton; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; import net.runelite.client.events.SessionOpen; @@ -37,6 +34,10 @@ import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.ImageUtil; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.awt.image.BufferedImage; + @PluginDescriptor( name = "Notes", description = "Enable the Notes panel", @@ -52,6 +53,9 @@ public class NotesPlugin extends Plugin @Inject private NotesConfig config; + @Inject + private NotesManager notesManager; + @Inject private EventBus eventBus; @@ -82,6 +86,9 @@ public class NotesPlugin extends Plugin .build(); clientToolbar.addNavigation(navButton); + + notesManager.loadNotes(); + panel.rebuild(); } @Override @@ -94,8 +101,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..12428f98f1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageAdded.java @@ -0,0 +1,13 @@ +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..fbc6fdbe6e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/notes/events/PageDeleted.java @@ -0,0 +1,13 @@ +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..8fda3a1628 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 @@ -24,11 +24,10 @@ */ package net.runelite.client.ui.components.materialtabs; -import java.awt.BorderLayout; -import java.awt.FlowLayout; +import javax.swing.*; +import java.awt.*; import java.util.ArrayList; import java.util.List; -import javax.swing.JPanel; /** * This class will be a container (group) for the new Material Tabs. It will @@ -83,10 +82,20 @@ public class MaterialTabGroup extends JPanel return tabs.get(index); } - public void addTab(MaterialTab tab) - { + public void addTab(MaterialTab tab) { 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 0000000000000000000000000000000000000000..343c3dce0cd5c460af2627b8a4acb39f68cc86fe GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa&H|6fVg?3oVGw3ym^DWND5&k} z;uxYaF*)G?V@#UG-}0FczsOIw5!T~$N>Is2IU-(UvQbc7qSu|vgo)vBE7!_33zDV) PwJ~_Q`njxgN@xNAsURJG literal 0 HcmV?d00001