Add support for multiple note tabs

This commit is contained in:
Manatsawin Hanmongkolchai
2019-10-02 00:28:28 +07:00
parent c1e24e8188
commit c089dc9cb4
10 changed files with 416 additions and 125 deletions

View File

@@ -0,0 +1,7 @@
package net.runelite.client.plugins.notes;
class DeleteOnlyPageException extends RuntimeException {
DeleteOnlyPageException() {
super("Cannot delete the only page");
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (c) 2019, whs <https://github.com/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));
}
}

View File

@@ -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 = "",

View File

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019, whs <https://github.com/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<String> 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<ArrayList<String>>() {
}.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();
}
}

View File

@@ -1,6 +1,7 @@
/*
* Copyright (c) 2018 Charlie Waters
* Copyright (c) 2018, Psikoi <https://github.com/psikoi>
* Copyright (c) 2019, whs <https://github.com/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<MaterialTab> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
/***

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B