介绍
Demo说明
本文基于maven项目开发,idea版本为2022.3以上,jdk为1.8
本文在JTools插件之上进行开发
本插件目标是做一款笔记插件,用于开发者在开发过程中随时记录信息
仓库地址:
jtools-notes
JTools插件说明
Tools插件是一个Idea插件,此插件提供统一Spi规范,极大的降低了idea插件的开发难度,并提供开发者模块,可以极大的为开发者开发此插件提供便利
Tools插件安装需要idea2022.3以上版本
- 插件下载连接:
https://download.csdn.net/download/qq_42413011/89702325
- pojo-serializer插件:
https://gitee.com/myprofile/pojo-serializer
成果展示
依赖安装
<dependency>
<groupId>com.fifesoft</groupId>
<artifactId>rsyntaxtextarea</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.52</version>
</dependency>
点击这里动态安装插件sdk
创建PluginImpl
package com.lhstack.aaa;
import com.lhstack.tools.plugins.Action;
import com.lhstack.tools.plugins.Helper;
import com.lhstack.tools.plugins.IPlugin;
import com.lhstack.tools.plugins.Logger;
import javax.swing.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PluginImpl implements IPlugin {
/**
* 缓存笔记视图,key=project locationHash
*/
private final Map<String, NotesView> viewMap = new HashMap<>();
/**
* 缓存logger,key=project locationHash
*/
private final Map<String, Logger> loggerMap = new HashMap<>();
/**
* 创建笔记视图
*
* @param locationHash
* @return
*/
@Override
public JComponent createPanel(String locationHash) {
return viewMap.computeIfAbsent(locationHash, key -> {
return new NotesView(locationHash, loggerMap.get(locationHash));
});
}
/**
* 缓存logger
*
* @param projectHash
* @param logger
* @param openThisPage
*/
@Override
public void openProject(String projectHash, Logger logger, Runnable openThisPage) {
loggerMap.put(projectHash, logger);
}
/**
* 项目关闭时,清理相关缓存
*
* @param projectHash
*/
@Override
public void closeProject(String projectHash) {
NotesView notesView = viewMap.remove(projectHash);
if (notesView != null) {
notesView.run();
}
loggerMap.remove(projectHash);
}
/**
* 插件卸载,清理缓存
*/
@Override
public void unInstall() {
viewMap.values().forEach(Runnable::run);
viewMap.clear();
loggerMap.clear();
}
/**
* 插件面板icon
*
* @return
*/
@Override
public Icon pluginIcon() {
return Helper.findIcon("logo.svg", PluginImpl.class);
}
/**
* 插件打开,顶部的tab icon
*
* @return
*/
@Override
public Icon pluginTabIcon() {
return Helper.findIcon("logo_tab.svg", PluginImpl.class);
}
/**
* 插件名称
*
* @return
*/
@Override
public String pluginName() {
return "笔记";
}
/**
* 插件描述
*
* @return
*/
@Override
public String pluginDesc() {
return "这是一个笔记插件";
}
/**
* 插件版本
*
* @return
*/
@Override
public String pluginVersion() {
return "0.0.1";
}
/**
* 插件内容tab右侧的按钮
*
* @param locationHash
* @return
*/
@Override
public List<Action> swingTabPanelActions(String locationHash) {
return Arrays.asList(new Action() {
@Override
public Icon icon() {
return Helper.findIcon("icons/home.svg", PluginImpl.class);
}
@Override
public String title() {
return "主页";
}
@Override
public void actionPerformed() {
//如果未选中,点击则打开主页面板
if (!isSelected()) {
viewMap.get(locationHash).switchHomeView();
}
}
/**
* 按钮是否需要选中
* @return
*/
@Override
public boolean isSelected() {
return viewMap.get(locationHash).isHomeView();
}
}, new Action() {
@Override
public Icon icon() {
return Helper.findIcon("icons/content.svg", PluginImpl.class);
}
@Override
public String title() {
return "内容";
}
@Override
public void actionPerformed() {
//按钮未选中,则触发
if (!isSelected()) {
//如果不能切换到内容视图,则激活日志面板,打印提示日志
if (!viewMap.get(locationHash).switchContentView()) {
loggerMap.get(locationHash).activeConsolePanel();
loggerMap.get(locationHash).warn("请先选择对应的节点");
}
}
}
/**
* 按钮是否需要选中
* @return
*/
@Override
public boolean isSelected() {
return viewMap.get(locationHash).isContentView();
}
});
}
}
META-INF/ToolsPlugin.txt
com.lhstack.aaa.PluginImpl
视图代码
package com.lhstack.aaa;
import com.lhstack.tools.plugins.Logger;
import javax.swing.*;
import java.awt.*;
public class NotesView extends JPanel implements Runnable {
private static final String HOME_PAGE = "HOME";
private static final String CONTENT_PAGE = "CONTENT";
private final HomeView homeView;
private final ContentView contentView;
private final CardLayout cardLayout;
private String currentView;
public NotesView(String locationHash, Logger logger) {
//笔记主页视图
this.homeView = new HomeView(locationHash, this, logger);
//笔记内容视图
this.contentView = new ContentView(locationHash, this, logger, homeView::getDatas);
//创建卡片布局
this.cardLayout = new CardLayout();
//添加主页视图到布局
cardLayout.addLayoutComponent(homeView, HOME_PAGE);
//添加内容视图到布局
cardLayout.addLayoutComponent(contentView, CONTENT_PAGE);
//添加视图到容器
this.add(homeView);
this.add(contentView);
//为容器设置卡片布局
this.setLayout(cardLayout);
//显示主页视图
this.cardLayout.show(this, HOME_PAGE);
//缓存当前显示的视图
this.currentView = HOME_PAGE;
}
public void switchHomeView() {
//显示主页
cardLayout.show(this, HOME_PAGE);
//设置当前显示的视图为主页
currentView = HOME_PAGE;
}
public boolean switchContentView() {
//切换内容面板,需要判断是否切换成功
//获取视图面板当前选中的节点,没有就是false
return this.homeView.getSelectedData().map(data -> {
//获取到,则将当前节点放入内容视图
this.contentView.onShow(data);
//切换到内容视图
cardLayout.show(this, CONTENT_PAGE);
//修改当前缓存视图为内容视图
currentView = CONTENT_PAGE;
return true;
}).orElse(false);
}
/**
* 判断当前是否为内容视图,用于按钮选中效果
*
* @return
*/
public boolean isContentView() {
return currentView.equals(CONTENT_PAGE);
}
/**
* @return
*/
public boolean isHomeView() {
return currentView.equals(HOME_PAGE);
}
/**
* 卸载,项目关闭回调
*/
@Override
public void run() {
this.contentView.run();
}
}
主页视图
package com.lhstack.aaa;
import com.lhstack.aaa.entity.Data;
import com.lhstack.tools.plugins.Helper;
import com.lhstack.tools.plugins.Logger;
import javax.swing.*;
import javax.swing.border.MatteBorder;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
public class HomeView extends JPanel {
private final NotesView notesView;
private final Logger logger;
private final String locationHash;
private JTree tree;
private List<Data> datas;
private DefaultTreeModel treeModel;
private DefaultMutableTreeNode root;
public HomeView(String locationHash, NotesView notesView, Logger logger) {
this.notesView = notesView;
this.logger = logger;
this.locationHash = locationHash;
this.setLayout(new BorderLayout());
this.initMenu();
this.initContent();
}
private void initContent() {
//加载数据
this.datas = DataManager.loadData(locationHash);
//创建root节点
this.root = new DefaultMutableTreeNode();
//初始化树
initTree(root, datas);
//创建树模型
this.treeModel = new DefaultTreeModel(root);
//创建tree
this.tree = new JTree(treeModel);
//不显示root节点
this.tree.setRootVisible(false);
//设置不可编辑
this.tree.setEditable(false);
//创建render,自定义未选中的背景色
DefaultTreeCellRenderer cellRenderer = new DefaultTreeCellRenderer();
//设置为透明背景
cellRenderer.setBackgroundNonSelectionColor(new Color(0, 0, 0, 0));
this.tree.setCellRenderer(cellRenderer);
//自定义选择模式
this.tree.setSelectionModel(new DefaultTreeSelectionModel() {
@Override
public void setSelectionPath(TreePath path) {
super.setSelectionPath(path);
if (path != null) {
tree.scrollPathToVisible(path);
}
}
});
//设置不支持多选
this.tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
//设置鼠标监听
this.tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// 根据点击的位置获取最近的行
int row = tree.getClosestRowForLocation(e.getX(), e.getY());
// 获取行高和树的行数
int rowHeight = tree.getRowHeight();
int totalRows = tree.getRowCount();
// 如果点击的位置超出了树的行数总高度,取消选中
if (e.getY() > totalRows * rowHeight || row == -1) {
tree.clearSelection(); // 如果点击的不是任何行,取消选中
} else {
tree.setSelectionRow(row); // 选中行
}
//获取选中的节点
TreePath treePath = tree.getSelectionPath();
if (treePath != null) {
//右键菜单
if (SwingUtilities.isRightMouseButton(e)) {
JPopupMenu popupMenu = new JPopupMenu();
JMenuItem addNodeItem = new JMenuItem("新增节点", Helper.findIcon("icons/addNode.svg", HomeView.class));
addNodeItem.addActionListener(event -> {
try {
String name = JOptionPane.showInputDialog("请输入节点名称");
if (name == null || name.trim().isEmpty()) {
logger.activeConsolePanel();
logger.warn("新增节点,节点名称不能为空");
return;
}
//当前节点作为父节点
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) treePath.getLastPathComponent();
//获取节点数据
Data parentData = (Data) parentNode.getUserObject();
//创建数据节点
Data data = new Data();
data.setName(name);
//获取父级几点children,如果没有,则初始化
List<Data> childrenList = parentData.getChildren();
if (childrenList == null) {
childrenList = new ArrayList<>();
parentData.setChildren(childrenList);
}
//添加数据节点到父节点的children
childrenList.add(data);
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(data);
parentNode.add(newNode);
//更新视图
treeModel.insertNodeInto(newNode, parentNode, parentNode.getIndex(newNode));
//持久化数据
DataManager.storeData(datas, locationHash);
} catch (Throwable err) {
logger.error("添加节点失败: " + err);
}
});
JMenuItem removeItem = new JMenuItem("删除节点", Helper.findIcon("icons/deleteNode.svg", HomeView.class));
removeItem.addActionListener(event -> {
int confirm = JOptionPane.showConfirmDialog(null, "你确定要删除节点吗,如果是树节点,子节点的内容会放到这个节点的父级", "警告", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
if (confirm == JOptionPane.OK_OPTION) {
try {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) treePath.getLastPathComponent();
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent();
List<Data> parenDataList;
if (parent == null) {
parent = root;
parenDataList = datas;
} else {
Data parentData = (Data) parent.getUserObject();
if (parentData == null) {
parenDataList = datas;
} else {
parenDataList = parentData.getChildren();
if (parenDataList == null) {
parenDataList = new ArrayList<>();
parentData.setChildren(parenDataList);
}
}
}
Enumeration<TreeNode> children = node.children();
parent.remove(node);
parenDataList.remove((Data) node.getUserObject());
if (children != null) {
while (children.hasMoreElements()) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement();
parent.add(child);
parenDataList.add((Data) child.getUserObject());
}
}
treeModel.reload(parent);
logger.info(datas);
DataManager.storeData(datas, locationHash);
} catch (Throwable err) {
logger.error("删除节点错误: " + err);
}
}
});
JMenuItem editItem = new JMenuItem("编辑节点", Helper.findIcon("icons/editNode.svg", HomeView.class));
editItem.addActionListener(event -> {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) treePath.getLastPathComponent();
Data data = (Data) node.getUserObject();
String name = JOptionPane.showInputDialog(null, "请输入节点名称", data.getName());
if (name == null || name.trim().isEmpty()) {
logger.activeConsolePanel();
logger.warn("编辑节点名称不能为空");
return;
}
data.setName(name);
treeModel.nodeStructureChanged(node);
DataManager.storeData(datas, locationHash);
});
JMenuItem openContentItem = new JMenuItem("打开内容", Helper.findIcon("icons/open.svg", HomeView.class));
openContentItem.addActionListener(event -> {
notesView.switchContentView();
});
popupMenu.add(addNodeItem);
popupMenu.add(removeItem);
popupMenu.add(editItem);
popupMenu.add(openContentItem);
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) treePath.getLastPathComponent();
if (treeNode.getChildCount() > 0) {
JMenuItem removeDirItem = new JMenuItem("删除目录", Helper.findIcon("icons/deleteNode.svg", HomeView.class));
removeDirItem.setToolTipText("删除整个目录和目录下所有的节点");
removeDirItem.addActionListener(event -> {
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) treeNode.getParent();
treeNode.removeFromParent();
List<Data> parentDataList;
Data data = (Data) parent.getUserObject();
if (data == null) {
parentDataList = datas;
} else {
parentDataList = data.getChildren();
if (parentDataList == null) {
parentDataList = new ArrayList<>();
data.setChildren(parentDataList);
}
}
parentDataList.remove((Data) treeNode.getUserObject());
treeModel.reload(parent);
DataManager.storeData(datas, locationHash);
});
popupMenu.add(removeDirItem);
}
popupMenu.show(e.getComponent(), e.getX() + 10, e.getY() + 10);
}
if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 3) {
notesView.switchContentView();
}
}
}
});
Helper.treeSpeedSearch(tree, true, treePath -> {
DefaultMutableTreeNode lastPathComponent = (DefaultMutableTreeNode) treePath.getLastPathComponent();
Object userObject = lastPathComponent.getUserObject();
if (userObject != null) {
Data data = (Data) userObject;
return data.getName();
}
return "";
});
JScrollPane jScrollPane = new JScrollPane(this.tree);
MatteBorder border = BorderFactory.createMatteBorder(1, 0, 0, 0, Color.gray);
this.tree.setBorder(border);
this.add(jScrollPane, BorderLayout.CENTER);
}
public Optional<Data> getSelectedData() {
return Optional.ofNullable(tree.getSelectionPath()).map(TreePath::getLastPathComponent)
.map(DefaultMutableTreeNode.class::cast).map(DefaultMutableTreeNode::getUserObject).map(Data.class::cast);
}
private void initTree(DefaultMutableTreeNode root, List<Data> datas) {
datas.forEach(data -> {
DefaultMutableTreeNode node = new DefaultMutableTreeNode(data);
root.add(node);
if (data.getChildren() != null && !data.getChildren().isEmpty()) {
initTree(node, data.getChildren());
}
});
}
private void initMenu() {
JPanel panel = new JPanel();
panel.setBorder(null);
panel.setLayout(new FlowLayout(FlowLayout.RIGHT));
panel.add(Helper.actionButton(Helper.findIcon("icons/unfold.svg", PluginImpl.class), "全部展开", str -> {
expandAll();
}));
panel.add(Helper.actionButton(Helper.findIcon("icons/packup.svg", PluginImpl.class), "全部收起", str -> {
collapseAll();
}));
panel.add(Helper.actionButton(Helper.findIcon("icons/newNode.svg", PluginImpl.class), "新增节点", str -> {
SwingUtilities.invokeLater(() -> {
try {
String name = JOptionPane.showInputDialog("请输入节点名称");
if (name == null || name.trim().isEmpty()) {
logger.activeConsolePanel();
logger.warn("新增节点,节点名称不能为空");
return;
}
TreePath treePath = tree.getSelectionPath();
Data data = new Data();
data.setName(name);
if (treePath != null) {
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) treePath.getLastPathComponent();
Data parentData = (Data) parentNode.getUserObject();
List<Data> childrenList = parentData.getChildren();
if (childrenList == null) {
childrenList = new ArrayList<>();
parentData.setChildren(childrenList);
} else {
parentData.getChildren().add(data);
}
childrenList.add(data);
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(data);
parentNode.add(newNode);
treeModel.insertNodeInto(newNode, parentNode, parentNode.getIndex(newNode));
} else {
datas.add(data);
DefaultMutableTreeNode node = new DefaultMutableTreeNode(data);
root.add(node);
if (root.getChildCount() == 1) {
treeModel.reload(root);
} else {
treeModel.insertNodeInto(node, root, root.getIndex(node));
}
}
DataManager.storeData(datas, locationHash);
} catch (Throwable err) {
logger.error("添加节点失败: " + err);
}
});
}));
this.add(panel, BorderLayout.NORTH);
}
private void collapseAll() {
for (int i = 0; i < tree.getRowCount(); i++) {
tree.collapseRow(i);
}
}
private void expandAll() {
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
}
public List<Data> getDatas() {
return datas;
}
}
内容视图
package com.lhstack.aaa;
import com.lhstack.aaa.entity.Data;
import com.lhstack.tools.plugins.Logger;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rsyntaxtextarea.TextEditorPane;
import org.fife.ui.rtextarea.RTextScrollPane;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import java.awt.*;
import java.util.List;
import java.util.function.Supplier;
public class ContentView extends JPanel implements DocumentListener, Runnable {
private final TextEditorPane textEditorPane;
private final JLabel title;
private final Logger logger;
private final String locationHash;
private final Supplier<List<Data>> datas;
private Data data;
public ContentView(String locationHash, NotesView notesView, Logger logger, Supplier<List<Data>> datas) {
this.setLayout(new BorderLayout());
this.setBorder(null);
this.logger = logger;
this.textEditorPane = initTextEditorPane();
this.title = new JLabel();
this.title.setFont(new Font("", Font.PLAIN, 16));
this.add(title, BorderLayout.NORTH);
RTextScrollPane rTextScrollPane = new RTextScrollPane(this.textEditorPane);
rTextScrollPane.setBorder(null);
this.add(rTextScrollPane, BorderLayout.CENTER);
this.datas = datas;
this.locationHash = locationHash;
}
private TextEditorPane initTextEditorPane() {
TextEditorPane pane = new TextEditorPane();
pane.setTabSize(2);
pane.setLineWrap(true);
pane.setHighlightCurrentLine(true);
pane.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
pane.setCodeFoldingEnabled(true);
return pane;
}
public void onShow(Data data) {
this.data = data;
this.title.setText(data.getName());
this.title.setHorizontalAlignment(JLabel.CENTER);
Document document = this.textEditorPane.getDocument();
document.removeDocumentListener(this);
this.textEditorPane.setText(data.getText());
document.addDocumentListener(this);
}
@Override
public void insertUpdate(DocumentEvent e) {
this.data.setText(textEditorPane.getText());
DataManager.storeData(datas.get(), locationHash);
}
@Override
public void removeUpdate(DocumentEvent e) {
this.data.setText(textEditorPane.getText());
DataManager.storeData(datas.get(), locationHash);
}
@Override
public void changedUpdate(DocumentEvent e) {
this.data.setText(textEditorPane.getText());
DataManager.storeData(datas.get(), locationHash);
}
@Override
public void run() {
this.textEditorPane.resetKeyboardActions();
this.textEditorPane.clearParsers();
this.textEditorPane.clearMarkAllHighlights();
}
}
数据加载,持久化管理
package com.lhstack.aaa;
import com.alibaba.fastjson2.JSON;
import com.lhstack.aaa.entity.Data;
import com.lhstack.tools.plugins.Helper;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
public class DataManager {
public static File loadFile(String locationHash) {
try {
String path = Helper.getProjectBasePath(locationHash);
File file = new File(path, ".idea/JTools/notes");
if (!file.exists()) {
file.mkdirs();
}
File dataFile = new File(file, "data.json");
if (!dataFile.exists()) {
dataFile.createNewFile();
}
return dataFile;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public static void storeData(List<Data> data, String locationHash) {
try {
File file = loadFile(locationHash);
Files.write(file.toPath(), JSON.toJSONBytes(data));
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public static List<Data> loadData(String locationHash) {
try {
File file = loadFile(locationHash);
if(file.length() <= 0){
return new ArrayList<>();
}
byte[] bytes = Files.readAllBytes(file.toPath());
return JSON.parseArray(new String(bytes, StandardCharsets.UTF_8), Data.class);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
数据结构定义
package com.lhstack.aaa.entity;
import java.util.List;
import java.util.Objects;
public class Data {
private String name;
private String text;
private List<Data> children;
public String getName() {
return name;
}
public Data setName(String name) {
this.name = name;
return this;
}
public String getText() {
return text;
}
public Data setText(String text) {
this.text = text;
return this;
}
public List<Data> getChildren() {
return children;
}
public Data setChildren(List<Data> children) {
this.children = children;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Data data = (Data) o;
return Objects.equals(name, data.name) && Objects.equals(text, data.text) && Objects.equals(children, data.children);
}
@Override
public int hashCode() {
return Objects.hash(name, text, children);
}
@Override
public String toString() {
return name;
}
}
操作说明
选中节点点击添加,会在当前节点新增节点
未选中节点添加,则在root节点新增节点
点击节点外部内容,即可取消选中
双击展开树节点
三击打开内容面板
右键菜单,如果是树,则会多一个删除目录菜单
- ``