!93 修复行号bug

Merge pull request !93 from 格物方能致知/fix-I7W7F6
This commit is contained in:
格物方能致知 2023-09-13 04:41:09 +00:00 committed by Gitee
commit 32689ddbb3
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
11 changed files with 164 additions and 260 deletions

11
pom.xml
View File

@ -16,16 +16,26 @@
<javafx.version>20.0.2</javafx.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.fxmisc.richtext/richtextfx -->
<!--JavaFX 的富文本区域-->
<dependency>
<groupId>org.fxmisc.richtext</groupId>
<artifactId>richtextfx</artifactId>
<version>0.11.1</version>
</dependency>
<!--图标库主依赖-->
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>12.3.1</version>
</dependency>
<!--蚂蚁图标库-->
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-antdesignicons-pack</artifactId>
<version>12.3.1</version>
</dependency>
<!--亚特兰大fx主题-->
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
@ -66,6 +76,7 @@
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<!--国际化依赖-->
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>

View File

@ -15,6 +15,7 @@ module org.jcnc.jnotepad {
requires org.kordamp.ikonli.core;
requires org.kordamp.ikonli.javafx;
requires org.kordamp.ikonli.antdesignicons;
requires org.fxmisc.richtext;
exports org.jcnc.jnotepad;
exports org.jcnc.jnotepad.model.enums;
exports org.jcnc.jnotepad.app.config;
@ -29,6 +30,7 @@ module org.jcnc.jnotepad {
exports org.jcnc.jnotepad.common.interfaces;
opens org.jcnc.jnotepad.app.config;
exports org.jcnc.jnotepad.views.root.center.main.bottom.status;
exports org.jcnc.jnotepad.plugin.interfaces;
exports org.jcnc.jnotepad.ui.dialog;
exports org.jcnc.jnotepad.ui.dialog.interfaces;
exports org.jcnc.jnotepad.model.entity;

View File

@ -121,7 +121,7 @@ public class OpenFile implements EventHandler<ActionEvent> {
String text = textBuilder.toString();
LogUtil.getLogger(this.getClass()).info("已调用读取文件功能");
Platform.runLater(() -> {
textArea.getMainTextArea().setText(text);
textArea.appendText(text);
CenterTab tab = createNewTab(file.getName(), textArea, encoding);
// 设置当前标签页关联本地文件
tab.setRelevance(true);

View File

@ -9,7 +9,7 @@ public class PluginInfo {
/**
* 插件名称
*/
private String pluginName;
private String name;
/**
* 插件版本
*/
@ -19,12 +19,17 @@ public class PluginInfo {
*/
private boolean enabled;
public String getPluginName() {
return pluginName;
/**
* 主类名称
*/
private String mainClass;
public String getName() {
return name;
}
public void setPluginName(String pluginName) {
this.pluginName = pluginName;
public void setName(String name) {
this.name = name;
}
public String getVersion() {
@ -42,4 +47,12 @@ public class PluginInfo {
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getMainClass() {
return mainClass;
}
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
}
}

View File

@ -1,58 +0,0 @@
package org.jcnc.jnotepad.plugin;
import org.jcnc.jnotepad.plugin.interfaces.Plugin;
import org.jcnc.jnotepad.util.LogUtil;
import java.util.Map;
/**
* 新按钮插件
*
* @author luke
*/
public class ButtonPlugin implements Plugin {
@Override
public String getCategoryName() {
return "新按钮插件";
}
@Override
public String getDisplayName() {
return "新按钮";
}
/**
* 初始化插件
*/
@Override
public void initialize() {
LogUtil.getLogger(this.getClass()).info("新按钮插件初始化了!");
}
@Override
public void execute() {
// 在这里实现新按钮插件的逻辑
LogUtil.getLogger(this.getClass()).info("新按钮插件执行了!");
}
/**
* 获取插件的配置参数
*
* @return 插件的配置参数
*/
@Override
public Map<String, Object> getConfig() {
return null;
}
/**
* 设置插件的配置参数
*
* @param config 插件的配置参数
*/
@Override
public void setConfig(Map<String, Object> config) {
}
}

View File

@ -8,7 +8,9 @@ import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.jcnc.jnotepad.ui.dialog.factory.impl.BasicFileChooserFactory;
import org.jcnc.jnotepad.util.LogUtil;
import org.jcnc.jnotepad.util.PopUpUtil;
import org.jcnc.jnotepad.util.UiUtil;
import org.slf4j.Logger;
import java.io.File;
import java.util.List;
@ -22,6 +24,7 @@ import java.util.Map;
* @author luke
*/
public class PluginDemo {
Logger logger = LogUtil.getLogger(this.getClass());
/**
* 启动插件演示界面
@ -53,8 +56,8 @@ public class PluginDemo {
/**
* 创建加载插件的按钮
*
* @param primaryStage JavaFX的主舞台
* @param fileChooser 文件选择器
* @param primaryStage JavaFX的主舞台
* @param fileChooser 文件选择器
* @param pluginManager 插件管理器
* @return 加载插件的按钮
*/
@ -65,13 +68,16 @@ public class PluginDemo {
File selectedFile = fileChooser.showOpenDialog(primaryStage);
if (selectedFile != null) {
String pluginFilePath = selectedFile.getAbsolutePath();
pluginManager.loadPlugins(pluginFilePath);
PluginLoader.getInstance().loadPlugins(pluginFilePath);
// 更新插件信息显示
displayPluginInfo(primaryStage, pluginManager);
} else {
PopUpUtil.infoAlert(null, null, "未找到插件!", null, null);
logger.info("未找到插件!");
}
} catch (Exception ignored) {
LogUtil.getLogger(this.getClass()).info("未加载插件!");
} catch (Exception e) {
logger.error("加载插件失败!", e);
}
});
return loadButton;

View File

@ -0,0 +1,98 @@
package org.jcnc.jnotepad.plugin;
import org.jcnc.jnotepad.exception.AppException;
import org.jcnc.jnotepad.model.entity.PluginInfo;
import org.jcnc.jnotepad.plugin.interfaces.Plugin;
import org.jcnc.jnotepad.util.JsonUtil;
import org.jcnc.jnotepad.util.LogUtil;
import org.slf4j.Logger;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
/**
* 插件加载类
*
* @author gewuyou
*/
public class PluginLoader {
private static final PluginLoader INSTANCE = new PluginLoader();
Logger logger = LogUtil.getLogger(this.getClass());
/**
* 从插件jar包中读取 json 文件到 PluginInfo
*
* @param pluginJar jar
*/
private static PluginInfo readPlugin(File pluginJar) throws IOException {
InputStream is;
StringBuilder sb;
try (JarFile jarFile = new JarFile(pluginJar)) {
ZipEntry zipEntry = jarFile.getEntry("META-INF/plugin.json");
is = jarFile.getInputStream(zipEntry);
try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String temp;
sb = new StringBuilder();
while ((temp = br.readLine()) != null) {
sb.append(temp);
}
}
}
return JsonUtil.OBJECT_MAPPER.readValue(sb.toString(), PluginInfo.class);
}
public static PluginLoader getInstance() {
return INSTANCE;
}
/**
* 加载插件
*
* @param pluginFilePath 插件文件的路径
*/
public void loadPlugins(String pluginFilePath) {
PluginManager pluginManager = PluginManager.getInstance();
List<Plugin> plugins = pluginManager.getPlugins();
Map<String, List<String>> categories = pluginManager.getLoadedPluginsByCategory();
Map<String, PluginInfo> pluginInfos = pluginManager.getPluginInfos();
File file = new File(pluginFilePath);
if (file.exists() && file.isFile()) {
try {
PluginInfo pluginInfo = readPlugin(file);
pluginInfos.put(pluginInfo.getName(), pluginInfo);
// 创建URLClassLoader以加载Jar文件中的类
Class<?> pluginClass;
try (URLClassLoader classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()})) {
pluginClass = classLoader.loadClass(pluginInfo.getMainClass());
}
if (pluginClass == null) {
return;
}
Plugin plugin;
plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
plugins.add(plugin);
// 添加插件到类别中
String categoryName = plugin.getCategoryName();
String displayName = plugin.getDisplayName();
categories.computeIfAbsent(categoryName, k -> new ArrayList<>()).add(displayName);
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new AppException(e);
} catch (ClassNotFoundException e) {
logger.error("无法找到对应的插件类!", e);
} catch (NoSuchMethodException e) {
logger.error("插件类中没有找到指定方法!", e);
}
} else {
LogUtil.getLogger(this.getClass()).info("PluginInfo file not found: {}", pluginFilePath);
}
}
}

View File

@ -1,14 +1,10 @@
package org.jcnc.jnotepad.plugin;
import org.jcnc.jnotepad.model.entity.PluginInfo;
import org.jcnc.jnotepad.plugin.interfaces.Plugin;
import org.jcnc.jnotepad.util.LogUtil;
import org.slf4j.Logger;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -33,6 +29,11 @@ public class PluginManager {
* 插件类别
*/
private final Map<String, List<String>> categories = new HashMap<>();
/**
* 插件信息
*/
private final Map<String, PluginInfo> pluginInfos = new HashMap<>();
private PluginManager() {
@ -42,47 +43,6 @@ public class PluginManager {
return INSTANCE;
}
/**
* 加载插件
*
* @param pluginFilePath 插件文件的路径
*/
public void loadPlugins(String pluginFilePath) {
File file = new File(pluginFilePath);
if (file.exists() && file.isFile()) {
// 创建URLClassLoader以加载Jar文件中的类
Class<?> pluginClass = null;
try (URLClassLoader classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()})) {
pluginClass = classLoader.loadClass("org.jcnc.jnotepad.plugin.ButtonPlugin");
} catch (ClassNotFoundException e) {
logger.error("无法找到对应的插件类!", e);
} catch (MalformedURLException e) {
logger.error("无法创建URL格式错误!", e);
} catch (IOException e) {
logger.error("IO异常!", e);
}
if (pluginClass == null) {
return;
}
Plugin plugin = null;
try {
plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
logger.error("发生异常!", e);
}
if (plugin == null) {
return;
}
plugins.add(plugin);
// 添加插件到类别中
String categoryName = plugin.getCategoryName();
String displayName = plugin.getDisplayName();
categories.computeIfAbsent(categoryName, k -> new ArrayList<>()).add(displayName);
} else {
LogUtil.getLogger(this.getClass()).info("PluginInfo file not found: {}", pluginFilePath);
}
}
/**
* 卸载插件
@ -122,4 +82,12 @@ public class PluginManager {
public Map<String, List<String>> getLoadedPluginsByCategory() {
return categories;
}
public List<Plugin> getPlugins() {
return plugins;
}
public Map<String, PluginInfo> getPluginInfos() {
return pluginInfos;
}
}

View File

@ -1,9 +1,7 @@
package org.jcnc.jnotepad.ui.module;
import javafx.beans.property.StringProperty;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import org.jcnc.jnotepad.controller.config.AppConfigController;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.jcnc.jnotepad.util.LogUtil;
import org.jcnc.jnotepad.views.root.center.main.bottom.status.BottomStatusBox;
import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTab;
@ -22,84 +20,36 @@ import java.io.IOException;
*
* @author luke
*/
public class LineNumberTextArea extends BorderPane {
public class LineNumberTextArea extends CodeArea {
/**
* 用于确定行号区域宽度的大小表格每个元素表示不同的行数范围
*/
private static final int[] SIZE_TABLE = {9, 99, 999, 9999, 99999, 999999, 9999999,
99999999, 999999999, Integer.MAX_VALUE};
/**
* 用于记录日志的静态Logger对象
*/
private static final Logger logger = LogUtil.getLogger(LineNumberTextArea.class);
/**
* 行号区域的最小宽度
*/
private static final int MIN_LINE_NUMBER_WIDTH = 30;
/**
* 主文本区域的TextArea实例
*/
private final TextArea mainTextArea = new TextArea();
/**
* 行号区域的TextArea实例
*/
private final TextArea lineNumberArea = new TextArea();
/**
* 构造函数
* <p>
* 用于创建 LineNumberTextArea 对象
*/
public LineNumberTextArea() {
// 设置主文本区域是否自动换行根据应用配置决定
mainTextArea.setWrapText(AppConfigController.getInstance().getAutoLineConfig());
// 设置行号区域不可编辑
lineNumberArea.setEditable(false);
// 设置行号区域的首选宽度和最小宽度为最小行号宽度
lineNumberArea.setPrefWidth(MIN_LINE_NUMBER_WIDTH);
lineNumberArea.setMinWidth(MIN_LINE_NUMBER_WIDTH);
// 为行号区域和主文本区域添加CSS样式类
lineNumberArea.getStyleClass().add("text-line-number");
mainTextArea.getStyleClass().add("main-text-area");
// 设置 LineNumberTextArea 的样式包括边框和背景颜色
this.setStyle(
"-fx-border-color:white;" +
"-fx-background-color:white"
);
// 初始化监听器用于处理事件
this.setParagraphGraphicFactory(LineNumberFactory.get(this));
initListeners();
// 将主文本区域设置为中央内容将行号区域设置为左侧内容
setCenter(mainTextArea);
setLeft(lineNumberArea);
}
}
/**
* 初始化监听器方法
*/
private void initListeners() {
// 监听主要文本区域的滚动位置变化
mainTextArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> lineNumberArea.setScrollTop(mainTextArea.getScrollTop()));
// 监听行号文本区域的滚动位置变化
lineNumberArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> mainTextArea.setScrollTop(lineNumberArea.getScrollTop()));
// 监听行号文本区域的文本变化
lineNumberArea.textProperty().addListener((observable, oldValue, newValue) -> updateLineNumberWidth());
// 监听主要文本区域的光标位置变化
this.mainTextArea.caretPositionProperty().addListener((caretObservable, oldPosition, newPosition) -> BottomStatusBox.getInstance().updateWordCountStatusLabel());
// 监听主要文本区域的文本变化
this.textProperty().addListener((observable, oldValue, newValue) -> {
updateLineNumberArea();
BottomStatusBox.getInstance().updateWordCountStatusLabel();
save();
});
@ -121,7 +71,7 @@ public class LineNumberTextArea extends BorderPane {
File file = (File) tab.getUserData();
// 获取主文本区域中的文本内容
String newValue = this.mainTextArea.getText();
String newValue = this.getText();
// 如果文件对象为空记录警告信息并返回不执行保存操作
if (file == null) {
@ -140,90 +90,4 @@ public class LineNumberTextArea extends BorderPane {
LogUtil.getLogger(this.getClass()).info("已忽视IO异常!");
}
}
/**
* 更新行号宽度方法
*/
private void updateLineNumberWidth() {
// 获取主文本区域的段落数量即文本的行数
int numOfLines = mainTextArea.getParagraphs().size();
// 初始化一个计数器用于确定适合的行号宽度
int count = 1;
// 遍历行号宽度大小的表格
for (int i = 0; i < SIZE_TABLE.length; i++) {
// 检查文本行数是否在当前表格项的范围内
if (numOfLines <= SIZE_TABLE[i]) {
// 如果是设置计数器为当前索引+1并退出循环
count = i + 1;
break;
}
}
// 计算实际的行号区域宽度确保不小于一个最小宽度值
int actualWidth = Math.max(count * 10 + 11, MIN_LINE_NUMBER_WIDTH);
// 检查实际宽度是否与当前行号区域的宽度不同
if (actualWidth != lineNumberArea.getWidth()) {
// 如果不同设置行号区域的首选宽度为实际宽度
lineNumberArea.setPrefWidth(actualWidth);
}
}
/**
* 获取主要文本区域的text属性
*
* @return 主要文本区域的text属性
*/
public StringProperty textProperty() {
return mainTextArea.textProperty();
}
/**
* 更新行号区域方法
*/
private void updateLineNumberArea() {
// 获取主文本区域的垂直滚动位置
double mainTextAreaScrollTop = mainTextArea.getScrollTop();
// 获取行号区域的垂直滚动位置
double lineNumberAreaScrollTop = lineNumberArea.getScrollTop();
// 获取主文本区域的段落数量即文本的行数
int numOfLines = mainTextArea.getParagraphs().size();
// 用于构建行号文本的字符串构建器
StringBuilder lineNumberText = new StringBuilder();
// 循环迭代生成行号文本,
for (int i = 1; i <= numOfLines; i++) {
// 将行号和换行符添加到字符串中
lineNumberText.append(i);
if (i != numOfLines) {
lineNumberText.append("\n");
}
}
// 将生成的行号文本设置到 行号区域
lineNumberArea.setText(lineNumberText.toString());
// 恢复主文本区域的垂直滚动位置
mainTextArea.setScrollTop(mainTextAreaScrollTop);
// 恢复行号区域的垂直滚动位置
lineNumberArea.setScrollTop(lineNumberAreaScrollTop);
}
/**
* 获取主要文本区域
*
* @return 主要文本区域
*/
public TextArea getMainTextArea() {
return mainTextArea;
}
}

View File

@ -3,10 +3,10 @@ package org.jcnc.jnotepad.views.root.center.main.bottom.status;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import org.jcnc.jnotepad.app.i18n.UiResourceBundle;
import org.jcnc.jnotepad.common.constants.TextConstants;
import org.jcnc.jnotepad.ui.module.AbstractHorizontalBox;
import org.jcnc.jnotepad.ui.module.LineNumberTextArea;
import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTab;
import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTabPane;
@ -94,7 +94,7 @@ public class BottomStatusBox extends AbstractHorizontalBox {
if (instance.getSelected() == null) {
return;
}
TextArea textArea = instance.getSelected().getLineNumberTextArea().getMainTextArea();
LineNumberTextArea textArea = instance.getSelected().getLineNumberTextArea();
int caretPosition = textArea.getCaretPosition();
int row = getRow(caretPosition, textArea.getText());
int column = getColumn(caretPosition, textArea.getText());

View File

@ -56,7 +56,7 @@ public class CenterTab extends Tab {
public void setAutoLine(boolean autoLine) {
this.autoLine = autoLine;
lineNumberTextArea.getMainTextArea().setWrapText(autoLine);
lineNumberTextArea.setWrapText(autoLine);
}
public LineNumberTextArea getLineNumberTextArea() {