介绍
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*/@Overridepublic JComponent createPanel(String locationHash) {return viewMap.computeIfAbsent(locationHash, key -> {return new NotesView(locationHash, loggerMap.get(locationHash));});}/*** 缓存logger** @param projectHash* @param logger* @param openThisPage*/@Overridepublic void openProject(String projectHash, Logger logger, Runnable openThisPage) {loggerMap.put(projectHash, logger);}/*** 项目关闭时,清理相关缓存** @param projectHash*/@Overridepublic void closeProject(String projectHash) {NotesView notesView = viewMap.remove(projectHash);if (notesView != null) {notesView.run();}loggerMap.remove(projectHash);}/*** 插件卸载,清理缓存*/@Overridepublic void unInstall() {viewMap.values().forEach(Runnable::run);viewMap.clear();loggerMap.clear();}/*** 插件面板icon** @return*/@Overridepublic Icon pluginIcon() {return Helper.findIcon("logo.svg", PluginImpl.class);}/*** 插件打开,顶部的tab icon** @return*/@Overridepublic Icon pluginTabIcon() {return Helper.findIcon("logo_tab.svg", PluginImpl.class);}/*** 插件名称** @return*/@Overridepublic String pluginName() {return "笔记";}/*** 插件描述** @return*/@Overridepublic String pluginDesc() {return "这是一个笔记插件";}/*** 插件版本** @return*/@Overridepublic String pluginVersion() {return "0.0.1";}/*** 插件内容tab右侧的按钮** @param locationHash* @return*/@Overridepublic List<Action> swingTabPanelActions(String locationHash) {return Arrays.asList(new Action() {@Overridepublic Icon icon() {return Helper.findIcon("icons/home.svg", PluginImpl.class);}@Overridepublic String title() {return "主页";}@Overridepublic void actionPerformed() {//如果未选中,点击则打开主页面板if (!isSelected()) {viewMap.get(locationHash).switchHomeView();}}/*** 按钮是否需要选中* @return*/@Overridepublic boolean isSelected() {return viewMap.get(locationHash).isHomeView();}}, new Action() {@Overridepublic Icon icon() {return Helper.findIcon("icons/content.svg", PluginImpl.class);}@Overridepublic String title() {return "内容";}@Overridepublic void actionPerformed() {//按钮未选中,则触发if (!isSelected()) {//如果不能切换到内容视图,则激活日志面板,打印提示日志if (!viewMap.get(locationHash).switchContentView()) {loggerMap.get(locationHash).activeConsolePanel();loggerMap.get(locationHash).warn("请先选择对应的节点");}}}/*** 按钮是否需要选中* @return*/@Overridepublic 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() {//切换内容面板,需要判断是否切换成功//获取视图面板当前选中的节点,没有就是falsereturn 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);}/*** 卸载,项目关闭回调*/@Overridepublic 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);//创建treethis.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() {@Overridepublic 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() {@Overridepublic 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);}//添加数据节点到父节点的childrenchildrenList.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);}@Overridepublic void insertUpdate(DocumentEvent e) {this.data.setText(textEditorPane.getText());DataManager.storeData(datas.get(), locationHash);}@Overridepublic void removeUpdate(DocumentEvent e) {this.data.setText(textEditorPane.getText());DataManager.storeData(datas.get(), locationHash);}@Overridepublic void changedUpdate(DocumentEvent e) {this.data.setText(textEditorPane.getText());DataManager.storeData(datas.get(), locationHash);}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(name, text, children);}@Overridepublic String toString() {return name;}
}
操作说明
选中节点点击添加,会在当前节点新增节点
未选中节点添加,则在root节点新增节点
点击节点外部内容,即可取消选中
双击展开树节点
三击打开内容面板
右键菜单,如果是树,则会多一个删除目录菜单
- ``