├── .gitignore ├── README.md ├── files └── FiraCode-Medium.ttf ├── lib └── flatlaf-2.6.jar └── src └── com └── intellij ├── Main.java ├── entity ├── ProcessResult.java ├── ProjectEntity.java └── config │ ├── ApplicationConfigure.java │ ├── CreateProjectConfigure.java │ └── ProjectConfigure.java ├── manage ├── FileManager.java ├── ProcessExecuteEngine.java └── ProjectManager.java └── window ├── AbstractWindow.java ├── MainWindow.java ├── WelcomeWindow.java ├── component ├── JLinePanel.java └── JListItem.java ├── dialog ├── AbstractDialog.java ├── CompileErrorDialog.java ├── CreateProjectDialog.java ├── DirectoryChooserDialog.java ├── GitProjectDialog.java └── ProjectConfigDialog.java ├── enums └── CloseAction.java ├── layout ├── ListLayout.java └── MarginLayout.java └── service ├── AbstractService.java ├── MainService.java └── WelcomeService.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | out/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于Swing实现的简易Java IDE 2 | 视频教程地址:https://www.bilibili.com/video/BV1G84y1v7Vj/ 3 | 4 | 全套文档下载包含: 5 | * 已编译好的可执行Java程序一份 6 | * 项目源代码一份 7 | * 工程实践报告一份 8 | 9 | ## 项目背景 10 | 随着时代的发展,计算机软件正朝着让更多大众用户也能轻松上手的方向发展, 11 | 在计算机发展早期,我们都是使用纯命令行的形式进行编程、对计算机进行操 12 | 作等,但是只有专业的计算机从业人员才具备这样的能力,对应我们这些刚入 13 | 门的学生来说,太过复杂,我们需要的是一种更加简单方便的开发工具。 14 | 15 | ## 目的 16 | 开发一个简易的集成开发环境,支持对项目的一键编译、运行, 17 | 全程都是使用图形化界面进行操作,相比命令行的形式,要简单上百倍。 18 | 同学们可以使用我们的集成开发环境软件对项目进行开发。 19 | 20 | ## 使用截图 21 | 22 | 项目创建界面: 23 | 24 | ![e3649359a3b05dedc7c31caac8cbb8db](https://s2.loli.net/2023/09/04/k1PVInyzO5aXTMc.png) 25 | 26 | 项目开发工程页面: 27 | 28 | ![e640e4dcced7d9d5502c2c55c662f3df](https://s2.loli.net/2023/09/04/fEcRsvVOaPGpYrb.png) 29 | 30 | 拿去当课设可以说非常舒服了~ 31 | -------------------------------------------------------------------------------- /files/FiraCode-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itbaima-study/Intellij-IDEA-Extreme/7cbc26ab64025ce17aaa8b479c4d17c7a80c4ca8/files/FiraCode-Medium.ttf -------------------------------------------------------------------------------- /lib/flatlaf-2.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itbaima-study/Intellij-IDEA-Extreme/7cbc26ab64025ce17aaa8b479c4d17c7a80c4ca8/lib/flatlaf-2.6.jar -------------------------------------------------------------------------------- /src/com/intellij/Main.java: -------------------------------------------------------------------------------- 1 | package com.intellij; 2 | 3 | import com.formdev.flatlaf.FlatDarkLaf; 4 | import com.intellij.manage.ProjectManager; 5 | import com.intellij.window.WelcomeWindow; 6 | 7 | import javax.swing.*; 8 | 9 | /** 10 | * 整个项目的主启动类,项目的运行就从这里开始 11 | */ 12 | public class Main { 13 | public static void main(String[] args) throws Exception { 14 | //加载项目 15 | ProjectManager.loadProjects(); 16 | //加载皮肤 17 | UIManager.setLookAndFeel(new FlatDarkLaf()); 18 | //初始化窗口 19 | WelcomeWindow startWindow = new WelcomeWindow(); 20 | startWindow.openWindow(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/com/intellij/entity/ProcessResult.java: -------------------------------------------------------------------------------- 1 | package com.intellij.entity; 2 | 3 | /** 4 | * 进程运行结果实体类 5 | */ 6 | public class ProcessResult { 7 | private final int exitCode; 8 | private final String output; 9 | 10 | public ProcessResult(int exitCode, String output) { 11 | this.exitCode = exitCode; 12 | this.output = output; 13 | } 14 | 15 | public String getOutput() { 16 | return output; 17 | } 18 | 19 | public int getExitCode() { 20 | return exitCode; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/com/intellij/entity/ProjectEntity.java: -------------------------------------------------------------------------------- 1 | package com.intellij.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 项目实体类(项目列表、保存项目用到) 7 | */ 8 | public class ProjectEntity implements Serializable { 9 | private final String name; //项目名称 10 | private final String filepath; //项目位置 11 | 12 | public ProjectEntity(String name, String filepath) { 13 | this.name = name; 14 | this.filepath = filepath; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public String getFilePath() { 22 | return filepath; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/intellij/entity/config/ApplicationConfigure.java: -------------------------------------------------------------------------------- 1 | package com.intellij.entity.config; 2 | 3 | import com.intellij.entity.ProjectEntity; 4 | 5 | import java.io.Serializable; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.function.Predicate; 9 | 10 | /** 11 | * 当前应用的配置实体类 12 | */ 13 | public class ApplicationConfigure implements Serializable { 14 | private final List list = new ArrayList<>(); 15 | 16 | public void addProjectEntity(ProjectEntity entity){ 17 | list.add(entity); 18 | } 19 | 20 | public void removeProjectEntityIf(Predicate predicate){ 21 | list.removeIf(predicate); 22 | } 23 | 24 | public List getList(){ 25 | return list; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/com/intellij/entity/config/CreateProjectConfigure.java: -------------------------------------------------------------------------------- 1 | package com.intellij.entity.config; 2 | 3 | /** 4 | * 创建项目配置实体类 5 | */ 6 | public class CreateProjectConfigure { 7 | 8 | private final boolean hasDefaultCode; 9 | private final String name; 10 | private final String path; 11 | private final String buildSystem; 12 | private final String language; 13 | 14 | public CreateProjectConfigure(boolean hasDefaultCode, String name, String path, String buildSystem, String language) { 15 | this.hasDefaultCode = hasDefaultCode; 16 | this.name = name; 17 | this.path = path; 18 | this.buildSystem = buildSystem; 19 | this.language = language; 20 | } 21 | 22 | public boolean hasDefaultCode() { 23 | return hasDefaultCode; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public String getPath() { 31 | return path; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "CreateProjectConfigure{" + 37 | "hasDefaultCode=" + hasDefaultCode + 38 | ", name='" + name + '\'' + 39 | ", path='" + path + '\'' + 40 | ", buildSystem='" + buildSystem + '\'' + 41 | ", language='" + language + '\'' + 42 | '}'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/com/intellij/entity/config/ProjectConfigure.java: -------------------------------------------------------------------------------- 1 | package com.intellij.entity.config; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 项目配置实体类 7 | */ 8 | public class ProjectConfigure implements Serializable { 9 | private final String mainClass; 10 | private final String javaCommand; 11 | 12 | public ProjectConfigure(String mainClass, String javaCommand) { 13 | this.mainClass = mainClass; 14 | this.javaCommand = javaCommand; 15 | } 16 | 17 | public String getJavaCommand() { 18 | return javaCommand; 19 | } 20 | 21 | public String getMainClass() { 22 | return mainClass; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/intellij/manage/FileManager.java: -------------------------------------------------------------------------------- 1 | package com.intellij.manage; 2 | 3 | import com.intellij.entity.config.CreateProjectConfigure; 4 | import com.intellij.entity.config.ProjectConfigure; 5 | 6 | import java.io.*; 7 | import java.nio.file.Files; 8 | import java.nio.file.Paths; 9 | import java.util.Arrays; 10 | import java.util.LinkedList; 11 | import java.util.Queue; 12 | 13 | public class FileManager { 14 | public static boolean createProject(CreateProjectConfigure configure){ 15 | File dir = new File(configure.getPath() + "/" +configure.getName()); 16 | if(dir.exists() || dir.mkdirs()) { 17 | File src = new File(dir.getAbsolutePath() + "/src"); 18 | createProjectConfigure(configure); 19 | ProjectManager.createProject(configure.getName(), configure.getPath()); 20 | if(!src.mkdir()) return false; 21 | if(configure.hasDefaultCode()) { 22 | File defaultCodeFile = new File(dir.getAbsolutePath() + "/src/Main.java"); 23 | try(FileWriter writer = new FileWriter(defaultCodeFile)) { 24 | writer.write(defaultMainCode()); 25 | return true; 26 | } catch (IOException e) { 27 | e.printStackTrace(); 28 | return false; 29 | } 30 | } 31 | return true; 32 | } else { 33 | return false; 34 | } 35 | } 36 | 37 | private static void createProjectConfigure(CreateProjectConfigure configure){ 38 | ProjectConfigure projectConfigure = configure.hasDefaultCode() ? 39 | new ProjectConfigure("Main", "java") : 40 | new ProjectConfigure("", "java"); 41 | try(ObjectOutputStream writer = new ObjectOutputStream(Files.newOutputStream( 42 | Paths.get(configure.getPath() + "/" + configure.getName() + "/.idea")))) { 43 | writer.writeObject(projectConfigure); 44 | writer.flush(); 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | } 48 | } 49 | 50 | public static void deleteProject(String name, String path) { 51 | Queue deleteQueue = new LinkedList<>(); //bfs算法删除项目目录下全部文件 52 | deleteQueue.add(new File(path)); 53 | while (!deleteQueue.isEmpty()) { 54 | File file = deleteQueue.poll(); 55 | if (file.isDirectory()) { 56 | File[] files = file.listFiles(); 57 | if (files == null || files.length == 0) { 58 | if (!file.delete()) doNothing(); 59 | } else { 60 | deleteQueue.addAll(Arrays.asList(files)); 61 | deleteQueue.add(file); 62 | } 63 | } else { 64 | if (!file.delete()) doNothing(); 65 | } 66 | } 67 | ProjectManager.deleteProject(name); 68 | } 69 | 70 | public static String defaultCode(String className, String packageName){ 71 | return "package " + packageName + ";\n" + 72 | "\n" + 73 | "public class "+className+" {\n" + 74 | "\n" + 75 | "}"; 76 | } 77 | 78 | private static String defaultMainCode(){ 79 | return "public class Main {\n" + 80 | " public static void main(String[] args) {\n" + 81 | " System.out.println(\"Hello World!\");\n" + 82 | " }\n" + 83 | "}"; 84 | } 85 | 86 | private static void doNothing(){} 87 | } 88 | -------------------------------------------------------------------------------- /src/com/intellij/manage/ProcessExecuteEngine.java: -------------------------------------------------------------------------------- 1 | package com.intellij.manage; 2 | 3 | import com.intellij.entity.ProcessResult; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.OutputStream; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | * 这是进程执行引擎,包括使用javac、javap、java命令实现对项目的: 13 | * - 编译源代码操作 14 | * - 反编译.class文件操作 15 | * - 执行程序操作 16 | * 所有跟项目相关的操作都使用此执行引擎完成。 17 | */ 18 | public class ProcessExecuteEngine { 19 | 20 | private enum OS { Windows, Linux, MacOS } 21 | 22 | /** 23 | * 执行命令主体 24 | * @param commands 多个命令 25 | * @return 最后一条命令启动的进程 26 | */ 27 | private static Process runCommand(String... commands){ 28 | try { 29 | Runtime runtime = Runtime.getRuntime(); 30 | OS os = osType(); //不同操作系统执行的命令不同 31 | Process process = null; 32 | for (String command : commands) { 33 | if(os == OS.Linux || os == OS.MacOS) { 34 | process = runtime.exec(new String[]{"/bin/bash", "-c", command}); 35 | }else { 36 | process = runtime.exec(command); 37 | } 38 | } 39 | return process; 40 | } catch (IOException e) { 41 | e.printStackTrace(); 42 | return null; 43 | } 44 | } 45 | 46 | private static OS osType() { 47 | switch (System.getProperty("os.name")) { 48 | case "Mac OS X": 49 | return OS.MacOS; 50 | case "Linux": 51 | return OS.Linux; 52 | case "Windows 11": 53 | case "Windows 10": 54 | case "Windows 7": 55 | case "Windows 8": 56 | case "Windows 8.1": 57 | return OS.Windows; 58 | default: 59 | throw new IllegalStateException("未知的操作系统类型!"); 60 | } 61 | } 62 | 63 | /** 64 | * 编译Java源代码,并将代码生成到out目录下 65 | * @param projectPath 项目根目录 66 | */ 67 | public static ProcessResult buildProject(String projectPath){ 68 | OS os = osType(); 69 | Process process; 70 | if(os == OS.Linux || os == OS.MacOS) { 71 | process = runCommand("javac -s "+projectPath+" -d "+projectPath+"/out $(find '"+projectPath+"/src' -name '*.java')"); 72 | } else { 73 | process = runCommand("cmd /C cd "+projectPath+" & dir *.java/s/b > "+projectPath+"/.list", 74 | "javac -s "+projectPath+" -d "+projectPath+"/out @"+projectPath+"/.list"); 75 | } 76 | if(process == null) return new ProcessResult(-1, "未知错误"); 77 | try { 78 | int exitCode = process.waitFor(); 79 | runCommand("cmd /C cd "+projectPath+" & del .list"); 80 | return exitCode == 0 ? new ProcessResult(0, "") 81 | : new ProcessResult(exitCode, streamToString(process.getErrorStream())); 82 | } catch (Exception e) { 83 | e.printStackTrace(); 84 | return new ProcessResult(-1, "未知错误"); 85 | } 86 | } 87 | 88 | /** 89 | * 运行项目 90 | * @param projectPath 项目根目录 91 | * @param mainClass 主类 92 | */ 93 | public static ProcessResult startProcess(String projectPath, String javaCommand, String mainClass, Consumer redirect){ 94 | try { 95 | currentProcess = runCommand(javaCommand+" -cp " + projectPath + "/out " + mainClass); 96 | if(currentProcess == null) return new ProcessResult(-1, "未知错误"); 97 | InputStreamReader reader = new InputStreamReader(currentProcess.getInputStream()); 98 | char[] chars = new char[1024]; 99 | int len; 100 | while ((len = reader.read(chars)) > 0) 101 | redirect.accept(new String(chars, 0, len)); 102 | int code = currentProcess.waitFor(); 103 | return new ProcessResult(code, streamToString(currentProcess.getErrorStream())); 104 | } catch (Exception e){ 105 | e.printStackTrace(); 106 | } finally { 107 | synchronized (ProcessExecuteEngine.class) { //多线程控制,防止并发修改 108 | currentProcess = null; 109 | } 110 | } 111 | return new ProcessResult(-1, "未知错误"); 112 | } 113 | 114 | private static Process currentProcess = null; 115 | 116 | /** 117 | * 如果当前正在运行进程,停止当前正在运行的进程 118 | */ 119 | public static void stopProcess(){ 120 | synchronized (ProcessExecuteEngine.class) { //多线程控制,防止并发修改 121 | if(currentProcess != null) 122 | currentProcess.destroyForcibly(); 123 | } 124 | } 125 | 126 | /** 127 | * 反编译项目,并返回反编译结果 128 | * @param classFilePath .class文件路径 129 | * @return 反编译结果 130 | */ 131 | public static String decompileCode(String classFilePath){ 132 | Process process = runCommand("javap -c " + classFilePath); 133 | if(process == null) return ""; 134 | return streamToString(process.getInputStream()); 135 | } 136 | 137 | /** 138 | * 使用git命令从远程仓库下载代码 139 | * @param url 远程地址 140 | * @param branch 分支 141 | * @param dir 保存位置 142 | * @return 下载结果 143 | */ 144 | public static ProcessResult fetchFromGit(String url, String branch, String dir){ 145 | Process process = runCommand("git clone "+url+" -b "+branch + " " +dir); 146 | if(process == null) return new ProcessResult(-1, "未知错误"); 147 | try { 148 | int exitCode = process.waitFor(); 149 | return exitCode == 0 ? new ProcessResult(0, "") 150 | : new ProcessResult(exitCode, streamToString(process.getErrorStream())); 151 | } catch (InterruptedException e) { 152 | e.printStackTrace(); 153 | return new ProcessResult(-1, "未知错误"); 154 | } 155 | } 156 | 157 | /** 158 | * 将输入的字符串重定向给当前正在运行的进程 159 | * @param input 输入 160 | */ 161 | public static void redirectToProcess(String input){ 162 | synchronized (ProcessExecuteEngine.class) { //多线程控制,防止并发修改 163 | if(currentProcess != null) { 164 | OutputStream stream = currentProcess.getOutputStream(); 165 | try { 166 | stream.write(input.getBytes()); 167 | stream.flush(); 168 | } catch (IOException e) { 169 | e.printStackTrace(); 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * 快速将流中内容转换为字符串 177 | * @param stream 输入流 178 | * @return 字符串 179 | */ 180 | private static String streamToString(InputStream stream){ 181 | InputStreamReader reader = new InputStreamReader(stream); 182 | StringBuilder builder = new StringBuilder(); 183 | try { 184 | int len; 185 | char[] chars = new char[1024 * 1024]; 186 | while ((len = reader.read(chars)) > 0) 187 | builder.append(chars, 0, len); 188 | }catch (IOException e){ 189 | e.printStackTrace(); 190 | } 191 | return builder.toString(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/com/intellij/manage/ProjectManager.java: -------------------------------------------------------------------------------- 1 | package com.intellij.manage; 2 | 3 | import com.intellij.entity.ProjectEntity; 4 | import com.intellij.entity.config.ApplicationConfigure; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.ObjectInputStream; 9 | import java.io.ObjectOutputStream; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.stream.Stream; 13 | 14 | public class ProjectManager { 15 | private static ProjectManager INSTANCE; 16 | private static ApplicationConfigure configure; 17 | 18 | private ProjectManager(){} 19 | 20 | public static void loadProjects() throws IOException, ClassNotFoundException { 21 | File file = new File("files/config"); 22 | if(!file.exists()) { 23 | if(file.createNewFile()) { 24 | configure = new ApplicationConfigure(); 25 | saveConfigure(); 26 | } else { 27 | throw new RuntimeException("无法创建配置文件!"); 28 | } 29 | }else { 30 | ObjectInputStream stream = new ObjectInputStream(Files.newInputStream(file.toPath())); 31 | configure = (ApplicationConfigure) stream.readObject(); 32 | stream.close(); 33 | } 34 | INSTANCE = new ProjectManager(); 35 | } 36 | 37 | public static void saveConfigure(){ 38 | try (ObjectOutputStream stream = new ObjectOutputStream(Files.newOutputStream(Paths.get("files/config")))){ 39 | stream.writeObject(configure); 40 | stream.flush(); 41 | } catch (IOException e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | public static ProjectManager getManager() { 47 | return INSTANCE; 48 | } 49 | 50 | public Stream getProjectList(){ 51 | return configure.getList().stream(); 52 | } 53 | 54 | public static void deleteProject(String name){ 55 | configure.removeProjectEntityIf(project -> project.getName().equals(name)); 56 | } 57 | 58 | public static void createProject(String name, String filepath){ 59 | configure.addProjectEntity(new ProjectEntity(name, filepath + "/" + name)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/com/intellij/window/AbstractWindow.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window; 2 | 3 | import com.intellij.window.enums.CloseAction; 4 | import com.intellij.window.service.AbstractService; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.event.WindowAdapter; 9 | import java.awt.event.WindowEvent; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.function.Consumer; 13 | 14 | /** 15 | * AbstractWindow 是当前项目中,所有窗口的顶层抽象类,继承自JFrame类。 16 | * 考虑到我们的窗口功能复杂,所以: 17 | * - 窗口内只需要编写组件部分,仅包括UI相关内容。 18 | * - 每个窗口都有一个对应的Service来对各项功能进行具体实现。 19 | * - 与工具类、管理类这些底层操作相关的,只能由Service来交互。 20 | * 因此,我们在编写窗口的时候需要分三层来写,这样逻辑会更加清晰一些,比如: 21 | * - WelcomeWindow <-> WelcomeService <-> Tools、Manager 22 | * - MainWindow <-> MainService <-> Tools、Manager 23 | * 这样我们的代码就不会太凌乱,各位能够快速入手。 24 | */ 25 | public abstract class AbstractWindow extends JFrame { 26 | //窗口的默认关闭行为 27 | private CloseAction action = CloseAction.DISPOSE; 28 | //窗口的业务层实现 29 | protected R service; 30 | //当前窗口中所有的组件表,方便业务层快速获取对应组件 31 | private final Map componentMap = new HashMap<>(); 32 | 33 | /** 34 | * 所有继承自此类实现的窗口,都需要添加以下几个参数,用于快速设定窗口必要属性 35 | * @param title 窗口标题 36 | * @param defaultSize 默认大小 37 | * @param resizeable 是否可以修改大小 38 | */ 39 | protected AbstractWindow(String title, Dimension defaultSize, boolean resizeable, Class clazz){ 40 | //首先设定窗口的标题、默认大小、是否可修改大小等 41 | this.setTitle(title); 42 | Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); //注意,设定大小不能超过屏幕尺寸 43 | this.setSize((int) Math.min(screenSize.getWidth(), defaultSize.getWidth()), 44 | (int) Math.min(screenSize.getHeight(), defaultSize.getHeight())); 45 | this.setResizable(resizeable); 46 | 47 | //接着计算窗口的中心位置,将窗口移动到屏幕中心 48 | Point screenCenter = calculateCenter(screenSize.getWidth(), screenSize.getHeight(), 49 | defaultSize.getWidth(), defaultSize.getHeight()); 50 | this.setLocation(screenCenter); 51 | 52 | //这里我们将窗口设计成点击X号不直接关闭,由子类来决定是直接关闭还是做一些其他的事情 53 | this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 54 | this.addWindowListener(new WindowAdapter() { 55 | @Override 56 | public void windowClosing(WindowEvent e) { 57 | if(onClose()) { //调用抽象方法,是否需要关闭窗口由子类的具体实现进行判断 58 | AbstractWindow.this.closeWindow(); 59 | } 60 | } 61 | }); 62 | 63 | //最后设定窗口的业务层,直接反射创建并完成配置 64 | try { 65 | this.service = clazz.getConstructor().newInstance(); 66 | this.service.setWindow(this, componentMap::get); 67 | } catch (ReflectiveOperationException e) { 68 | e.printStackTrace(); 69 | } 70 | //至此,构造方法中对窗口的基本内容初始化完成 71 | } 72 | 73 | /** 74 | * 传入外部长宽与窗口(内部)长宽,计算中心位置,用于窗口居中位置计算 75 | * @param outerWidth 外部宽度(如屏幕宽度) 76 | * @param outerHeight 外部高度(如屏幕高度) 77 | * @param innerWidth 外部宽度(如屏幕宽度) 78 | * @param innerHeight 外部高度(如屏幕高度) 79 | * @return 居中位置 80 | */ 81 | private Point calculateCenter(double outerWidth, double outerHeight, double innerWidth, double innerHeight){ 82 | int x = (int) ((outerWidth - innerWidth) / 2); 83 | int y = (int) ((outerHeight - innerHeight) / 2); 84 | return new Point(x, y); 85 | } 86 | 87 | /** 88 | * 关闭当前窗口 89 | */ 90 | public final void closeWindow(){ 91 | this.action.doAction(this); 92 | } 93 | 94 | /** 95 | * 打开当前窗口,直接展示当前窗口,通过改变可见性 96 | */ 97 | public final void openWindow(){ 98 | super.setVisible(true); 99 | } 100 | 101 | /** 102 | * 一律只能用我们自己的openWindow()方法展示窗口,原本的可见性设置直接封掉 103 | */ 104 | @Override 105 | public final void setVisible(boolean b) { 106 | throw new UnsupportedOperationException("请使用openWindow()方法展示窗口!"); 107 | } 108 | 109 | /** 110 | * 抽象方法,由子类进行实现,返回true或是false决定是否关闭窗口 111 | * @return 是否关闭窗口 112 | */ 113 | protected abstract boolean onClose(); 114 | 115 | /** 116 | * 窗口初始化方法,也就是窗口内部该有什么组件,该怎么放,所有的组件初始化操作都在这里实现 117 | * 一般放在构造方法中调用,而具体的调用时机由子类决定 118 | */ 119 | protected abstract void initWindowContent(); 120 | 121 | /** 122 | * 设定窗口的关闭动作,默认是调用dispose()方法,也可以设定为System.exit() 123 | */ 124 | protected final void setDefaultCloseAction(CloseAction action){ 125 | if(action == null) 126 | throw new IllegalArgumentException("窗口关闭动作不能为null!"); 127 | this.action = action; 128 | } 129 | 130 | /** 131 | * 根据组件名称获取对应组件,并自动转换为对应类型 132 | * @param componentName 组件名称 133 | * @return 组件 134 | */ 135 | @SuppressWarnings("unchecked") 136 | protected final T getComponent(String componentName){ 137 | return (T) componentMap.get(componentName); 138 | } 139 | 140 | /** 141 | * 原版的组件添加方法太不方便了,要写很多行代码,这里写个更方便的方法。 142 | * 例如,本来应该这样写: 143 | * 144 | * JButton button = new JButton(); 145 | * button.setLabel("我是按钮"); 146 | * button.setSize(100, 200); 147 | * button.setLocation(200, 200); 148 | * this.add(button); 149 | * 150 | * 现在可以写成: 151 | * 152 | * this.addComponent("welcome.button.test", new JButton(), button -> { 153 | * button.setLabel("我是按钮"); 154 | * button.setSize(100, 200); 155 | * button.setLocation(200, 200); 156 | * }); 157 | * 158 | * 集组件配置、添加组件到容器、添加到组件表一步到位。 159 | * @param name 组件的名称 160 | * @param component 待添加的组件 161 | * @param constraints 组件约束 162 | * @param consumer 组件配置在这里写 163 | */ 164 | protected final void addComponent(String name, T component, Object constraints, Consumer consumer){ 165 | if(consumer != null) 166 | consumer.accept(component); 167 | this.componentMap.put(name, component); 168 | this.add(component, constraints); 169 | } 170 | 171 | /** 172 | * 此方法是对指定的容器执行上述操作,用法相同。 173 | * @param target 指定容器 174 | * @param name 组件的名称 175 | * @param component 待添加的组件 176 | * @param consumer 组件配置在这里写 177 | */ 178 | protected void addComponent(Container target, String name, T component, Consumer consumer){ 179 | if(consumer != null) 180 | consumer.accept(component); 181 | this.componentMap.put(name, component); 182 | target.add(component); 183 | } 184 | 185 | /** 186 | * 此方法是对指定的容器执行上述操作,支持组件约束,用法相同。 187 | * @param target 指定容器 188 | * @param name 组件的名称 189 | * @param component 待添加的组件 190 | * @param constraints 组件约束 191 | * @param consumer 组件配置在这里写 192 | */ 193 | protected void addComponent(Container target, String name, T component, Object constraints, Consumer consumer){ 194 | if(consumer != null) 195 | consumer.accept(component); 196 | this.componentMap.put(name, component); 197 | target.add(component, constraints); 198 | } 199 | 200 | /** 201 | * 不添加组件到当前容器,仅仅将组件添加到组件表中进行管理 202 | * @param name 组件名称 203 | * @param component 组件 204 | */ 205 | protected final void mapComponent(String name, T component){ 206 | this.componentMap.put(name, component); 207 | this.add(component); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/com/intellij/window/MainWindow.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window; 2 | 3 | import com.intellij.manage.ProcessExecuteEngine; 4 | import com.intellij.window.enums.CloseAction; 5 | import com.intellij.window.service.MainService; 6 | 7 | import javax.swing.*; 8 | import javax.swing.tree.DefaultMutableTreeNode; 9 | import javax.swing.tree.TreePath; 10 | import java.awt.*; 11 | import java.io.File; 12 | import java.util.LinkedList; 13 | import java.util.Objects; 14 | import java.util.Queue; 15 | 16 | public class MainWindow extends AbstractWindow { 17 | 18 | private final String path; 19 | private final String name; 20 | 21 | private DefaultMutableTreeNode root; 22 | 23 | public MainWindow(String name, String path) { 24 | super("项目:"+name, new Dimension(1000, 600), true, MainService.class); 25 | //设定路径和项目名称,然后开始配置窗口内容 26 | this.path = path; 27 | this.name = name; 28 | //窗口关闭不能直接退出程序,因为要回到欢迎界面 29 | this.setDefaultCloseAction(CloseAction.DISPOSE); 30 | //为业务层设定当前项目的路径 31 | service.setPath(path); 32 | //然后是加载当前项目的配置,项目的配置不同会影响组件的某些显示状态 33 | service.loadProjectConfig(); 34 | //最后再初始化窗口内容 35 | this.initWindowContent(); 36 | } 37 | 38 | @Override 39 | protected void initWindowContent() { 40 | //我们的代码编辑主界面包括最上面的一排工具栏 41 | this.addComponent("main.panel.tools", new JPanel(), BorderLayout.NORTH, this::initControlTools); 42 | 43 | //以及左边的文件树区域和中间的代码编辑区域,还有最下面的控制台区域 44 | this.addComponent("main.panel.content", new JSplitPane(), BorderLayout.CENTER, panel -> { 45 | //这里我们先分出最下方控制台和中心区域两个部分,所以先纵向分割一下 46 | panel.setOrientation(JSplitPane.VERTICAL_SPLIT); 47 | 48 | //首先配置最下方的控制台区域 49 | panel.setBottomComponent(this.createConsole()); 50 | panel.setDividerLocation(380); //下面的分割条默认在 y = 400 位置上 51 | 52 | //这一块是中心区域,中心区域包含左侧文件树和右侧代码编辑界面 53 | JSplitPane centerPanel = new JSplitPane(); 54 | centerPanel.setLeftComponent(this.createLeftPanel()); 55 | centerPanel.setRightComponent(this.createRightPanel()); 56 | centerPanel.setDividerLocation(200); //中间的分割条默认在 x = 200 位置上 57 | panel.setTopComponent(centerPanel); 58 | }); 59 | } 60 | 61 | /** 62 | * 对最上面一排工具栏包括里面的各个按钮进行初始化。 63 | * @param panel 工具栏面板 64 | */ 65 | private void initControlTools(JPanel panel){ 66 | //这里采用流式布局,直接让按钮居右按顺序放置 67 | panel.setPreferredSize(new Dimension(0, 35)); 68 | FlowLayout layout = new FlowLayout(); 69 | layout.setAlignment(FlowLayout.RIGHT); 70 | panel.setLayout(layout); 71 | 72 | //第一个按钮是运行/停止按钮,这个按钮有两种状态,如果主类已经配置,那么就可以运行,否则就不能运行 73 | this.addComponent(panel, "main.button.run", new JButton("运行"), button -> { 74 | button.setPreferredSize(new Dimension(60, 25)); 75 | if(service.getConfigure().getMainClass().isEmpty()) { //判断主类是否已经配置 76 | button.setEnabled(false); 77 | button.setToolTipText("请先完成项目运行配置!"); 78 | } else { 79 | button.setEnabled(true); 80 | button.setToolTipText("点击编译运行项目"); 81 | } 82 | button.addActionListener(e -> service.runButtonAction()); 83 | }); 84 | //第二个按钮是构建按钮,通过它就可以快速对项目进行构建了 85 | this.addComponent(panel, "main.button.build", new JButton("构建"), button -> { 86 | button.setPreferredSize(new Dimension(60, 25)); 87 | button.addActionListener(e -> service.buildButtonAction()); 88 | }); 89 | //第三个是设置按钮,这个按钮也比较简单,直接打开对应的配置对话框就可以了 90 | this.addComponent(panel, "main.button.settings", new JButton("设置"), button -> { 91 | button.setPreferredSize(new Dimension(60, 25)); 92 | button.addActionListener(e -> service.settingButtonAction()); 93 | }); 94 | } 95 | 96 | /** 97 | * 创建左侧文件树板块,用于展示整个项目的文件列表 98 | * @return 文件树板块 99 | */ 100 | private JScrollPane createLeftPanel(){ 101 | //首先配置文件树 102 | root = new DefaultMutableTreeNode(new NodeData(path, name)); 103 | buildTreeNode(root); 104 | JTree fileTree = new JTree(root); 105 | this.mapComponent("main.tree.files", fileTree); 106 | fileTree.addTreeSelectionListener(e -> { 107 | TreePath treePath = e.getPath(); 108 | StringBuilder filePath = new StringBuilder(this.path); 109 | for (int i = 1; i < treePath.getPathCount(); i++) 110 | filePath.append("/").append(treePath.getPathComponent(i)); 111 | this.service.switchEditFile(filePath.toString()); 112 | }); 113 | //接着是右键文件树的弹出菜单,对文件进行各种操作,包括创建新的源文件和删除源文件 114 | JPopupMenu treePopupMenu = new JPopupMenu(); 115 | this.mapComponent("main.popup.tree", treePopupMenu); 116 | this.add(treePopupMenu); 117 | JMenuItem createItem = new JMenuItem("创建源文件"); 118 | createItem.addActionListener(e -> service.createNewFile()); 119 | JMenuItem deleteItem = new JMenuItem("删除"); 120 | deleteItem.addActionListener(e -> service.deleteProjectFile()); 121 | treePopupMenu.add(createItem); 122 | treePopupMenu.add(deleteItem); 123 | fileTree.addMouseListener(service.fileTreeRightClick()); 124 | //文件树构造完成后,直接放进滚动面板返回就行了 125 | return new JScrollPane(fileTree); 126 | } 127 | 128 | /** 129 | * 创建右侧编辑板块,用于对项目代码进行编辑操作 130 | * @return 编辑板块 131 | */ 132 | private JScrollPane createRightPanel(){ 133 | JTextArea editArea = new JTextArea(); 134 | this.mapComponent("main.textarea.edit", editArea); 135 | //快速配置编辑文本域的各项功能 136 | this.service.setupEditArea(); 137 | //编辑界面的字体采用FiraCode,好看不止一点半点 138 | try { 139 | Font font = Font.createFont(Font.TRUETYPE_FONT, new File("files/FiraCode-Medium.ttf")); 140 | editArea.setFont(font.deriveFont(13.0F)); 141 | } catch (Exception e) { 142 | e.printStackTrace(); 143 | } 144 | //默认情况下无法进行编辑,必须选中文件之后才可以 145 | editArea.setEditable(false); 146 | return new JScrollPane(editArea); 147 | } 148 | 149 | /** 150 | * 创建底部控制台板块,用于展示控制台输出信息 151 | * @return 底部板块 152 | */ 153 | private JScrollPane createConsole(){ 154 | JTextArea consoleArea = new JTextArea("控制台中尚未启动任何进程"); 155 | this.mapComponent("main.textarea.console", consoleArea); 156 | consoleArea.setEditable(false); 157 | consoleArea.addKeyListener(service.inputRedirect()); 158 | return new JScrollPane(consoleArea); 159 | } 160 | 161 | /** 162 | * 快速刷新文件树,构建JTree结点并重新绘制 163 | */ 164 | public void refreshFileTree(){ 165 | buildTreeNode(root); 166 | SwingUtilities.updateComponentTreeUI(this); 167 | } 168 | 169 | /** 170 | * 构建JTree结点,采用BFS算法完成 171 | */ 172 | private void buildTreeNode(DefaultMutableTreeNode root){ 173 | root.removeAllChildren(); //先清理掉 174 | //BFS算法列出所有结点并构建成树 175 | Queue queue = new LinkedList<>(); 176 | queue.add(root); 177 | while (!queue.isEmpty()) { 178 | DefaultMutableTreeNode node = queue.poll(); 179 | NodeData data = (NodeData) node.getUserObject(); 180 | for (File file : Objects.requireNonNull(data.getFile().listFiles())) { 181 | if (file.getName().charAt(0) == '.') continue; //隐藏文件不需要显示出来 182 | DefaultMutableTreeNode child = new DefaultMutableTreeNode(new NodeData(file.getAbsolutePath(), file.getName())); 183 | node.add(child); 184 | if(file.isDirectory()) queue.offer(child); 185 | } 186 | } 187 | } 188 | 189 | @Override 190 | protected boolean onClose() { 191 | //关闭之前如果还有运行的项目没有结束,一定要结束掉 192 | ProcessExecuteEngine.stopProcess(); 193 | //然后回到初始界面 194 | WelcomeWindow window = new WelcomeWindow(); 195 | window.openWindow(); 196 | return true; 197 | } 198 | 199 | /** 200 | * NodeData是JTree的专用结点信息存储介质,包括文件相关信息。 201 | */ 202 | private static class NodeData{ 203 | private final String filepath; 204 | private final String nodeName; 205 | 206 | public NodeData(String filepath, String nodeName) { 207 | this.filepath = filepath; 208 | this.nodeName = nodeName; 209 | } 210 | 211 | public File getFile() { 212 | return new File(filepath); 213 | } 214 | 215 | @Override 216 | public String toString() { 217 | return nodeName; 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/com/intellij/window/WelcomeWindow.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window; 2 | 3 | import com.intellij.manage.ProjectManager; 4 | import com.intellij.window.component.JLinePanel; 5 | import com.intellij.window.component.JListItem; 6 | import com.intellij.window.enums.CloseAction; 7 | import com.intellij.window.layout.ListLayout; 8 | import com.intellij.window.layout.MarginLayout; 9 | import com.intellij.window.service.WelcomeService; 10 | 11 | import javax.swing.*; 12 | import java.awt.*; 13 | 14 | /** 15 | * WelcomeWindow 是初始欢迎窗口,也是程序启动之后的窗口。 16 | * 本窗口包含的功能有: 17 | * 1. 创建新的项目(包括文件夹创建、项目初始文件创建) 18 | * 2. 打开系统已有项目。 19 | * 3. 从版本控制系统获取项目。 20 | * 4. 管理项目列表(展示列表、项目右键可删除) 21 | * 本窗口的特性: 22 | * 1. 关闭窗口等于直接结束程序。 23 | * 2. 在关闭窗口时需要保存当前运行时的配置。 24 | * 3. 关闭项目后会回到此窗口。 25 | * 整个窗口布局为: 26 | * - 最顶层为三个按钮(新建项目、打开项目、从VCS获取) 27 | * - 按钮下方有一个搜索框,可以快速搜索项目。 28 | * - 最下方的区域全部作为项目列表,展示所有项目。 29 | */ 30 | public class WelcomeWindow extends AbstractWindow { 31 | public WelcomeWindow() { 32 | super("欢迎访问 Intellij IDEA Extreme", new Dimension(320, 500), false, WelcomeService.class); 33 | //设定窗口关闭行为为直接退出程序 34 | this.setDefaultCloseAction(CloseAction.EXIT); 35 | //初始化窗口组件 36 | this.initWindowContent(); 37 | } 38 | 39 | @Override 40 | protected boolean onClose() { 41 | //退出程序时,配置文件记得保存一下 42 | ProjectManager.saveConfigure(); 43 | return true; //欢迎窗口点击直接关闭就可以了 44 | } 45 | 46 | @Override 47 | protected void initWindowContent() { 48 | //上半部分就是工具栏,包括创建项目、打开项目之类的按钮,按钮下方就是一个搜索框,可以搜索当前的项目列表中的项目 49 | //下半部分就是列表,所以说这里我们直接分为上下两个部分来写 50 | //要写这样的界面的话,布局采用用BorderLayout就很合适,默认就是,所以说无需修改布局 51 | 52 | //首先是上半部分,也就是按钮和搜索框的区域: 53 | this.addComponent("welcome.panel.top", new JPanel(), BorderLayout.NORTH, panel -> { 54 | //顶部区域需要一个最外层的面板来装载,依然采用边界布局,因为还得继续分两半 55 | panel.setPreferredSize(new Dimension(0, 65)); 56 | panel.setLayout(new BorderLayout()); 57 | //上半部分是按钮区域,这里使用带分割线的面板组件 58 | this.addComponent(panel, "welcome.panel.top.top", 59 | new JLinePanel(true), BorderLayout.NORTH, this::initContentTop); 60 | //下半部分是搜索框,这个就简单,价格搜索框就完事了 61 | this.addComponent(panel, "welcome.panel.top.bottom", 62 | new JPanel(), BorderLayout.CENTER, this::initContentBottom); 63 | }); 64 | 65 | //接着是下半部分,直接一个方法就封装好了,因为可能需要反复使用 66 | this.initProjectList(); 67 | } 68 | 69 | /** 70 | * 对上半部分面板的上半部分进行配置 71 | * @param top 下半部分面板 72 | */ 73 | private void initContentTop(JLinePanel top){ 74 | top.setPreferredSize(new Dimension(100, 35)); 75 | FlowLayout flowLayout = new FlowLayout(); 76 | top.setLayout(flowLayout); 77 | //创建项目按钮 78 | this.addComponent(top, "welcome.button.create", new JButton("创建项目"), button -> { 79 | button.setPreferredSize(new Dimension(90, 25)); 80 | button.addActionListener(e -> service.createNewProject()); 81 | }); 82 | //打开项目按钮 83 | this.addComponent(top, "welcome.button.open", new JButton("打开项目"), button -> { 84 | button.setPreferredSize(new Dimension(90, 25)); 85 | button.addActionListener(e -> service.openProject()); 86 | }); 87 | //从VCS获取项目 88 | this.addComponent(top, "welcome.button.vcs", new JButton("从VCS获取"), button -> { 89 | button.setPreferredSize(new Dimension(100, 25)); 90 | button.addActionListener(e -> service.openVcsProject()); 91 | }); 92 | } 93 | 94 | /** 95 | * 对上半部分面板的下半部分进行配置 96 | * @param bottom 下半部分面板 97 | */ 98 | private void initContentBottom(JPanel bottom){ 99 | bottom.setLayout(new MarginLayout(5)); 100 | this.addComponent(bottom, "welcome.field.search", new JTextField(), 101 | field -> field.addKeyListener(service.refreshListAdapter())); 102 | } 103 | 104 | /** 105 | * 对下半部分的项目列表进行配置,这里采用的机制是将整个下半部分的组件全部替换成一个全新的 106 | * 滚动列表,这样可以避免很多奇奇怪怪的问题,虽然有点浪费性能 107 | */ 108 | public void initProjectList(){ 109 | JTextField searchField = this.getComponent("welcome.field.search"); 110 | String search = searchField.getText(); 111 | //装载所有项目Item的面板,这里使用ListLayout来展示为列表形式 112 | JPanel listPanel = new JPanel(); 113 | listPanel.setLayout(new ListLayout()); 114 | //提供项目管理器获取所有已经读取的项目列表,开始装载 115 | ProjectManager.getManager().getProjectList() 116 | .filter(proj -> proj.getName().contains(search)) 117 | .forEach(proj -> this.addComponent(listPanel, //JListItem使我们自己定义列表Item组件,很好看的 118 | "welcome.item."+proj.getName(), new JListItem(proj.getName(), proj.getFilePath()), item -> { 119 | item.setClickAction(() -> service.enterProject(proj)); 120 | item.configurePopupMenu(menu -> { 121 | JMenuItem deleteProject = new JMenuItem("删除当前项目"); 122 | deleteProject.addActionListener(e -> service.deleteProject(proj)); 123 | menu.add(deleteProject); 124 | }); 125 | })); 126 | //如果原本就有此组件,那么就移除 127 | JScrollPane projectList = this.getComponent("welcome.scroll.project"); 128 | if(projectList != null) this.remove(projectList); 129 | //替换成一个全新的项目列表组件 130 | projectList = new JScrollPane(listPanel); 131 | this.addComponent("welcome.scroll.project", projectList, BorderLayout.CENTER, null); 132 | //动态更新组件之后,一定要调用updateComponentTreeUI方法重新绘制整个界面 133 | SwingUtilities.updateComponentTreeUI(this); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/com/intellij/window/component/JLinePanel.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.component; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * 面板有些时候画一个分界线出来会更好看,所以说这里扩展一下JPanel 8 | * LinePanel会在边上添加边界线 9 | */ 10 | public class JLinePanel extends JPanel { 11 | private final boolean top; 12 | private final boolean bottom; 13 | private final boolean left; 14 | private final boolean right; 15 | 16 | /** 17 | * 这里因为只需要底部添加分界线,所以说就不写其他的构造方法了 18 | * @param bottom 是否添加底部分界线 19 | */ 20 | public JLinePanel(boolean bottom) { 21 | this.bottom = bottom; 22 | this.top = this.left = this.right = false; 23 | } 24 | 25 | @Override 26 | protected void paintComponent(Graphics g) { 27 | super.paintComponent(g); //原有的绘制不变,下面是额外的边界线绘制操作 28 | g.setColor(Color.GRAY); 29 | if (top) g.drawLine(0, 0, this.getWidth(), 0); 30 | if (bottom) g.drawLine(0, this.getHeight(), this.getWidth(), this.getHeight()); 31 | if (left) g.drawLine(0, 0, 0, this.getHeight()); 32 | if (right) g.drawLine(this.getWidth(), 0, this.getWidth(), this.getHeight()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/com/intellij/window/component/JListItem.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.component; 2 | 3 | import javax.swing.*; 4 | import javax.swing.plaf.ComponentUI; 5 | import java.awt.*; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | import java.util.function.Consumer; 9 | 10 | public class JListItem extends JComponent { 11 | private final String name; 12 | private final String filepath; 13 | private boolean mouseOver = false; 14 | private Runnable clickAction = () -> {}; 15 | 16 | private final JPopupMenu popupMenu = new JPopupMenu(); 17 | 18 | public JListItem(String name, String filepath) { 19 | this.name = name; 20 | this.filepath = filepath; 21 | this.setUI(new JListItemUI()); 22 | this.setPreferredSize(new Dimension(0, 50)); 23 | this.add(popupMenu); 24 | this.addMouseListener(new MouseAdapter() { 25 | @Override 26 | public void mouseEntered(MouseEvent e) { 27 | mouseOver = true; 28 | JListItem.this.repaint(); 29 | } 30 | 31 | @Override 32 | public void mouseExited(MouseEvent e) { 33 | mouseOver = false; 34 | JListItem.this.repaint(); 35 | } 36 | 37 | @Override 38 | public void mouseClicked(MouseEvent e) { 39 | if(e.getButton() == MouseEvent.BUTTON1) 40 | clickAction.run(); 41 | else if (e.getButton() == MouseEvent.BUTTON3) 42 | popupMenu.show(JListItem.this, e.getX(), e.getY()); 43 | } 44 | }); 45 | } 46 | 47 | public void setClickAction(Runnable clickAction){ 48 | this.clickAction = clickAction; 49 | } 50 | 51 | public void configurePopupMenu(Consumer consumer){ 52 | consumer.accept(this.popupMenu); 53 | } 54 | 55 | private class JListItemUI extends ComponentUI { 56 | @Override 57 | public void paint(Graphics g, JComponent c) { 58 | if(mouseOver) { 59 | g.setColor(new Color(255, 255, 255, 128)); 60 | g.fillRoundRect(5, 5, c.getWidth() - 10, c.getHeight() - 5, 10, 10); 61 | } 62 | g.setColor(hashColor(name.hashCode())); 63 | g.fillRoundRect(10, 10, 35, 35, 10, 10); 64 | g.setColor(Color.WHITE); 65 | g.drawString(filepath, 50, 42); 66 | Font font = g.getFont(); 67 | g.setFont(new Font(font.getName(), Font.PLAIN, 15)); 68 | g.drawString(name, 50, 22); 69 | g.setColor(Color.WHITE); 70 | g.setFont(new Font(font.getName(), Font.PLAIN, 20)); 71 | g.drawString(name.substring(0, 1), 20, 35); 72 | } 73 | } 74 | 75 | /** 76 | * 根据哈希值随机选择一个颜色 77 | * @return 颜色 78 | */ 79 | private Color hashColor(int hashCode){ 80 | Color[] colors = {Color.PINK, Color.ORANGE, new Color(0, 201, 87), new Color(160, 102, 211), 81 | new Color(227, 207, 87), new Color(221, 160, 221), new Color(51, 161, 210), 82 | new Color(46, 139, 87), new Color(252, 230, 201), new Color(128, 138, 135)}; 83 | int index = Math.abs(hashCode) % colors.length; 84 | return colors[index]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/com/intellij/window/dialog/AbstractDialog.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.dialog; 2 | 3 | import com.intellij.window.AbstractWindow; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.WindowAdapter; 8 | import java.awt.event.WindowEvent; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | * 当前项目中所有对话框的顶层抽象类 13 | */ 14 | public abstract class AbstractDialog extends JDialog { 15 | 16 | public AbstractDialog(AbstractWindow parent, String title, Dimension size){ 17 | super(parent, title, true); //对话框默认情况下都采用这种模式 18 | this.setSize(size); //对话框的大小默认情况下无法进行修改 19 | this.setResizable(false); 20 | this.setLocation(this.calculateCenter()); //对话框也要相对于当前窗口进行居中 21 | this.setLayout(null); //因为对话框默认大小不可变,所以对话框默认布局为空,方便布置组件 22 | this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 23 | this.addWindowListener(new WindowAdapter() { 24 | @Override 25 | public void windowClosing(WindowEvent e) { 26 | AbstractDialog.this.closeDialog(); 27 | } 28 | }); 29 | initDialogContent(); //初始化对话框组件 30 | } 31 | 32 | public AbstractDialog(AbstractDialog parent, String title, Dimension size){ 33 | super(parent, title, true); //对话框默认情况下都采用这种模式 34 | this.setSize(size); //对话框的大小默认情况下无法进行修改 35 | this.setResizable(false); 36 | this.setLocation(this.calculateCenter()); //对话框也要相对于当前窗口进行居中 37 | this.setLayout(null); //因为对话框默认大小不可变,所以对话框默认布局为空,方便布置组件 38 | this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 39 | this.addWindowListener(new WindowAdapter() { 40 | @Override 41 | public void windowClosing(WindowEvent e) { 42 | AbstractDialog.this.closeDialog(); 43 | } 44 | }); 45 | initDialogContent(); //初始化对话框组件 46 | } 47 | 48 | /** 49 | * 整个对话框的初始化方法,由子类实现,默认在当前类的构造方法中完成调用 50 | */ 51 | protected abstract void initDialogContent(); 52 | 53 | public void closeDialog() { 54 | this.dispose(); 55 | } 56 | 57 | public void openDialog() { 58 | super.setVisible(true); 59 | } 60 | 61 | /** 62 | * 一律只能用我们自己的openDialog()方法展示对话框,原本的可见性设置直接封掉 63 | */ 64 | @Override 65 | public void setVisible(boolean b) { 66 | throw new UnsupportedOperationException("请使用openDialog()方法展示对话框!"); 67 | } 68 | 69 | /** 70 | * 计算当前对话框的居中位置,对话框的居中是相对于窗口的,所以说不能向像窗口那样计算 71 | * 我们要拿到窗口的位置,然后计算窗口的中心位置再减去对话框的尺寸,就是居中位置了 72 | * @return 对话框居中位置 73 | */ 74 | private Point calculateCenter(){ 75 | Point windowPoint = this.getParent().getLocation(); 76 | Dimension windowSize = this.getParent().getSize(); 77 | int x = (int) (windowPoint.getX() + windowSize.getWidth() / 2 - this.getWidth() / 2); 78 | int y = (int) (windowPoint.getY() + windowSize.getHeight() / 2 - this.getHeight() / 2); 79 | return new Point(x, y); 80 | } 81 | 82 | /** 83 | * 原版的组件添加方法太不方便了,要写很多行代码,这里调整一下 84 | * @param component 待添加的组件 85 | * @param consumer 组件配置在这里写 86 | */ 87 | protected void addComponent(T component, Consumer consumer){ 88 | if(consumer != null) 89 | consumer.accept(component); 90 | this.add(component); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/com/intellij/window/dialog/CompileErrorDialog.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.dialog; 2 | 3 | import com.intellij.window.AbstractWindow; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * 编译失败对话框 10 | */ 11 | public class CompileErrorDialog extends AbstractDialog { 12 | public CompileErrorDialog(AbstractWindow parent, String text) { 13 | super(parent, "编译失败", new Dimension(600, 300)); 14 | this.setLayout(new BorderLayout()); 15 | JTextArea area = new JTextArea(text); 16 | this.addComponent(new JScrollPane(area), pane -> area.setEditable(false)); 17 | 18 | } 19 | 20 | @Override 21 | protected void initDialogContent() {} 22 | } 23 | -------------------------------------------------------------------------------- /src/com/intellij/window/dialog/CreateProjectDialog.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.dialog; 2 | 3 | import com.intellij.entity.config.CreateProjectConfigure; 4 | import com.intellij.manage.FileManager; 5 | import com.intellij.window.MainWindow; 6 | import com.intellij.window.WelcomeWindow; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.event.KeyAdapter; 11 | import java.awt.event.KeyEvent; 12 | import java.io.File; 13 | 14 | /** 15 | * 项目创建对话框 16 | */ 17 | public class CreateProjectDialog extends AbstractDialog { 18 | private JTextField nameField; //项目名称输入框 19 | private JTextField pathField; //项目路径输入框 20 | private JLabel finalPath; //最终保存位置展示标签 21 | private JCheckBox defaultCode; //是否需要生成默认代码 22 | private JButton createButton; //创建项目按钮 23 | 24 | private final WelcomeWindow parentWindow; //这里暂时存一下父窗口,后面方便一起关掉 25 | 26 | public CreateProjectDialog(WelcomeWindow parent){ 27 | super(parent, "创建新的项目", new Dimension(500, 300)); 28 | this.parentWindow = parent; 29 | } 30 | 31 | @Override 32 | protected void initDialogContent() { 33 | //首先添加最左侧的标签 34 | this.addComponent(new JLabel("项目名称:"), label -> label.setBounds(20, 20, 100, 20)); 35 | this.addComponent(new JLabel("项目路径:"), label -> label.setBounds(20, 60, 100, 20)); 36 | this.addComponent(new JLabel("项目语言:"), label -> label.setBounds(20, 120, 100, 20)); 37 | this.addComponent(new JLabel("构建系统:"), label -> label.setBounds(20, 160, 100, 20)); 38 | 39 | //然后是两个文本框,每个文本框都要添加监听器,当输入时,会实时更新最终路径展示标签 40 | this.addComponent((nameField = new JTextField()), field -> { 41 | field.setBounds(100, 20, 380, 25); 42 | field.addKeyListener(new KeyAdapter() { 43 | @Override 44 | public void keyReleased(KeyEvent e) { 45 | onKeyPress(); 46 | } 47 | }); 48 | }); 49 | //路径选择文本框,此文本框还有一个文件选择器打开按钮和最终路径展示的标签 50 | this.addComponent((pathField = new JTextField()), field -> { 51 | field.setBounds(100, 60, 330, 25); 52 | field.addKeyListener(new KeyAdapter() { 53 | @Override 54 | public void keyReleased(KeyEvent e) { 55 | onKeyPress(); 56 | } 57 | }); 58 | }); 59 | this.addComponent((finalPath = new JLabel("请填写项目名称和项目保存路径!")), 60 | label -> label.setBounds(100, 85, 380, 20)); 61 | this.addComponent(new JButton("..."), button -> { 62 | button.setBounds(435, 60, 45, 25); 63 | button.addActionListener(e -> selectDirectory()); 64 | }); 65 | 66 | //然后是下面的两个选择框(目前只实现了一种,所以说就不扩展了) 67 | this.addComponent(new JComboBox(), box -> { 68 | box.setBounds(100, 120, 200, 25); 69 | box.addItem("Java"); 70 | }); 71 | this.addComponent(new JComboBox(), box -> { 72 | box.setBounds(100, 160, 200, 25); 73 | box.addItem("Intellij"); 74 | }); 75 | 76 | //最后是是否生成默认代码的勾选框和创建按钮 77 | this.addComponent((defaultCode = new JCheckBox("是否生成默认代码")), 78 | box -> box.setBounds(100, 190, 200, 25)); 79 | this.addComponent((createButton = new JButton("创建项目")), button -> { 80 | button.setBounds(390, 240, 100, 25); 81 | button.setEnabled(false); 82 | button.setToolTipText("请先填写上述配置信息!"); 83 | button.addActionListener(e -> { 84 | boolean hasDefaultCode = defaultCode.isSelected(); 85 | String name = nameField.getText(); 86 | String path = pathField.getText(); 87 | String buildSystem = "Intellij"; 88 | String language = "Java"; 89 | if(!FileManager.createProject(new CreateProjectConfigure(hasDefaultCode, name, path, buildSystem, language))) { 90 | JOptionPane.showMessageDialog(this, "未知错误,创建项目失败!"); 91 | return; 92 | } 93 | //项目创建成功,关闭所有窗口,打开项目编辑窗口 94 | this.closeDialog(); 95 | this.parentWindow.dispose(); //这里别用closeWindow,因为默认是退出程序 96 | //打开项目编辑窗口 97 | MainWindow window = new MainWindow(name, path + "/" + name); 98 | window.openWindow(); 99 | }); 100 | }); 101 | } 102 | 103 | private void onKeyPress(){ 104 | if(nameField.getText() == null || pathField.getText() == null) return; 105 | if(!pathField.getText().isEmpty() && !nameField.getText().isEmpty()) { 106 | finalPath.setText("保存位置:"+ pathField.getText() + "/" + nameField.getText()); 107 | createButton.setEnabled(true); 108 | createButton.setToolTipText("点击创建项目"); 109 | } else { 110 | createButton.setEnabled(false); 111 | createButton.setToolTipText("请先填写上述配置信息!"); 112 | } 113 | } 114 | 115 | private void selectDirectory(){ 116 | DirectoryChooserDialog directoryChooserDialog = new DirectoryChooserDialog(CreateProjectDialog.this); 117 | directoryChooserDialog.openDialog(); 118 | File selectedFile = directoryChooserDialog.getSelectedFile(); 119 | if(selectedFile != null) { 120 | pathField.setText(selectedFile.getAbsolutePath()); 121 | if(nameField.getText() != null && !nameField.getText().isEmpty()) { 122 | finalPath.setText("保存位置:"+ pathField.getText() + "/" + nameField.getText()); 123 | createButton.setEnabled(true); 124 | createButton.setToolTipText("点击创建项目"); 125 | } else { 126 | createButton.setEnabled(false); 127 | createButton.setToolTipText("请先填写上述配置信息!"); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/com/intellij/window/dialog/DirectoryChooserDialog.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.dialog; 2 | 3 | import com.intellij.window.AbstractWindow; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.io.File; 8 | 9 | /** 10 | * 目录选择对话框 11 | */ 12 | public class DirectoryChooserDialog extends AbstractDialog { 13 | 14 | private JFileChooser fileChooser; 15 | public DirectoryChooserDialog(AbstractDialog parent) { 16 | super(parent, "请选择一个目录", new Dimension(600, 400)); 17 | } 18 | 19 | public DirectoryChooserDialog(AbstractWindow parent) { 20 | super(parent, "请选择一个目录", new Dimension(600, 400)); 21 | } 22 | 23 | @Override 24 | protected void initDialogContent() { 25 | this.setLayout(new BorderLayout()); 26 | this.setResizable(true); 27 | fileChooser = new JFileChooser(); 28 | this.addComponent(fileChooser, chooser -> { 29 | //设定文件选择器只能选择目录 30 | chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 31 | //添加监听器,当选择完成后,就关闭当前窗口 32 | chooser.addActionListener(e -> DirectoryChooserDialog.this.closeDialog()); 33 | }); 34 | } 35 | 36 | public File getSelectedFile(){ 37 | return fileChooser.getSelectedFile(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/com/intellij/window/dialog/GitProjectDialog.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.dialog; 2 | 3 | import com.intellij.entity.ProcessResult; 4 | import com.intellij.manage.ProcessExecuteEngine; 5 | import com.intellij.manage.ProjectManager; 6 | import com.intellij.window.MainWindow; 7 | import com.intellij.window.WelcomeWindow; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | import java.awt.event.KeyAdapter; 12 | import java.awt.event.KeyEvent; 13 | import java.io.File; 14 | 15 | /** 16 | * Git项目拉取对话框 17 | */ 18 | public class GitProjectDialog extends AbstractDialog{ 19 | private JTextField location; 20 | private JTextField branch; 21 | private JTextField dir; 22 | private JButton startFetch; 23 | private final WelcomeWindow parent; 24 | 25 | public GitProjectDialog(WelcomeWindow parent) { 26 | super(parent, "从Git获取项目", new Dimension(400, 250)); 27 | this.parent = parent; 28 | } 29 | 30 | @Override 31 | protected void initDialogContent() { 32 | //首先是所有的名称 33 | this.addComponent(new JLabel("远程地址:"), label -> label.setBounds(20, 20, 100, 20)); 34 | this.addComponent(new JLabel("远程分支:"), label -> label.setBounds(20, 80, 100, 20)); 35 | this.addComponent(new JLabel("项目路径:"), label -> label.setBounds(20, 125, 100, 20)); 36 | //接着是三个配置框 37 | this.addComponent((location = new JTextField()), field -> { 38 | field.setBounds(100, 20, 280, 20); 39 | field.addKeyListener(this.inputObserver()); 40 | }); 41 | this.addComponent((branch = new JTextField()), field -> { 42 | field.setBounds(100, 80, 280, 20); 43 | field.addKeyListener(this.inputObserver()); 44 | }); 45 | this.addComponent((dir = new JTextField()), field -> { 46 | field.setBounds(100, 125, 250, 20); 47 | field.addKeyListener(this.inputObserver()); 48 | }); 49 | //然后是对应的描述 50 | this.addComponent(new JLabel("Git远程仓库地址,注意,此功能需要您的电脑安"), 51 | label -> label.setBounds(100, 40, 300, 20)); 52 | this.addComponent(new JLabel("装git命令行工具并且配置SSH之后才能使用!"), 53 | label -> label.setBounds(100, 55, 300, 20)); 54 | this.addComponent(new JLabel("对应的远程仓库分支,如:main"), 55 | label -> label.setBounds(100, 100, 300, 20)); 56 | this.addComponent(new JLabel("项目目录必须是一个已存在的空目录"), 57 | label -> label.setBounds(100, 145, 300, 20)); 58 | //最后是按钮 59 | this.addComponent(new JButton("..."), button -> { 60 | button.setBounds(355, 125, 30, 20); 61 | button.addActionListener(e -> selectDirectory()); 62 | }); 63 | this.addComponent((startFetch = new JButton("开始获取")), button -> { 64 | button.setBounds(160, 180, 100, 25); 65 | button.setEnabled(false); 66 | button.addActionListener(e -> { 67 | startFetch.setEnabled(false); 68 | ProcessResult result = ProcessExecuteEngine.fetchFromGit(location.getText(), branch.getText(), dir.getText()); 69 | if (result.getExitCode() == 0) { 70 | this.closeDialog(); 71 | this.parent.dispose(); 72 | String[] split = dir.getText().split("/"); 73 | String name = split[split.length - 1]; 74 | ProjectManager.createProject(name, dir.getText().substring(0, dir.getText().length() - name.length())); 75 | MainWindow window = new MainWindow(name, dir.getText()); 76 | window.openWindow(); 77 | } else { 78 | JOptionPane.showMessageDialog(this, result.getOutput(), "错误", JOptionPane.ERROR_MESSAGE); 79 | startFetch.setEnabled(true); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | private void selectDirectory(){ 86 | DirectoryChooserDialog directoryChooserDialog = new DirectoryChooserDialog(this); 87 | directoryChooserDialog.openDialog(); 88 | File selectedFile = directoryChooserDialog.getSelectedFile(); 89 | if(selectedFile != null) { 90 | dir.setText(selectedFile.getAbsolutePath()); 91 | startFetch.setEnabled(canStart()); 92 | } 93 | } 94 | 95 | private KeyAdapter inputObserver(){ 96 | return new KeyAdapter() { 97 | @Override 98 | public void keyReleased(KeyEvent e) { 99 | startFetch.setEnabled(canStart()); 100 | } 101 | }; 102 | } 103 | 104 | private boolean canStart(){ 105 | return !dir.getText().isEmpty() && !branch.getText().isEmpty() && !location.getText().isEmpty(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/com/intellij/window/dialog/ProjectConfigDialog.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.dialog; 2 | 3 | import com.intellij.entity.config.ProjectConfigure; 4 | import com.intellij.window.MainWindow; 5 | import com.intellij.window.service.MainService; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | 10 | /** 11 | * 项目配置对话框 12 | */ 13 | public class ProjectConfigDialog extends AbstractDialog{ 14 | 15 | private final ProjectConfigure configure; 16 | private final MainService service; 17 | private JTextField mainClass; 18 | private JTextField javaCommand; 19 | public ProjectConfigDialog(MainWindow parent, MainService service, ProjectConfigure configure) { 20 | super(parent, "项目配置", new Dimension(400, 220)); 21 | this.configure = configure; 22 | this.service = service; 23 | this.initComponentContent(); 24 | } 25 | 26 | @Override 27 | protected void initDialogContent() { 28 | //首先是所有的名称 29 | this.addComponent(new JLabel("主类:"), label -> label.setBounds(20, 20, 100, 20)); 30 | this.addComponent(new JLabel("Java位置:"), label -> label.setBounds(20, 80, 100, 20)); 31 | //接着是两个配置框 32 | this.addComponent((mainClass = new JTextField()), field -> field.setBounds(100, 20, 280, 20)); 33 | this.addComponent((javaCommand = new JTextField()), field -> field.setBounds(100, 80, 280, 20)); 34 | //然后是对应的描述 35 | this.addComponent(new JLabel("主类请使用包名.类名,如com.test.Main"), 36 | label -> label.setBounds(100, 45, 300, 20)); 37 | this.addComponent(new JLabel("此选项用于指定java可执行文件位置,一般情况下"), 38 | label -> label.setBounds(100, 105, 300, 20)); 39 | this.addComponent(new JLabel("使用系统默认位置,如:/usr/bin/java"), 40 | label -> label.setBounds(100, 120, 300, 20)); 41 | //最后是确认按钮 42 | this.addComponent(new JButton("确定"), button -> { 43 | button.setBounds(160, 155, 80, 25); 44 | button.addActionListener(e -> { 45 | this.updateConfigure(); 46 | this.closeDialog(); 47 | }); 48 | }); 49 | } 50 | 51 | private void initComponentContent(){ 52 | mainClass.setText(configure.getMainClass()); 53 | javaCommand.setText(configure.getJavaCommand()); 54 | } 55 | 56 | private void updateConfigure(){ 57 | ProjectConfigure config = new ProjectConfigure(mainClass.getText(), javaCommand.getText()); 58 | service.updateAndSaveConfigure(config); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/com/intellij/window/enums/CloseAction.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.enums; 2 | 3 | import com.intellij.window.AbstractWindow; 4 | 5 | import java.awt.*; 6 | import java.util.function.Consumer; 7 | 8 | /** 9 | * 预定义的窗口关闭行为 10 | */ 11 | public enum CloseAction { 12 | DISPOSE(Window::dispose), //调用窗口的dispose方法 13 | EXIT(window -> System.exit(0)); //调用窗口的退出方法 14 | 15 | private final Consumer> action; 16 | CloseAction(Consumer> action){ 17 | this.action = action; 18 | } 19 | 20 | public void doAction(AbstractWindow window){ 21 | action.accept(window); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/com/intellij/window/layout/ListLayout.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.layout; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * 内部的组件会以列表的形式从上往下排列 7 | * 注意,不能配合滚动条使用,如果需要,必须禁止横向大小修改,否则会出BUG 8 | */ 9 | public class ListLayout implements LayoutManager { 10 | 11 | @Override 12 | public void addLayoutComponent(String name, Component comp) {} 13 | 14 | @Override 15 | public void removeLayoutComponent(Component comp) {} 16 | 17 | @Override 18 | public Dimension preferredLayoutSize(Container parent) { 19 | return calculateSize(parent); 20 | } 21 | 22 | @Override 23 | public Dimension minimumLayoutSize(Container parent) { 24 | return calculateSize(parent); 25 | } 26 | 27 | @Override 28 | public void layoutContainer(Container parent) { 29 | int top = 0; 30 | for (Component component : parent.getComponents()) { 31 | int height = (int) component.getPreferredSize().getHeight(); 32 | component.setSize(parent.getWidth(), height); 33 | component.setLocation(0, top); 34 | top += height; 35 | } 36 | } 37 | 38 | /** 39 | * 计算内容的大小 40 | * @param parent 容器 41 | * @return 大小 42 | */ 43 | private Dimension calculateSize(Container parent){ 44 | int height = 0; 45 | for (Component component : parent.getComponents()) 46 | height += component.getPreferredSize().getHeight(); 47 | return new Dimension(parent.getWidth(), height); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/com/intellij/window/layout/MarginLayout.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.layout; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * 自定义的外边距布局 7 | * 唯一作用:为组件设定一个外边距然后再展示出来 8 | * 此布局要求内部有且只能有一个组件 9 | */ 10 | public class MarginLayout implements LayoutManager { 11 | //四个边距需要存储一下 12 | private final int left; 13 | private final int right; 14 | private final int top; 15 | private final int bottom; 16 | 17 | /** 18 | * 一次性设定四个边距,如果需要可以自行扩展 19 | * @param margin 边距 20 | */ 21 | public MarginLayout(int margin){ 22 | this.left = this.bottom = this.right = this.top = margin; 23 | } 24 | 25 | @Override 26 | public void addLayoutComponent(String name, Component comp) {} 27 | 28 | @Override 29 | public void removeLayoutComponent(Component comp) {} 30 | 31 | @Override 32 | public Dimension preferredLayoutSize(Container parent) { 33 | return parent.getSize(); 34 | } 35 | 36 | @Override 37 | public Dimension minimumLayoutSize(Container parent) { 38 | return parent.getMinimumSize(); 39 | } 40 | 41 | @Override 42 | public void layoutContainer(Container parent) { 43 | if (parent.getComponents().length != 1) 44 | throw new IllegalStateException("此布局的容器中必须有且仅有一个组件!"); 45 | Component component = parent.getComponent(0); 46 | //计算组件的大小 47 | component.setSize(this.calculateComponentSize(parent.getSize())); 48 | //计算组件的位置 49 | component.setLocation(this.calculateComponentLocation()); 50 | } 51 | 52 | /** 53 | * 组件大小计算 54 | * @param parent 外部容器大小 55 | * @return 最终大小 56 | */ 57 | private Dimension calculateComponentSize(Dimension parent){ 58 | //宽度就是外部容器宽度 减去 左右两个边距 59 | int width = (int) (parent.getWidth() - this.left - this.right); 60 | //高度就是外部容器高度 减去 上下两个边距 61 | int height = (int) (parent.getHeight() - this.top - this.bottom); 62 | return new Dimension(width, height); 63 | } 64 | 65 | /** 66 | * 计算组件的最终位置 67 | * @return 最终位置 68 | */ 69 | private Point calculateComponentLocation(){ 70 | return new Point(left, top); //这个就简单了,实际上就是 (左边距,上边距) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/com/intellij/window/service/AbstractService.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.service; 2 | 3 | import com.intellij.window.AbstractWindow; 4 | 5 | import java.awt.*; 6 | import java.util.function.Function; 7 | 8 | /** 9 | * AbstractService是所有窗口业务层实现的顶层抽象。 10 | * 这个类只进行各项实际业务处理,不负责UI相关内容,特别要求: 11 | * - 不能编写任何构造方法 12 | * 开始享用吧! 13 | */ 14 | public abstract class AbstractService { 15 | 16 | private AbstractWindow window; 17 | private Function componentGetter; 18 | 19 | /** 20 | * 方便AbstractWindows进行服务配置,快速指定当前Service所属的窗口。 21 | * @param window 当前业务层所属窗口 22 | */ 23 | public final void setWindow(AbstractWindow window, Function componentGetter) { 24 | this.window = window; 25 | this.componentGetter = componentGetter; 26 | } 27 | 28 | /** 29 | * 通过组件名称,快速得到对应组件对象 30 | * @param componentName 组件名称 31 | * @return 组件对象 32 | */ 33 | @SuppressWarnings("unchecked") 34 | protected final T getComponent(String componentName){ 35 | return (T) this.componentGetter.apply(componentName); 36 | } 37 | 38 | /** 39 | * 获取当前业务实现的所属窗口 40 | * @return 窗口 41 | */ 42 | protected final AbstractWindow getWindow(){ 43 | return this.window; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/com/intellij/window/service/MainService.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.service; 2 | 3 | import com.intellij.entity.ProcessResult; 4 | import com.intellij.entity.config.ProjectConfigure; 5 | import com.intellij.manage.ProcessExecuteEngine; 6 | import com.intellij.manage.FileManager; 7 | import com.intellij.window.MainWindow; 8 | import com.intellij.window.dialog.CompileErrorDialog; 9 | import com.intellij.window.dialog.ProjectConfigDialog; 10 | 11 | import javax.swing.*; 12 | import javax.swing.event.DocumentEvent; 13 | import javax.swing.event.DocumentListener; 14 | import javax.swing.undo.UndoManager; 15 | import java.awt.event.*; 16 | import java.io.*; 17 | import java.nio.file.Files; 18 | import java.nio.file.Paths; 19 | 20 | public class MainService extends AbstractService { 21 | //当前项目的路径和项目名称 22 | private String path; 23 | //当前项目的配置文件,包括主类、java可执行文件位置等。 24 | private ProjectConfigure configure; 25 | //用于记录当前正在编辑的文件 26 | private File currentFile; 27 | //重做管理器,用于编辑框支持撤销和重做操作的 28 | private UndoManager undoManager; 29 | //用于记录当前项目是否处于运行状态 30 | private boolean isProjectRunning = false; 31 | 32 | /** 33 | * 设定当前项目的名称和路径 34 | * @param path 路径 35 | */ 36 | public void setPath(String path){ 37 | this.path = path.replace("\\", "/"); 38 | } 39 | 40 | /** 41 | * 获取当前项目的配置 42 | * @return 项目配置 43 | */ 44 | public ProjectConfigure getConfigure() { 45 | return configure; 46 | } 47 | 48 | /** 49 | * 加载项目配置文件 50 | */ 51 | public void loadProjectConfig(){ 52 | File file = new File(path+"/.idea"); 53 | if(file.exists()) { 54 | try (ObjectInputStream stream = new ObjectInputStream(Files.newInputStream(file.toPath()))){ 55 | configure = (ProjectConfigure) stream.readObject(); 56 | }catch (Exception e){ 57 | e.printStackTrace(); 58 | } 59 | } else { 60 | this.updateAndSaveConfigure(new ProjectConfigure("", "java")); 61 | } 62 | } 63 | 64 | /** 65 | * 更新并保存新的设置 66 | * @param configure 新的设置 67 | */ 68 | public void updateAndSaveConfigure(ProjectConfigure configure){ 69 | JButton button = this.getComponent("main.button.run"); 70 | this.configure = configure; 71 | try (ObjectOutputStream stream = new ObjectOutputStream(Files.newOutputStream(Paths.get(path+"/.idea")))){ 72 | stream.writeObject(configure); 73 | stream.flush(); 74 | if(button != null) { 75 | if(configure.getMainClass().isEmpty()) { 76 | button.setEnabled(false); 77 | button.setToolTipText("请先完成项目运行配置!"); 78 | } else { 79 | button.setEnabled(true); 80 | button.setToolTipText("点击编译运行项目"); 81 | } 82 | } 83 | }catch (Exception e){ 84 | e.printStackTrace(); 85 | } 86 | } 87 | 88 | /** 89 | * 运行按钮的行为,包括以下两种行为: 90 | * - 如果项目处于运行状态,那么点击就会停止项目。 91 | * - 如果项目没有处于运行状态,那么就会启动项目。 92 | */ 93 | public void runButtonAction(){ 94 | MainWindow window = (MainWindow) this.getWindow(); 95 | JButton button = this.getComponent("main.button.run"); 96 | JTextArea consoleArea = this.getComponent("main.textarea.console"); 97 | //判断当前项目是否已经开始运行了,分别进行操作 98 | if(!this.isProjectRunning) { 99 | //如果项目没有运行,那么需要先编译项目源代码,如果编译成功,那么就可以开始运行项目了 100 | button.setEnabled(false); 101 | consoleArea.setText("正在编译项目源代码..."); 102 | ProcessResult result = ProcessExecuteEngine.buildProject(path); 103 | if(result.getExitCode() != 0) { 104 | CompileErrorDialog dialog = new CompileErrorDialog(this.getWindow(), result.getOutput()); 105 | dialog.openDialog(); 106 | button.setEnabled(true); 107 | return; 108 | } 109 | //项目编译完成之后,可能会新增文件,所以需要刷新一下文件树 110 | window.refreshFileTree(); 111 | //新开一个线程实时对项目的运行进行监控,并实时将项目的输出内容更新到控制台 112 | new Thread(() -> { 113 | this.isProjectRunning = true; 114 | consoleArea.setText("正在编译项目源代码...编译完成,程序已启动:\n"); 115 | button.setText("停止"); 116 | button.setEnabled(true); 117 | //准备工作完成之后,就可以正式启动进程了,这里最后会返回执行结果 118 | ProcessResult res = ProcessExecuteEngine.startProcess( 119 | path, configure.getJavaCommand(), configure.getMainClass(), consoleArea::append); 120 | if(res.getExitCode() != 0) 121 | consoleArea.append(res.getOutput()); 122 | consoleArea.append("\n进程已结束,退出代码 "+res.getExitCode()); 123 | button.setText("运行"); 124 | this.isProjectRunning = false; 125 | }).start(); 126 | } else { 127 | //如果项目正在运行,那么点击按钮就相当于是结束项目运行 128 | ProcessExecuteEngine.stopProcess(); 129 | this.isProjectRunning = false; 130 | } 131 | } 132 | 133 | /** 134 | * 构建按钮的行为,很明显,直接构建就完事了 135 | */ 136 | public void buildButtonAction(){ 137 | MainWindow window = (MainWindow) this.getWindow(); 138 | ProcessResult result = ProcessExecuteEngine.buildProject(path); 139 | if(result.getExitCode() == 0) { 140 | JOptionPane.showMessageDialog(window, "编译成功!"); 141 | } else { 142 | CompileErrorDialog dialog = new CompileErrorDialog(window, result.getOutput()); 143 | dialog.openDialog(); 144 | } 145 | window.refreshFileTree(); 146 | } 147 | 148 | /** 149 | * 设置按钮的行为,更简单了,直接打开设置面板就完事 150 | */ 151 | public void settingButtonAction(){ 152 | MainWindow window = (MainWindow) this.getWindow(); 153 | ProjectConfigDialog dialog = new ProjectConfigDialog(window, this, configure); 154 | dialog.openDialog(); 155 | } 156 | 157 | /** 158 | * 创建一个新的源代码新的文件并生成默认代码 159 | */ 160 | public void createNewFile(){ 161 | String newFileName = JOptionPane.showInputDialog(this.getWindow(), 162 | "请输入你要创建的Java类名称(含包名,如 com.test.Main)", "创建新的Java文件", JOptionPane.PLAIN_MESSAGE); 163 | this.createFile(newFileName); 164 | } 165 | 166 | public void deleteProjectFile(){ 167 | String newFileName = JOptionPane.showInputDialog(this.getWindow(), 168 | "请输入你要删除的Java类名称(含包名,如 com.test.Main)", "删除Java文件", JOptionPane.WARNING_MESSAGE); 169 | this.deleteFile(newFileName); 170 | } 171 | 172 | /** 173 | * 配置文件树的右键弹出窗口 174 | * @return MouseAdapter 175 | */ 176 | public MouseAdapter fileTreeRightClick(){ 177 | JTree fileTree = this.getComponent("main.tree.files"); 178 | JPopupMenu treePopupMenu = this.getComponent("main.popup.tree"); 179 | return new MouseAdapter() { 180 | @Override 181 | public void mouseClicked(MouseEvent e) { 182 | if (e.getButton() == MouseEvent.BUTTON3) 183 | treePopupMenu.show(fileTree, e.getX(), e.getY()); 184 | } 185 | }; 186 | } 187 | 188 | /** 189 | * 配置编辑框的各项功能 190 | */ 191 | public void setupEditArea(){ 192 | JTextArea editArea = this.getComponent("main.textarea.edit"); 193 | //当文本内容发生变化时,自动写入到文件中 194 | editArea.getDocument().addDocumentListener(new DocumentListener() { 195 | @Override 196 | public void insertUpdate(DocumentEvent e) { 197 | MainService.this.saveFile(); 198 | } 199 | 200 | @Override 201 | public void removeUpdate(DocumentEvent e) { 202 | MainService.this.saveFile(); 203 | } 204 | 205 | @Override 206 | public void changedUpdate(DocumentEvent e) { 207 | MainService.this.saveFile(); 208 | } 209 | }); 210 | //按下Tab键时,应该输入四个空格,而不是一个Tab缩进(不然太丑) 211 | editArea.addKeyListener(new KeyAdapter() { 212 | @Override 213 | public void keyPressed(KeyEvent e) { 214 | if(e.getKeyCode() == 9) { 215 | e.consume(); 216 | editArea.insert(" ", editArea.getCaretPosition()); 217 | } 218 | } 219 | }); 220 | //由于默认的文本区域不支持重做和撤销操作,需要使用UndoManager进行配置,这里添加快捷键 221 | editArea.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo"); 222 | editArea.getActionMap().put("Redo", new AbstractAction() { 223 | @Override 224 | public void actionPerformed(ActionEvent e) { 225 | if(undoManager.canRedo()) undoManager.redo(); 226 | } 227 | }); 228 | editArea.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo"); 229 | editArea.getActionMap().put("Undo", new AbstractAction() { 230 | @Override 231 | public void actionPerformed(ActionEvent e) { 232 | if(undoManager.canUndo()) undoManager.undo(); 233 | } 234 | }); 235 | } 236 | 237 | /** 238 | * 让控制台输入重定向到进程的系统输入中 239 | * @return KeyAdapter 240 | */ 241 | public KeyAdapter inputRedirect(){ 242 | JTextArea consoleArea = this.getComponent("main.textarea.console"); 243 | return new KeyAdapter() { 244 | @Override 245 | public void keyReleased(KeyEvent e) { 246 | if(isProjectRunning) { 247 | String str = String.valueOf(e.getKeyChar()); 248 | ProcessExecuteEngine.redirectToProcess(str); 249 | consoleArea.append(str); 250 | } 251 | } 252 | }; 253 | } 254 | 255 | /** 256 | * 切换当前编辑的文件,并更新编辑面板中的内容 257 | * @param path 文件路径 258 | */ 259 | public void switchEditFile(String path) { 260 | JTextArea editArea = this.getComponent("main.textarea.edit"); 261 | currentFile = null; 262 | File file = new File(path); 263 | if(file.isDirectory()) return; 264 | editArea.getDocument().removeUndoableEditListener(undoManager); 265 | if(file.getName().endsWith(".class")) { 266 | editArea.setText(ProcessExecuteEngine.decompileCode(file.getAbsolutePath())); 267 | editArea.setEditable(false); 268 | } else { 269 | try(FileReader reader = new FileReader(file)) { 270 | StringBuilder builder = new StringBuilder(); 271 | int len; 272 | char[] chars = new char[1024]; 273 | while ((len = reader.read(chars)) > 0) 274 | builder.append(chars, 0, len); 275 | editArea.setText(builder.toString()); 276 | editArea.setEditable(true); 277 | } catch (IOException e) { 278 | e.printStackTrace(); 279 | } 280 | } 281 | editArea.getDocument().addUndoableEditListener((undoManager = new UndoManager())); 282 | currentFile = file; 283 | } 284 | 285 | private void deleteFile(String name){ 286 | if(name == null) return; 287 | String[] split = name.split("\\."); 288 | String className = split[split.length - 1]; 289 | String packageName = name.substring(0, name.length() - className.length() - 1); 290 | 291 | File file = new File(path+"/src/"+packageName.replace(".", "/")+"/"+className+".java"); 292 | if(file.exists() && file.delete()) { 293 | JOptionPane.showMessageDialog(this.getWindow(), "文件删除成功!"); 294 | }else { 295 | JOptionPane.showMessageDialog(this.getWindow(), "文件删除失败,文件不存在?"); 296 | } 297 | MainWindow window = (MainWindow) this.getWindow(); 298 | window.refreshFileTree(); 299 | } 300 | 301 | /** 302 | * 创建源文件,并生成默认代码 303 | * @param name 名称 304 | */ 305 | private void createFile(String name){ 306 | MainWindow window = (MainWindow) this.getWindow(); 307 | if(name == null) return; 308 | String[] split = name.split("\\."); 309 | String className = split[split.length - 1]; 310 | String packageName = name.substring(0, name.length() - className.length() - 1); 311 | 312 | try { 313 | File dir = new File(path+"/src/"+packageName.replace(".", "/")); 314 | if(!dir.exists() && !dir.mkdirs()) { 315 | JOptionPane.showMessageDialog(window, "无法创建文件夹!"); 316 | return; 317 | } 318 | File file = new File(path+"/src/"+packageName.replace(".", "/")+"/"+className+".java"); 319 | if(file.exists() || !file.createNewFile()) { 320 | JOptionPane.showMessageDialog(window, "无法创建,此文件已存在!"); 321 | return; 322 | } 323 | FileWriter writer = new FileWriter(file); 324 | writer.write(FileManager.defaultCode(className, packageName)); 325 | writer.flush(); 326 | window.refreshFileTree(); 327 | writer.close(); 328 | } catch (IOException e) { 329 | e.printStackTrace(); 330 | } 331 | } 332 | 333 | /** 334 | * 保存当前编辑框中的内容到当前文件中 335 | */ 336 | private void saveFile(){ 337 | JTextArea editArea = this.getComponent("main.textarea.edit"); 338 | if(currentFile == null) return; 339 | try (FileWriter writer = new FileWriter(currentFile)){ 340 | writer.write(editArea.getText()); 341 | writer.flush(); 342 | } catch (IOException e) { 343 | e.printStackTrace(); 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/com/intellij/window/service/WelcomeService.java: -------------------------------------------------------------------------------- 1 | package com.intellij.window.service; 2 | 3 | import com.intellij.entity.ProjectEntity; 4 | import com.intellij.manage.FileManager; 5 | import com.intellij.manage.ProjectManager; 6 | import com.intellij.window.MainWindow; 7 | import com.intellij.window.WelcomeWindow; 8 | import com.intellij.window.dialog.CreateProjectDialog; 9 | import com.intellij.window.dialog.DirectoryChooserDialog; 10 | import com.intellij.window.dialog.GitProjectDialog; 11 | 12 | import javax.swing.*; 13 | import java.awt.event.KeyAdapter; 14 | import java.awt.event.KeyEvent; 15 | import java.io.File; 16 | 17 | public class WelcomeService extends AbstractService { 18 | /** 19 | * 创建新项目行为 20 | */ 21 | public void createNewProject(){ 22 | CreateProjectDialog dialog = new CreateProjectDialog((WelcomeWindow) this.getWindow()); 23 | dialog.openDialog(); 24 | } 25 | 26 | /** 27 | * 打开已有项目行为 28 | */ 29 | public void openProject(){ 30 | DirectoryChooserDialog dialog = new DirectoryChooserDialog(this.getWindow()); 31 | dialog.openDialog(); 32 | File selectedFile = dialog.getSelectedFile(); 33 | if(selectedFile == null) return; 34 | ProjectManager.createProject(selectedFile.getName(), selectedFile.getParentFile().getAbsolutePath()); 35 | this.getWindow().dispose(); 36 | MainWindow window = new MainWindow(selectedFile.getName(), selectedFile.getAbsolutePath()); 37 | window.openWindow(); 38 | } 39 | 40 | /** 41 | * 从版本控制系统打开已有项目行为 42 | */ 43 | public void openVcsProject() { 44 | GitProjectDialog dialog = new GitProjectDialog((WelcomeWindow) this.getWindow()); 45 | dialog.openDialog(); 46 | } 47 | 48 | /** 49 | * 生成刷新项目列表的KeyAdapter 50 | * @return Adapter 51 | */ 52 | public KeyAdapter refreshListAdapter(){ 53 | return new KeyAdapter() { 54 | @Override 55 | public void keyReleased(KeyEvent e) { 56 | WelcomeWindow window = (WelcomeWindow) getWindow(); 57 | window.initProjectList(); 58 | } 59 | }; 60 | } 61 | 62 | /** 63 | * 进入项目,也就是关闭当前窗口打开编辑界面 64 | * @param project 项目实体 65 | */ 66 | public void enterProject(ProjectEntity project){ 67 | this.getWindow().dispose(); 68 | MainWindow window = new MainWindow(project.getName(), project.getFilePath()); 69 | window.openWindow(); 70 | } 71 | 72 | /** 73 | * 删除项目,包括项目文件 74 | * @param project 项目实体 75 | */ 76 | public void deleteProject(ProjectEntity project){ 77 | WelcomeWindow window = (WelcomeWindow) getWindow(); 78 | int res = JOptionPane.showConfirmDialog(window, 79 | "你确定要关闭删除这个项目吗(不可恢复)", "警告", JOptionPane.YES_NO_OPTION); 80 | if(res == JOptionPane.YES_OPTION) { 81 | FileManager.deleteProject(project.getName(), project.getFilePath()); 82 | window.initProjectList(); 83 | } 84 | } 85 | } 86 | --------------------------------------------------------------------------------