├── .github └── FUNDING.yml ├── .gitignore ├── README-ZH.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradlew ├── gradlew.bat ├── screenshot ├── main.png ├── preview.png └── settings.png ├── settings.gradle.kts ├── src └── main │ ├── java │ └── com │ │ └── dengzii │ │ └── plugin │ │ └── ui │ │ ├── ConfigurePanel.form │ │ ├── ConfigurePanel.java │ │ ├── CreateModuleDialog.form │ │ ├── CreateModuleDialog.java │ │ ├── FileDialog.form │ │ └── FileDialog.java │ ├── kotlin │ └── com │ │ └── dengzii │ │ └── plugin │ │ └── template │ │ ├── Config.kt │ │ ├── CreateModuleAction.kt │ │ ├── CreateTemplateFromDirAction.kt │ │ ├── FileWriteCommand.kt │ │ ├── TemplateConfigurable.kt │ │ ├── model │ │ ├── FileTreeDsl.kt │ │ ├── FileTreeNode.kt │ │ └── Module.kt │ │ ├── template │ │ ├── AucTemplate.kt │ │ ├── FileTemplateFactory.kt │ │ └── Template.kt │ │ ├── tools │ │ ├── NotificationUtils.kt │ │ ├── PersistentConfig.kt │ │ └── ui │ │ │ ├── ActionToolBarUtils.kt │ │ │ ├── ColumnInfo.kt │ │ │ ├── ComponentExtensions.kt │ │ │ ├── PopMenuUtils.kt │ │ │ ├── TableAdapter.kt │ │ │ ├── TableColumnModelDecorator.kt │ │ │ ├── XDialog.kt │ │ │ ├── XMenu.kt │ │ │ └── XMenuBar.kt │ │ ├── ui │ │ ├── EditToolbar.kt │ │ ├── EditableTable.kt │ │ ├── PreviewPanel.kt │ │ └── RealConfigurePanel.kt │ │ └── utils │ │ ├── Logger.kt │ │ ├── PluginKit.kt │ │ └── PopMenuUtils.kt │ └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── fileTemplates │ ├── Template MainActivity.java.ft │ ├── Template Manifest.xml.ft │ └── Template build.gradle.ft └── test ├── AucTemplateTest.kt ├── FileTreeDslTest.kt └── com └── dengzii └── plugin └── template └── model ├── FileTreeNodeTest.kt └── ModuleTest.kt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['paypal.me/denua'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | # Generate Module From Template 2 | 3 | [![JetBrains IntelliJ plugins](https://img.shields.io/jetbrains/plugin/d/13586-generate-module-from-template) ](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) 4 | [![JetBrains IntelliJ plugins](https://img.shields.io/jetbrains/plugin/v/13586-generate-module-from-template) ](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) 5 | 6 | [README - EN](https://github.com/dengzii/GenerateModuleFromTemplate/blob/master/README.md) 7 | 8 | 使用这个用于 IntelliJ IDEs 的目录模板插件, 帮助你从模板生成任何目录结构 9 | 10 | ### 功能 11 | 12 | 1. 自定义目录结构 13 | 2. 目录, 文件名, 文件模板支持配置占位符 14 | 3. 支持文件模板配置 15 | 4. 与小伙伴分享你的模板 16 | 17 | ### 使用 18 | 19 | 1. 在设置中配置插件的模板: File > Settings > Tools > Module Template Settings 20 | 2. 在 Structure 中配置目录树, 右键编辑树结构. 21 | 3. 目录树可以使用占位符, 占位符是这样的 -> ${YOUR_PLACEHOLDER_HERE}. 22 | 4. File Template 中可以配置指定文件的模板, 文件名中可以使用占位符, 会自动替换成你创建时配置的. 23 | 5. Placeholder 中列出了你目录树中所有的占位符, 你可以给他们设置默认值. 24 | 6. 插件中使用的模板是在 IDE 本身 Editor>File And Code Templates 中的模板. 25 | 7. 如果插件更新升级了, 则之前配置保存的模板可能会存在不兼容问题. 26 | 8. **你的 star, 是我更新的动力.** 27 | 28 | - 已存在的文件和目录将被跳过 29 | - 如果目录名包含 `/` 将被分割展开. 30 | - 占位符可以无限嵌套, 例如 `${${A}_${B}}`, A=a, B=b 则会变成一个新的占位符 ${a_b}. 31 | 32 | ### 更新日志 33 | 34 | - 1.4.0: feature: Support export and import template to file, adjust action button position. 35 | - 1.3.1: fix: AucFrame module template bugs. 36 | - 1.3: fix: Placeholder don't work when call FileTreeNode.include. 37 | - 1.2: feature: all IntelliJ platform IDEs support, file template selection support when edit module template. 38 | - 1.1: feature: support create module template, placeholder, file template 39 | - 1.1: feature: support create module template, placeholder, file template 1.0: basically feature, generate module 40 | directories from template 41 | - 1.0: basically feature, generate module directories from template 42 | 43 | ### 截图 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate Module From Template 2 | 3 | [![JetBrains IntelliJ plugins](https://img.shields.io/jetbrains/plugin/d/13586-generate-module-from-template) ](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) 4 | [![JetBrains IntelliJ plugins](https://img.shields.io/jetbrains/plugin/v/13586-generate-module-from-template) ](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) 5 | 6 | [中文 - README](https://github.com/dengzii/GenerateModuleFromTemplate/blob/master/README-ZH.md) 7 | 8 | [Video Tutorial - YouTube](https://youtu.be/TyeXnbCcBP4) 9 | 10 | ### Create a directory structure from a highly customizable template 11 | 12 | Using this plugin, help you create directories and files from the customizable template. 13 | 14 | ### Feature 15 | 16 | 1. Custom directory structure. 17 | 2. Support placeholders / ApacheVelocity template language, and replace it when you create a module. 18 | 3. Specify file templates from IDE custom/build-in templates. 19 | 4. Passing placeholders to file template as variables. 20 | 5. Output/import template file. share your template with your partner. 21 | 22 | ### Usage 23 | 24 | 1. Configure template in plugin settings: File > Settings > Tools > Module Template Settings. 25 | 2. Create directories from the 'Structure' tab, click the right mouse button to operate the file tree. 26 | 3. FileTree can use placeholders / ApacheVelocity template language , the placeholder should like this -> ${YOUR_PLACEHOLDER_HERE}. 27 | 4. The 'File Template' tab lists which template the specified file uses, you can also use placeholders / attributes for FileName 28 | field. 29 | 5. The 'Placeholder' tab's table defines placeholders / attributes for replacing filenames and file templates 30 | 31 | *NOTE* 32 | 33 | - The nested placeholder in dir tree will be calculated and merged to a new placeholder, eg: `${${A}_${B}}`, A=a, 34 | B=b,result=`${a_b}`. 35 | - The existing files will be skipped. 36 | - The Java class file name may depend on ClassName, you better keep the class name and file name consistent, else the 37 | file name in the template will not effective. 38 | - The placeholders are best not the same as the built-in property of `Apache Velocity`. 39 | 40 | ### Changelog 41 | 42 | - 1.5.0: Fix: specify a template doesn't work., feature: fetch template variables as placeholders when create the file, 43 | support specify file template when create the module. ui looks more comfortable. 44 | - 1.4.0: feature: Support export and import template to file, adjust action button position. 45 | - 1.3.1: fix: AucFrame module template bugs. 46 | - 1.3.0: fix: Placeholder don't work when call FileTreeNode.include. 47 | - 1.2.0: feature: all IntelliJ platform IDEs support, file template selection support when edit module template. 48 | - 1.1.0: feature: support create module template, placeholder, file template 49 | - 1.1.0: feature: support create module template, placeholder, file template 1.0: basically feature, generate module 50 | directories from template 51 | - 1.0.0: basically feature, generate module directories from template 52 | 53 | ### Screenshot 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | id("org.jetbrains.kotlin.jvm") version "1.9.24" 4 | id("org.jetbrains.intellij") version "1.17.3" 5 | } 6 | 7 | group = "com.dengzii" 8 | version = "1.6.9" 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | // Configure Gradle IntelliJ Plugin 15 | // Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html 16 | intellij { 17 | version.set("2023.2.6") 18 | type.set("IC") // Target IDE Platform 19 | } 20 | 21 | tasks { 22 | // Set the JVM compatibility versions 23 | withType { 24 | sourceCompatibility = "17" 25 | targetCompatibility = "17" 26 | } 27 | withType { 28 | kotlinOptions.jvmTarget = "17" 29 | } 30 | 31 | patchPluginXml { 32 | sinceBuild.set("232") 33 | untilBuild.set("242.*") 34 | } 35 | 36 | signPlugin { 37 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 38 | privateKey.set(System.getenv("PRIVATE_KEY")) 39 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 40 | } 41 | 42 | publishPlugin { 43 | token.set(System.getenv("PUBLISH_TOKEN")) 44 | } 45 | 46 | dependencies { 47 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 48 | } 49 | 50 | // val jbrExplicitVersion = "jbr_jcef-17.0.10-osx-aarch64-b1000.48" 51 | // 52 | // buildSearchableOptions { 53 | // jbrVersion.set(jbrExplicitVersion) 54 | // } 55 | // runIde { 56 | // jbrVersion.set(jbrExplicitVersion) 57 | // } 58 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 2 | kotlin.stdlib.default.dependency=false 3 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 4 | org.gradle.configuration-cache=true 5 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 6 | org.gradle.caching=true 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /screenshot/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzii/GenerateModuleFromTemplate/6e57dca59f6b079f288cc7e383690ab5a87399dd/screenshot/main.png -------------------------------------------------------------------------------- /screenshot/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzii/GenerateModuleFromTemplate/6e57dca59f6b079f288cc7e383690ab5a87399dd/screenshot/preview.png -------------------------------------------------------------------------------- /screenshot/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzii/GenerateModuleFromTemplate/6e57dca59f6b079f288cc7e383690ab5a87399dd/screenshot/settings.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "GenerateModuleFromTemplate" -------------------------------------------------------------------------------- /src/main/java/com/dengzii/plugin/ui/ConfigurePanel.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
219 | -------------------------------------------------------------------------------- /src/main/java/com/dengzii/plugin/ui/ConfigurePanel.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.ui; 2 | 3 | import com.intellij.ui.components.JBList; 4 | import com.intellij.ui.components.JBTabbedPane; 5 | 6 | import javax.swing.*; 7 | 8 | /** 9 | *
10 |  * author : dengzi
11 |  * e-mail : dengzii@foxmail.com
12 |  * github : https://github.com/dengzii
13 |  * time   : 2020/1/8
14 |  * desc   :
15 |  * 
16 | */ 17 | public class ConfigurePanel extends JPanel { 18 | 19 | public JPanel contentPane; 20 | public JTextField tfName; 21 | public JBList listTemplate; 22 | public JPanel panelStructure; 23 | public JPanel panelPlaceholder; 24 | public JPanel panelFileTemp; 25 | public JCheckBox cbPlaceholder; 26 | public JBTabbedPane tabbedPane; 27 | public JPanel panelActionBar; 28 | public JCheckBox cbLowercaseDir; 29 | public JCheckBox cbCapitalizeFile; 30 | public JCheckBox cbExpandPkgName; 31 | public JCheckBox cbEnableVelocity; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/dengzii/plugin/ui/CreateModuleDialog.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/main/java/com/dengzii/plugin/ui/CreateModuleDialog.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.ui; 2 | 3 | import com.dengzii.plugin.template.Config; 4 | import com.dengzii.plugin.template.TemplateConfigurable; 5 | import com.dengzii.plugin.template.model.Module; 6 | import com.dengzii.plugin.template.ui.EditableTable; 7 | import com.dengzii.plugin.template.ui.PreviewPanel; 8 | import com.dengzii.plugin.template.utils.Logger; 9 | import com.intellij.icons.AllIcons; 10 | import com.intellij.openapi.options.ShowSettingsUtil; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.ui.components.JBCheckBox; 13 | 14 | import javax.swing.*; 15 | import java.awt.*; 16 | import java.awt.event.ActionEvent; 17 | import java.awt.event.KeyEvent; 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | 23 | public class CreateModuleDialog extends JDialog { 24 | 25 | private static final String TAG = CreateModuleDialog.class.getSimpleName(); 26 | private final OnFinishListener onFinishListener; 27 | private final HashMap panels = new HashMap<>(); 28 | private final List titles = new ArrayList<>(); 29 | private final Project project; 30 | private JPanel rootPanel; 31 | private JLabel labelTitle; 32 | private JComboBox cbModuleTemplate; 33 | private JPanel mainPanel; 34 | private JPanel contentPanel; 35 | private JButton btConfigure; 36 | private JButton btCancel; 37 | private JButton btPrevious; 38 | private JButton btFinish; 39 | private JScrollPane scrollPanePlaceHolder; 40 | private JScrollPane scrollPaneFileTemplate; 41 | private JPanel panelConfig; 42 | private EditableTable tablePlaceholder; 43 | private EditableTable tableFileTemplate; 44 | private java.util.List moduleTemplates = Collections.emptyList(); 45 | private Module selectedModule; 46 | private PreviewPanel previewPanel; 47 | private int currentPanelIndex; 48 | 49 | private CreateModuleDialog(Project project, OnFinishListener onFinishListener) { 50 | setContentPane(rootPanel); 51 | setModal(true); 52 | getRootPane().setDefaultButton(btFinish); 53 | this.project = project; 54 | this.onFinishListener = onFinishListener; 55 | } 56 | 57 | public static void createAndShow(Project project, OnFinishListener onFinishListener) { 58 | 59 | CreateModuleDialog dialog = new CreateModuleDialog(project, onFinishListener); 60 | dialog.initDialog(); 61 | dialog.initData(); 62 | dialog.pack(); 63 | dialog.setVisible(true); 64 | } 65 | 66 | public static void main(String[] args) { 67 | createAndShow(null, config -> { 68 | 69 | }); 70 | } 71 | 72 | private void onNextClick(ActionEvent e) { 73 | tablePlaceholder.stopEdit(); 74 | tableFileTemplate.stopEdit(); 75 | selectedModule.getTemplate().removeAllTemplateInTree(); 76 | selectedModule.getTemplate().removeAllPlaceHolderInTree(); 77 | selectedModule.getTemplate().setPlaceholders(tablePlaceholder.getPairResult()); 78 | selectedModule.getTemplate().setFileTemplates(tableFileTemplate.getPairResult()); 79 | 80 | previewPanel.setPreviewMode(true); 81 | previewPanel.setModuleConfig(selectedModule); 82 | 83 | if (currentPanelIndex == panels.size() - 1) { 84 | onFinishListener.onFinish(selectedModule); 85 | dispose(); 86 | return; 87 | } 88 | currentPanelIndex++; 89 | setPanel(); 90 | setButton(); 91 | } 92 | 93 | private void onPreviousClick(ActionEvent e) { 94 | if (currentPanelIndex == 0) { 95 | return; 96 | } 97 | currentPanelIndex--; 98 | setPanel(); 99 | setButton(); 100 | } 101 | 102 | private void onConfClick(ActionEvent e) { 103 | ShowSettingsUtil.getInstance().editConfigurable(project, new TemplateConfigurable(this::initData, null)); 104 | } 105 | 106 | private void setButton() { 107 | btPrevious.setEnabled(true); 108 | btFinish.setText("Next"); 109 | if (currentPanelIndex == panels.size() - 1) { 110 | btFinish.setText("Finish"); 111 | } else if (currentPanelIndex == 0) { 112 | btPrevious.setEnabled(false); 113 | } 114 | } 115 | 116 | private void setPanel() { 117 | String title = titles.get(currentPanelIndex); 118 | labelTitle.setText(title); 119 | contentPanel.removeAll(); 120 | contentPanel.add(panels.get(title)); 121 | contentPanel.doLayout(); 122 | contentPanel.updateUI(); 123 | } 124 | 125 | private void onModuleConfigChange() { 126 | Logger.INSTANCE.i(TAG, "onModuleConfigChange"); 127 | selectedModule = moduleTemplates.get(cbModuleTemplate.getSelectedIndex()).clone(); 128 | selectedModule.getTemplate().expandPath(); 129 | 130 | panelConfig.removeAll(); 131 | panelConfig.add(new JBCheckBox("Capitalize file", selectedModule.getCapitalizeFile())); 132 | panelConfig.add(new JBCheckBox("Lowercase dir", selectedModule.getLowercaseDir())); 133 | panelConfig.add(new JBCheckBox("Expand package name", selectedModule.getPackageNameToDir())); 134 | panelConfig.doLayout(); 135 | panelConfig.updateUI(); 136 | for (Component c : panelConfig.getComponents()) { 137 | c.setEnabled(false); 138 | } 139 | 140 | previewPanel.setPreviewMode(true); 141 | previewPanel.setModuleConfig(selectedModule); 142 | tablePlaceholder.setPairData(selectedModule.getTemplate().getAllPlaceholdersMap()); 143 | tableFileTemplate.setPairData(selectedModule.getTemplate().getAllTemplateMap()); 144 | } 145 | 146 | private void initDialog() { 147 | 148 | Dimension screen = getToolkit().getScreenSize(); 149 | int w = screen.width / 3 + 160; 150 | int h = 600; 151 | int x = screen.width / 2 - w / 2; 152 | int y = screen.height / 2 - h / 2; 153 | setLocation(x, y); 154 | rootPanel.setPreferredSize(new Dimension(w, h)); 155 | 156 | setTitle("Create Module"); 157 | setDefaultCloseOperation(DISPOSE_ON_CLOSE); 158 | rootPanel.registerKeyboardAction(e -> dispose(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), 159 | JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 160 | 161 | tablePlaceholder = new EditableTable(new String[]{"Key", "Value"}, new Boolean[]{false, true}); 162 | tablePlaceholder.setToolBarVisible(false); 163 | tableFileTemplate = new EditableTable(new String[]{"File Name", "Template"}, new Boolean[]{false, true}); 164 | tableFileTemplate.setToolBarVisible(false); 165 | 166 | scrollPanePlaceHolder.setViewportView(tablePlaceholder); 167 | scrollPaneFileTemplate.setViewportView(tableFileTemplate); 168 | 169 | btConfigure.setIcon(AllIcons.General.GearPlain); 170 | previewPanel = new PreviewPanel(true); 171 | titles.add("Create Module From Template"); 172 | titles.add("Preview Module"); 173 | 174 | panels.put(titles.get(0), mainPanel); 175 | panels.put(titles.get(1), previewPanel); 176 | 177 | btFinish.addActionListener(this::onNextClick); 178 | btPrevious.addActionListener(this::onPreviousClick); 179 | btCancel.addActionListener(e -> dispose()); 180 | btConfigure.addActionListener(this::onConfClick); 181 | 182 | cbModuleTemplate.addItemListener(e -> { 183 | onModuleConfigChange(); 184 | }); 185 | } 186 | 187 | private void initData() { 188 | Logger.INSTANCE.i(TAG, "initData"); 189 | moduleTemplates = Config.INSTANCE.loadModuleTemplates(); 190 | // moduleTemplates = new ArrayList<>(); 191 | // moduleTemplates.add(Module.Companion.getAucModule()); 192 | List temp = new ArrayList<>(); 193 | moduleTemplates.forEach(i -> temp.add(i.getTemplateName())); 194 | cbModuleTemplate.setModel(new DefaultComboBoxModel<>(temp.toArray())); 195 | if (cbModuleTemplate.getModel().getSize() > 0) { 196 | cbModuleTemplate.setSelectedIndex(0); 197 | onModuleConfigChange(); 198 | } 199 | } 200 | 201 | public interface OnFinishListener { 202 | void onFinish(Module config); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/com/dengzii/plugin/ui/FileDialog.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | -------------------------------------------------------------------------------- /src/main/java/com/dengzii/plugin/ui/FileDialog.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.ui; 2 | 3 | import com.dengzii.plugin.template.model.FileTreeNode; 4 | import com.dengzii.plugin.template.utils.PluginKit; 5 | import com.intellij.ide.fileTemplates.FileTemplate; 6 | import com.intellij.ide.fileTemplates.FileTemplateUtil; 7 | import com.intellij.openapi.project.ProjectManager; 8 | import org.apache.velocity.runtime.parser.ParseException; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.awt.event.KeyAdapter; 13 | import java.awt.event.KeyEvent; 14 | import java.util.Properties; 15 | 16 | public class FileDialog extends JDialog { 17 | private static final String NONE = "None"; 18 | 19 | private JPanel contentPane; 20 | private JComboBox cbTemplate; 21 | private JTextField tfName; 22 | private JLabel lbTemplate; 23 | private JLabel lbName; 24 | private JButton btConfirm; 25 | private JPanel panelTemplate; 26 | private JCheckBox cbProperties; 27 | 28 | private CreateFileCallback createFileCallback; 29 | private boolean isDir; 30 | private FileTreeNode parent; 31 | private FileTreeNode fileNode; 32 | private FileTemplate[] fileTemplates; 33 | 34 | public static void showForRefactor(FileTreeNode node, CreateFileCallback createFileCallback) { 35 | FileDialog fileDialog = new FileDialog(); 36 | fileDialog.createFileCallback = createFileCallback; 37 | fileDialog.isDir = node.isDir(); 38 | fileDialog.parent = node.getParent(); 39 | fileDialog.fileNode = node; 40 | fileDialog.initDialog(); 41 | } 42 | 43 | public static void showForCreate(FileTreeNode parent, boolean isDir, CreateFileCallback createFileCallback) { 44 | FileDialog fileDialog = new FileDialog(); 45 | fileDialog.createFileCallback = createFileCallback; 46 | fileDialog.isDir = isDir; 47 | fileDialog.parent = parent; 48 | fileDialog.initDialog(); 49 | } 50 | 51 | private FileDialog() { 52 | setContentPane(contentPane); 53 | setDefaultCloseOperation(DISPOSE_ON_CLOSE); 54 | } 55 | 56 | private void initFileTemplateList() { 57 | fileTemplates = PluginKit.Companion.getAllFileTemplate(); 58 | String[] items = new String[fileTemplates.length + 1]; 59 | items[0] = NONE; 60 | for (int i = 0; i < fileTemplates.length; i++) { 61 | items[i + 1] = fileTemplates[i].getName(); 62 | } 63 | cbTemplate.setModel(new DefaultComboBoxModel<>(items)); 64 | if (isRefactor() && fileNode.getTemplateFile() != null 65 | && PluginKit.Companion.getFileTemplate(fileNode.getTemplateFile()) != null) { 66 | cbTemplate.setSelectedItem(fileNode.getTemplateFile()); 67 | } 68 | cbTemplate.addItemListener(e -> { 69 | if (cbTemplate.getSelectedIndex() == 0 || fileTemplates.length == 0) { 70 | tfName.setText(""); 71 | return; 72 | } 73 | FileTemplate selected = fileTemplates[cbTemplate.getSelectedIndex() - 1]; 74 | if (selected != null && !selected.getFileName().isBlank()) { 75 | tfName.setText(selected.getFileName() + "." + selected.getExtension()); 76 | } 77 | }); 78 | } 79 | 80 | private void onConfirm() { 81 | if (tfName.getText().trim().isEmpty()) { 82 | return; 83 | } 84 | String template = getSelectedTemplate(); 85 | template = NONE.equals(template) ? "" : template; 86 | 87 | if (isRefactor()) { 88 | fileNode.setName(tfName.getText()); 89 | // setup template 90 | } else { 91 | fileNode = new FileTreeNode(parent, tfName.getText(), isDir); 92 | } 93 | if (!isDir && template != null) { 94 | if (!template.isEmpty() && cbProperties.isSelected()) { 95 | FileTemplate ft = fileTemplates[cbTemplate.getSelectedIndex() - 1]; 96 | String[] p = new String[0]; 97 | try { 98 | p = FileTemplateUtil.calculateAttributes(ft.getText(), new Properties(), true, ProjectManager.getInstance().getDefaultProject()); 99 | } catch (ParseException e) { 100 | e.printStackTrace(); 101 | } 102 | fileNode.putPlaceholders(p); 103 | } 104 | if (!template.isEmpty()){ 105 | fileNode.addFileTemplate(fileNode.getName(), template); 106 | } 107 | } 108 | 109 | if (!isRefactor() && parent != null && parent.hasChild(tfName.getText(), isDir)) { 110 | lbName.setText(lbName.getText() + " (already exist.)"); 111 | return; 112 | } 113 | createFileCallback.callback(fileNode); 114 | dispose(); 115 | } 116 | 117 | private void initDialog() { 118 | 119 | if (isDir) { 120 | lbTemplate.setVisible(false); 121 | cbTemplate.setVisible(false); 122 | panelTemplate.setVisible(false); 123 | } else { 124 | initFileTemplateList(); 125 | } 126 | pack(); 127 | 128 | Dimension screen = getToolkit().getScreenSize(); 129 | int w = getWidth(); 130 | int h = getHeight(); 131 | int x = screen.width / 2 - w / 2; 132 | int y = screen.height / 2 - h / 2 - 100; 133 | setLocation(x, y); 134 | setPreferredSize(new Dimension(w, h)); 135 | 136 | setTitle((isRefactor() ? "Refactor " : "New ") + (isDir ? "Directory" : "File")); 137 | if (isRefactor()) { 138 | tfName.setText(fileNode.getName()); 139 | } 140 | tfName.addKeyListener(new KeyAdapter() { 141 | @Override 142 | public void keyReleased(KeyEvent e) { 143 | super.keyReleased(e); 144 | if (e.getKeyCode() == KeyEvent.VK_ENTER) { 145 | onConfirm(); 146 | } 147 | } 148 | }); 149 | btConfirm.addActionListener(e -> onConfirm()); 150 | setVisible(true); 151 | tfName.requestFocus(); 152 | } 153 | 154 | private String getSelectedTemplate() { 155 | if (isDir) { 156 | return null; 157 | } 158 | return cbTemplate.getSelectedItem().toString(); 159 | } 160 | 161 | private boolean isRefactor() { 162 | return fileNode != null; 163 | } 164 | 165 | public interface CreateFileCallback { 166 | void callback(FileTreeNode fileTreeNode); 167 | } 168 | 169 | public static void main(String[] args) { 170 | FileDialog.showForCreate(null, true, fileTreeNode -> { 171 | 172 | }); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/Config.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template 2 | 3 | import com.dengzii.plugin.template.model.Module 4 | import com.dengzii.plugin.template.utils.Logger 5 | import com.google.gson.GsonBuilder 6 | import com.intellij.ide.util.PropertiesComponent 7 | 8 | /** 9 | *
10 |  * author : dengzi
11 |  * e-mail : dengzixx@gmail.com
12 |  * github : https://github.com/dengzii
13 |  * time   : 2020/1/3
14 |  * desc   :
15 |  * 
16 | */ 17 | 18 | object Config { 19 | private val TAG = Config::class.java.simpleName 20 | 21 | private const val KEY_TEMPLATES = "KEY_TEMPLATES" 22 | private const val KEY_INIT = "KEY_INIT" 23 | 24 | val GSON by lazy { GsonBuilder().setLenient().create() } 25 | private val STORE by lazy { PropertiesComponent.getInstance() } 26 | 27 | private fun clear() = STORE.unsetValue(KEY_TEMPLATES) 28 | 29 | fun loadModuleTemplates(): MutableList { 30 | val result = mutableListOf() 31 | val arr = STORE.getValues(KEY_TEMPLATES) 32 | 33 | if (STORE.getBoolean(KEY_INIT)) { 34 | Logger.i(TAG, "INIT... load sample template") 35 | result.add(Module.getAndroidApplication()) 36 | } 37 | if (arr.isNullOrEmpty()) { 38 | return result 39 | } 40 | Logger.i(TAG, "loadModuleTemplates") 41 | arr.filter { 42 | !it.isNullOrBlank() 43 | }.forEach { 44 | try { 45 | val module = GSON.fromJson(it, Module::class.java) 46 | module.initTemplate() 47 | result.add(module) 48 | } catch (e: Exception) { 49 | clear() 50 | Logger.e(TAG, e) 51 | return result 52 | } 53 | } 54 | 55 | return result 56 | } 57 | 58 | fun saveModuleTemplates(templates: List) { 59 | if (templates.isEmpty()) { 60 | clear() 61 | return 62 | } 63 | val t = mutableListOf() 64 | templates.forEach { 65 | t.add(GSON.toJson(it)) 66 | Logger.d(TAG, "saveModuleTemplates ${t[t.lastIndex]}") 67 | } 68 | STORE.setValue(KEY_INIT, false, false) 69 | STORE.setValues(KEY_TEMPLATES, t.toTypedArray()) 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/CreateModuleAction.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template 2 | 3 | import com.dengzii.plugin.ui.CreateModuleDialog 4 | import com.dengzii.plugin.template.utils.Logger 5 | import com.dengzii.plugin.template.utils.PluginKit 6 | import com.intellij.openapi.actionSystem.AnAction 7 | import com.intellij.openapi.actionSystem.AnActionEvent 8 | import com.intellij.openapi.project.Project 9 | 10 | /** 11 | *
12 |  * author : dengzi
13 |  * e-mail : dengzixx@gmail.com
14 |  * github : https://github.com/dengzii
15 |  * time   : 2019/12/31
16 |  * desc   :
17 | 
* 18 | */ 19 | class CreateModuleAction : AnAction() { 20 | 21 | companion object { 22 | var project: Project? = null 23 | } 24 | 25 | override fun actionPerformed(e: AnActionEvent) { 26 | val kit = PluginKit.init(e) 27 | if (!kit.isProjectValid()) { 28 | Logger.d(CreateModuleAction::class.java.simpleName, "Project is not valid.") 29 | return 30 | } 31 | project = kit.project 32 | CreateModuleDialog.createAndShow(kit.project) { 33 | FileWriteCommand.startAction(kit, it) 34 | } 35 | } 36 | 37 | override fun update(e: AnActionEvent) { 38 | super.update(e) 39 | e.presentation.isEnabled = true 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/CreateTemplateFromDirAction.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template 2 | 3 | import com.dengzii.plugin.template.TemplateConfigurable.OnApplyListener 4 | import com.dengzii.plugin.template.model.FileTreeNode 5 | import com.dengzii.plugin.template.utils.Logger 6 | import com.dengzii.plugin.template.utils.PluginKit 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.PlatformDataKeys 10 | import com.intellij.openapi.options.ShowSettingsUtil 11 | import com.intellij.psi.PsiDirectory 12 | import com.intellij.psi.PsiFileSystemItem 13 | 14 | /** 15 | *
16 |  * author : dengzi
17 |  * e-mail : dengzixx@gmail.com
18 |  * github : https://github.com/dengzii
19 |  * time   : 2022/10/10
20 |  * desc   :
21 | 
* 22 | */ 23 | class CreateTemplateFromDirAction : AnAction() { 24 | 25 | override fun actionPerformed(e: AnActionEvent) { 26 | val kit = PluginKit.init(e) 27 | if (!kit.isProjectValid()) { 28 | Logger.d(CreateTemplateFromDirAction::class.java.simpleName, "Project is not valid.") 29 | return 30 | } 31 | 32 | val currentPsiDirectory = kit.getCurrentPsiDirectory() ?: return 33 | val treeNode = FileTreeNode(null, currentPsiDirectory.name, true) 34 | val root = FileTreeNode(null, "", true) 35 | root.addChild(treeNode) 36 | buildTreeNode(treeNode, currentPsiDirectory) 37 | 38 | ShowSettingsUtil.getInstance().editConfigurable(e.project, TemplateConfigurable(null, root)) 39 | } 40 | 41 | private fun buildTreeNode(parent: FileTreeNode, psiDirectory: PsiDirectory) { 42 | for (psiFile in psiDirectory.children) { 43 | if (psiFile !is PsiFileSystemItem) { 44 | continue 45 | } 46 | val node = FileTreeNode(parent, psiFile.name, psiFile.isDirectory) 47 | if (psiFile.isDirectory) { 48 | buildTreeNode(node, psiFile as PsiDirectory) 49 | } 50 | parent.addChild(node) 51 | } 52 | } 53 | 54 | override fun update(e: AnActionEvent) { 55 | super.update(e) 56 | e.presentation.isEnabled = e.getData(PlatformDataKeys.PSI_ELEMENT) is PsiDirectory 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/FileWriteCommand.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template 2 | 3 | import com.dengzii.plugin.template.model.FileTreeNode 4 | import com.dengzii.plugin.template.model.Module 5 | import com.dengzii.plugin.template.tools.NotificationUtils 6 | import com.dengzii.plugin.template.utils.Logger 7 | import com.dengzii.plugin.template.utils.PluginKit 8 | import com.intellij.openapi.command.UndoConfirmationPolicy 9 | import com.intellij.openapi.command.WriteCommandAction 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.util.ThrowableRunnable 12 | import org.apache.velocity.VelocityContext 13 | import org.apache.velocity.util.StringUtils 14 | 15 | /** 16 | * Create file tree. 17 | * 18 | * @author https://github.com/dengzii 19 | */ 20 | class FileWriteCommand(private var kit: PluginKit, private var module: Module) : ThrowableRunnable { 21 | 22 | companion object { 23 | private val TAG = FileWriteCommand::class.java.simpleName 24 | fun startAction(kit: PluginKit, module: Module) { 25 | WriteCommandAction.writeCommandAction(kit.project) 26 | .withGlobalUndo() 27 | .withUndoConfirmationPolicy(UndoConfirmationPolicy.REQUEST_CONFIRMATION) 28 | .run(FileWriteCommand(kit, module)) 29 | } 30 | } 31 | 32 | override fun run() { 33 | val current = kit.getVirtualFile() ?: return 34 | if (!current.isDirectory) { 35 | Logger.i(TAG, "Current target is not directory.") 36 | return 37 | } 38 | val fileTreeNode = module.template 39 | Logger.d(TAG, "Placeholders : " + fileTreeNode.getPlaceholderInherit().toString()) 40 | Logger.d(TAG, "FileTemplates : " + fileTreeNode.getFileTemplateInherit().toString()) 41 | 42 | var context: VelocityContext? = null 43 | if (module.enableApacheVelocity) { 44 | context = VelocityContext().apply { 45 | put("StringUtils", StringUtils::class.java) 46 | fileTreeNode.getPlaceholderInherit()?.forEach { (k, v) -> 47 | put(k, v) 48 | } 49 | } 50 | } 51 | fileTreeNode.context = context 52 | 53 | fileTreeNode.resolveTreeFileName() 54 | fileTreeNode.resolveFileTemplate() 55 | 56 | fileTreeNode.expandPath() 57 | fileTreeNode.expandPkgName(true) 58 | 59 | val failedList = createFileTree(context, fileTreeNode, current) 60 | if (failedList.isNotEmpty()) { 61 | val msg = failedList.joinToString("\n") { it.name } 62 | NotificationUtils.showError(msg, "The following files creation failed.") 63 | } 64 | } 65 | 66 | private fun createFileTree( 67 | context: VelocityContext?, 68 | treeNode: FileTreeNode, 69 | currentDirectory: VirtualFile 70 | ): List { 71 | if (treeNode.isRoot()) { 72 | return treeNode.children.map { 73 | createFileTree(context, it, currentDirectory) 74 | }.flatten() 75 | } 76 | Logger.d(TAG, "create node $treeNode") 77 | val failedList = mutableListOf() 78 | if (treeNode.isDir) { 79 | Logger.d(TAG, "create dir ${treeNode.getPath()}") 80 | val dir = kit.createDir(treeNode.name, currentDirectory) 81 | if (dir == null) { 82 | failedList.add(treeNode) 83 | Logger.e(TAG, "create directory failure: ${treeNode.name}") 84 | } else { 85 | treeNode.children.forEach { 86 | failedList.addAll(createFileTree(context, it, dir)) 87 | } 88 | } 89 | } else { 90 | val template = treeNode.getTemplateFile() 91 | if (template?.isNotBlank() == true) { 92 | val result = try { 93 | kit.createFileFromTemplate( 94 | treeNode.name, 95 | template, 96 | treeNode.getPlaceholderInherit().orEmpty(), 97 | currentDirectory 98 | ) 99 | } catch (e: Throwable) { 100 | e 101 | } 102 | if (result is Throwable) { 103 | NotificationUtils.showError( 104 | "file: ${treeNode.name} template:$template, error: ${result.message}", 105 | "Create file failed." 106 | ) 107 | failedList.add(treeNode) 108 | } else { 109 | Logger.d(TAG, "create file from template ${treeNode.name}") 110 | } 111 | } else { 112 | if (!kit.createFile(treeNode.name, currentDirectory)) { 113 | failedList.add(treeNode) 114 | } 115 | Logger.d(TAG, "create file ${treeNode.getPath()}") 116 | } 117 | } 118 | return failedList 119 | } 120 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/TemplateConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template 2 | 3 | import com.dengzii.plugin.template.model.FileTreeNode 4 | import com.dengzii.plugin.template.ui.RealConfigurePanel 5 | import com.intellij.openapi.options.SearchableConfigurable 6 | import org.jetbrains.annotations.Nls 7 | import javax.swing.JComponent 8 | 9 | /** 10 | *
11 |  * author : dengzi
12 |  * e-mail : dengzixx@gmail.com
13 |  * github : https://github.com/dengzii
14 |  * time   : 2020/1/16
15 |  * desc   :
16 |  * 
17 | */ 18 | class TemplateConfigurable() : SearchableConfigurable { 19 | 20 | private lateinit var panelConfig: RealConfigurePanel 21 | private var onApplyListener: OnApplyListener? = null 22 | 23 | private var add: FileTreeNode? = null 24 | 25 | constructor(onApplyListener: OnApplyListener?, addTemplate: FileTreeNode? = null) : this() { 26 | this.onApplyListener = onApplyListener 27 | this.add = addTemplate 28 | } 29 | 30 | override fun apply() { 31 | panelConfig.cacheConfig() 32 | panelConfig.saveConfig() 33 | onApplyListener?.onApply() 34 | } 35 | 36 | override fun getId(): String { 37 | return "preferences.ModuleTemplateConfig" 38 | } 39 | 40 | override fun createComponent(): JComponent { 41 | panelConfig = RealConfigurePanel() 42 | if (add != null) { 43 | panelConfig.createTempFromDir(add!!) 44 | } 45 | return panelConfig 46 | } 47 | 48 | override fun isModified(): Boolean { 49 | return panelConfig.isModified() 50 | } 51 | 52 | @Nls(capitalization = Nls.Capitalization.Title) 53 | override fun getDisplayName(): String { 54 | return "Module Template Settings" 55 | } 56 | 57 | interface OnApplyListener { 58 | fun onApply() 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/model/FileTreeDsl.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.model 2 | 3 | class FileTreeDsl() : FileTreeNode() { 4 | 5 | constructor(block: FileTreeDsl.() -> Unit) : this() { 6 | invoke(block) 7 | } 8 | 9 | operator fun invoke(block: FileTreeDsl.() -> Unit): FileTreeDsl { 10 | this.block() 11 | return this 12 | } 13 | 14 | operator fun FileTreeNode.invoke(block: FileTreeNode.() -> Unit) { 15 | block(this) 16 | } 17 | 18 | /** 19 | * create directory nodes from the path 20 | * if contains '/' or '.' in path 21 | * 22 | * @param path The dir path 23 | * @param block The child node domain 24 | */ 25 | fun FileTreeNode.dir(path: String, block: FileTreeNode.() -> Unit = {}) { 26 | if (!isDir) { 27 | this(block) 28 | return 29 | } 30 | val newNode = FileTreeNode(this, path, true) 31 | if (addChild(newNode)) { 32 | newNode.invoke(block) 33 | } 34 | } 35 | 36 | fun FileTreeNode.file(name: String): FileTreeNode { 37 | if (!isDir) throw IllegalStateException("Can not create file in a file.") 38 | 39 | name.getPlaceholder().forEach { 40 | if (getPlaceholderInherit()?.containsKey(it) == false) { 41 | placeholder(it, "") 42 | } 43 | } 44 | val r = FileTreeNode(this, name, false) 45 | addChild(r) 46 | return r 47 | } 48 | 49 | fun FileTreeNode.placeholder(name: String, value: String) { 50 | if (this.placeholders == null) { 51 | this.placeholders = mutableMapOf() 52 | } 53 | placeholders!![name] = value 54 | } 55 | 56 | /** 57 | * merge all children of another node to this. 58 | * all placeholders and file templates of target node 59 | * will be copied to it's each children 60 | */ 61 | fun FileTreeNode.include(other: FileTreeNode, override: Boolean = false) { 62 | if (!isDir) return 63 | other.children.forEach { 64 | val includeChild = it.clone() 65 | if (other.placeholders != null) { 66 | if (includeChild.placeholders == null) { 67 | includeChild.placeholders = mutableMapOf() 68 | } 69 | other.placeholders?.forEach { k, v -> 70 | if (getFileTemplateInherit()?.containsKey(k) != true) { 71 | includeChild.placeholders?.put(k, v) 72 | } 73 | } 74 | } 75 | if (other.fileTemplates != null) { 76 | if (includeChild.fileTemplates == null) { 77 | includeChild.fileTemplates = mutableMapOf() 78 | } 79 | other.fileTemplates?.forEach { k, v -> 80 | if (fileTemplates?.containsKey(k) != true) { 81 | includeChild.fileTemplates?.put(k, v) 82 | } 83 | } 84 | } 85 | addChild(includeChild, override) 86 | } 87 | } 88 | 89 | fun FileTreeNode.fileTemplate(fileName: String, template: String) { 90 | if (this.fileTemplates == null) { 91 | this.fileTemplates = mutableMapOf() 92 | } 93 | fileTemplates!![fileName] = template 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/model/FileTreeNode.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dengzii.plugin.template.model 4 | 5 | import com.dengzii.plugin.template.utils.Logger 6 | import org.apache.velocity.VelocityContext 7 | import org.apache.velocity.app.Velocity 8 | import java.io.File 9 | import java.io.StringWriter 10 | import java.util.* 11 | import java.util.regex.Pattern 12 | 13 | /** 14 | *
 15 |  * author : dengzi
 16 |  * e-mail : dengzixx@gmail.com
 17 |  * github : https://github.com/dengzii
 18 |  * time   : 2019/1/1
 19 |  * desc   :
 20 | 
*/ 21 | open class FileTreeNode() { 22 | 23 | var name: String = "" 24 | 25 | var isDir = true 26 | var children 27 | set(value) { 28 | realChildren = value 29 | realChildren.forEach { labeledChildren[it.getLabel()] = it } 30 | } 31 | get() = realChildren 32 | 33 | var placeholders: MutableMap? = null 34 | 35 | // template for node, higher priority than fileTemplates 36 | private var template: String? = null 37 | 38 | // template of filename 39 | var fileTemplates: MutableMap? = null 40 | 41 | private var realChildren = mutableSetOf() 42 | 43 | // the label composed by 'name' and 'isDir'. 44 | @Transient 45 | private val labeledChildren = mutableMapOf() 46 | 47 | @Transient 48 | var parent: FileTreeNode? = null 49 | 50 | @Transient 51 | private var module: Module? = null 52 | 53 | @Transient 54 | var context: VelocityContext? = null 55 | 56 | var resolvedFileTemplates: MutableMap? = null 57 | 58 | // flag whether the node is resolved, if true, represents the node info has been resolved via the velocity engine or placeholders. 59 | @Transient 60 | var resolved: Boolean = false 61 | 62 | companion object { 63 | 64 | private val TAG = FileTreeNode::class.java.simpleName 65 | private val sPathSplitPattern = Pattern.compile("/") 66 | private val sPkgSplitPattern = Pattern.compile("\\.") 67 | private val sPlaceholderPattern = Pattern.compile("\\$\\{([A-Za-z\\d_\\-]+)}") 68 | 69 | fun root(path: String): FileTreeNode { 70 | val root = File(path) 71 | if (!root.isDirectory) { 72 | throw RuntimeException("The root must be a directory.") 73 | } 74 | return with(FileTreeNode(null, path, true)) { 75 | this.parent = this 76 | this 77 | } 78 | } 79 | } 80 | 81 | constructor(parent: FileTreeNode?, name: String, isDir: Boolean) : this() { 82 | this.name = name 83 | this.parent = parent 84 | this.isDir = isDir 85 | } 86 | 87 | fun init(module: Module) { 88 | this.module = module 89 | children.forEach { 90 | it.parent = this 91 | if (it.isDir) { 92 | it.init(module) 93 | } 94 | } 95 | } 96 | 97 | private fun getTreeNodeCount(): Int { 98 | var count = realChildren.size 99 | realChildren.forEach { 100 | count += it.getTreeNodeCount() 101 | } 102 | return count 103 | } 104 | 105 | fun removeFromParent(): Boolean { 106 | if (parent != null) { 107 | parent!!.labeledChildren.remove(getLabel()) 108 | parent!!.realChildren.remove(this) 109 | parent = null 110 | return true 111 | } 112 | return false 113 | } 114 | 115 | /** 116 | * add child to this node 117 | * 118 | * @param child The child need be add 119 | * @param override Weather override the node with the same name and type 120 | */ 121 | fun addChild(child: FileTreeNode, override: Boolean = false): Boolean { 122 | if (hasChild(child.getLabel())) { 123 | if (override) { 124 | Logger.d(TAG, "node has already exists $child") 125 | return false 126 | } else { 127 | Logger.w(TAG, "THE SAME CHILE ALREADY EXISTS IN $name: ${child.name}") 128 | } 129 | } 130 | child.parent = this 131 | realChildren.add(child) 132 | labeledChildren[child.getLabel()] = child 133 | return true 134 | } 135 | 136 | fun hasChild(name: String, isDir: Boolean): Boolean { 137 | realChildren.forEach { 138 | if (it.name == name && isDir == it.isDir) { 139 | return true 140 | } 141 | } 142 | return false 143 | } 144 | 145 | private fun hasChild(label: String): Boolean { 146 | return labeledChildren.containsKey(label) 147 | } 148 | 149 | private fun getChild(name: String, isDir: Boolean): FileTreeNode? { 150 | return labeledChildren["${name}_$isDir"] 151 | } 152 | 153 | /** 154 | * get the real name replace with placeholder 155 | */ 156 | fun getRealName(velocityContext: VelocityContext? = null): String { 157 | return getRealNameInternal(velocityContext, this.name) 158 | } 159 | 160 | private fun getRealNameInternal( 161 | velocityContext: VelocityContext? = getContextInherit(), 162 | fileName: String = this.name, 163 | ): String { 164 | return if (isDir) { 165 | val rn = getResolveTemplate(velocityContext, fileName, getPlaceholderInherit(), false) 166 | if (getModuleInherit()?.lowercaseDir == true) rn.lowercase() else rn 167 | } else { 168 | val capitalize = getModuleInherit()?.capitalizeFile ?: false 169 | getResolveTemplate(velocityContext, fileName, getPlaceholderInherit(), capitalize) 170 | } 171 | } 172 | 173 | fun getPlaceholderInNodeName() = name.getPlaceholder() 174 | 175 | fun removeAllTemplateInTree() { 176 | fileTemplates?.clear() 177 | template = null 178 | traversal({ it, _ -> 179 | it.removeAllTemplateInTree() 180 | }) 181 | } 182 | 183 | fun removeAllPlaceHolderInTree() { 184 | placeholders?.clear() 185 | traversal({ it, _ -> 186 | it.removeAllPlaceHolderInTree() 187 | }) 188 | } 189 | 190 | /** 191 | * Return all file template in tree node 192 | * 193 | * TODO: optimize 194 | */ 195 | fun getAllTemplateMap(): Map { 196 | val templates = fileTemplates.orEmpty().toMutableMap() 197 | realChildren.forEach { 198 | templates.putAll(it.getAllTemplateMap()) 199 | } 200 | return templates 201 | } 202 | 203 | private fun getNodeHasTemplateInTree(): List { 204 | val nodes = mutableListOf() 205 | if (fileTemplates != null) { 206 | nodes.add(this) 207 | } 208 | realChildren.forEach { 209 | nodes.addAll(it.getNodeHasTemplateInTree()) 210 | } 211 | return nodes 212 | } 213 | 214 | private fun getContextInherit(): VelocityContext? { 215 | return if (context == null) { 216 | parent?.getContextInherit() 217 | } else { 218 | context 219 | } 220 | } 221 | 222 | fun getFileTemplateInherit(): MutableMap? { 223 | return parent?.getFileTemplateInherit()?.apply { 224 | putAll(fileTemplates.orEmpty()) 225 | } ?: fileTemplates ?: mutableMapOf() 226 | } 227 | 228 | fun getPlaceholderInherit(): MutableMap? { 229 | val r = parent?.getPlaceholderInherit() ?: mutableMapOf() 230 | r.putAll(placeholders ?: mapOf()) 231 | return r 232 | } 233 | 234 | /** 235 | * Resolve all file name in tree node with apache velocity, and set the resolved name to the node. 236 | * 237 | * @param context The velocity context 238 | */ 239 | fun resolveTreeFileName(context: VelocityContext? = getContextInherit()) { 240 | name = getRealNameInternal(context) 241 | traversal({ it, _ -> 242 | it.resolveTreeFileName(context) 243 | }) 244 | } 245 | 246 | /** 247 | * Resolve all file template file name in tree node. 248 | */ 249 | fun resolveFileTemplate(context: VelocityContext? = getContextInherit()) { 250 | val templates = getFileTemplateInherit() 251 | if (templates != null) { 252 | val resolved = mutableMapOf() 253 | val capitalize = getModuleInherit()?.capitalizeFile ?: false 254 | templates.forEach { 255 | val resolvedFileName = 256 | getResolveTemplate(context, it.key, getPlaceholderInherit(), capitalize = capitalize) 257 | resolved[resolvedFileName] = it.value 258 | } 259 | fileTemplates = resolved 260 | } 261 | traversal({ it, _ -> 262 | it.resolveFileTemplate(context) 263 | }) 264 | } 265 | 266 | fun resolve() { 267 | resolved = true 268 | resolveTreeFileName() 269 | resolveFileTemplate() 270 | traversal({ it, _ -> 271 | it.resolve() 272 | }) 273 | } 274 | 275 | /** 276 | * Return the most matching file template. 277 | * If the node has template, return it, otherwise return the template in parent node. 278 | */ 279 | fun getTemplateFile(): String? { 280 | if (template != null) { 281 | return template 282 | } 283 | val tpl = getFileTemplateInherit() ?: return null 284 | val path = getPath() 285 | for (entry in tpl) { 286 | if (entry.key != name && path.endsWith(entry.key)) { 287 | return entry.value 288 | } 289 | } 290 | return tpl[name] 291 | } 292 | 293 | fun setTemplate(name: String) { 294 | template = name 295 | } 296 | 297 | fun putPlaceholders(placeholders: Map) { 298 | if (this.placeholders == null) { 299 | this.placeholders = mutableMapOf() 300 | } 301 | this.placeholders!!.putAll(placeholders) 302 | } 303 | 304 | fun putPlaceholders(placeholders: Array) { 305 | putPlaceholders(placeholders.associateWith { (this.placeholders?.getOrDefault(it, "") ?: "") }) 306 | } 307 | 308 | /** 309 | * Add placeholders with empty value. 310 | */ 311 | fun addFileTemplate(fileName: String, template: String) { 312 | if (fileTemplates == null) { 313 | fileTemplates = mutableMapOf() 314 | } 315 | fileTemplates!![fileName] = template 316 | } 317 | 318 | fun addFileTemplates(placeholders: Map) { 319 | if (this.fileTemplates == null) { 320 | this.fileTemplates = mutableMapOf() 321 | } 322 | fileTemplates!!.putAll(placeholders) 323 | } 324 | 325 | /** 326 | * set current node as root. 327 | * the file of specified path must be a directory and exist. 328 | * the root's 'name' is the root path of entire file tree. 329 | * 330 | * @param path: The root directory path of whole file tree 331 | */ 332 | fun setRoot(path: String): FileTreeNode { 333 | if (!File(path).isDirectory) { 334 | throw RuntimeException("The root must be a directory.") 335 | } 336 | parent = this 337 | name = path 338 | return this 339 | } 340 | 341 | /** 342 | * traversal and create file tree, must be called from the root node. 343 | * the existing files will be skipped 344 | */ 345 | fun create() { 346 | if (!isRoot()) { 347 | throw RuntimeException("Must create structure from root node.") 348 | } 349 | createChild() 350 | } 351 | 352 | /** 353 | * traverse and call back each node of the entire file tree. 354 | */ 355 | private fun traversal(block: (it: FileTreeNode, depth: Int) -> Unit, depth: Int = 0) { 356 | if (!isDir) return 357 | 358 | realChildren.forEach { 359 | block(it, depth) 360 | it.traversal(block, depth + 1) 361 | } 362 | } 363 | 364 | /** 365 | * create directories tree from a list 366 | * the larger the index, the deeper the directory 367 | * 368 | * @param dirs The dirs list to create tree 369 | * @param parent The parent of current node 370 | */ 371 | private fun createDirs(dirs: MutableList, parent: FileTreeNode): FileTreeNode { 372 | if (dirs.isEmpty()) { 373 | return parent 374 | } 375 | // the first dir 376 | val first = dirs.first() 377 | dirs.removeAt(0) 378 | val find = parent.getChild(first, true) 379 | if (find != null) { 380 | return createDirs(dirs, find) 381 | } 382 | val newNode = FileTreeNode(parent, first, true) 383 | parent.addChild(newNode) 384 | // create child dir 385 | return createDirs(dirs, newNode) 386 | } 387 | 388 | /** 389 | * get path of current node. 390 | * if the current node is the root node, it will return absolute path, 391 | * otherwise return relative path. 392 | * 393 | * @return The intact path of current node 394 | */ 395 | fun getPath(): String { 396 | if (isRoot() || parent == null || parent!!.getRealName() == "") { 397 | return getRealName() 398 | } 399 | return parent!!.getPath() + "/" + getRealName() 400 | } 401 | 402 | fun isRoot(): Boolean { 403 | return this == parent || parent == null 404 | } 405 | 406 | /** 407 | * expand path name to dir structure 408 | */ 409 | fun expandPath() { 410 | if (!isDir) { 411 | return 412 | } 413 | if (!isRoot()) { 414 | val dir = name.split("/") 415 | .filter { 416 | it.isNotBlank() 417 | } 418 | .toMutableList() 419 | expandDirs(dir) 420 | } 421 | realChildren.forEach { 422 | it.expandPath() 423 | } 424 | } 425 | 426 | /** 427 | * expand the package name to dir structure, eg: `com.example.app` to `com/example/app` 428 | */ 429 | fun expandPkgName(replacePlaceholder: Boolean = true) { 430 | if (!isDir) { 431 | return 432 | } 433 | val pkgName2Dir = getModuleInherit()?.packageNameToDir ?: false 434 | if (!pkgName2Dir) { 435 | return 436 | } 437 | if (replacePlaceholder) { 438 | name = getRealName() 439 | } 440 | if (!isRoot()) { 441 | val dir = name 442 | .split(sPkgSplitPattern) 443 | .filter { 444 | it.isNotBlank() 445 | } 446 | .toMutableList() 447 | expandDirs(dir) 448 | } 449 | realChildren.forEach { 450 | it.expandPkgName(replacePlaceholder) 451 | } 452 | } 453 | 454 | /** 455 | * expand the dir list 456 | * all the children of this will move the new dir 457 | */ 458 | private fun expandDirs(dirs: MutableList) { 459 | if (dirs.isEmpty() || dirs.size == 1) { 460 | return 461 | } 462 | this.name = dirs.first() 463 | dirs.removeAt(0) 464 | 465 | var preNode: FileTreeNode = this 466 | var newChild: FileTreeNode 467 | 468 | val oldChildren = realChildren.toMutableList() 469 | oldChildren.forEach { 470 | it.removeFromParent() 471 | } 472 | dirs.forEach { 473 | newChild = FileTreeNode(preNode, it, true) 474 | preNode.addChild(newChild) 475 | preNode = newChild 476 | } 477 | oldChildren.forEach { 478 | preNode.addChild(it) 479 | } 480 | } 481 | 482 | /** 483 | * Return the placeholder key/replacement map of the node tree. 484 | */ 485 | fun getAllPlaceholdersMap(): Map { 486 | val result = mutableMapOf() 487 | result.putAll(placeholders.orEmpty()) 488 | traversal({ fileTreeNode: FileTreeNode, _: Int -> 489 | if (fileTreeNode.placeholders != null) { 490 | result.putAll(fileTreeNode.placeholders!!) 491 | } 492 | }) 493 | return result 494 | } 495 | 496 | /** 497 | * get the tree graph of node inherit 498 | * 499 | * @return The tree graph of node 500 | */ 501 | fun getTreeGraph( 502 | context: VelocityContext? = null, 503 | templateFile: Boolean = false, 504 | resolveName: Boolean = true, 505 | ): String { 506 | return getNodeGraph(context, templateFile = templateFile, resolveName = resolveName).toString() 507 | } 508 | 509 | /** 510 | * clone current node, the following fields will be copied: 511 | * name, isDir, fileTemplates, placeHolderMap, children 512 | * 513 | * the parent will not be cloned 514 | */ 515 | fun clone(): FileTreeNode { 516 | val clone = FileTreeNode(null, name, isDir) 517 | clone.fileTemplates = fileTemplates?.toMutableMap() 518 | clone.placeholders = placeholders?.toMutableMap() 519 | clone.module = module 520 | realChildren.forEach { 521 | clone.addChild(it.clone()) 522 | } 523 | return clone 524 | } 525 | 526 | private fun removeChild(label: String): FileTreeNode? { 527 | if (hasChild(label)) { 528 | realChildren.remove(labeledChildren[label]) 529 | return labeledChildren.remove(label) 530 | } 531 | return null 532 | } 533 | 534 | private fun getNodeGraph( 535 | context: VelocityContext?, 536 | head: Stack = Stack(), 537 | str: StringBuilder = StringBuilder(), 538 | templateFile: Boolean = false, 539 | resolveName: Boolean = true, 540 | ): StringBuilder { 541 | 542 | head.forEach { 543 | str.append(it) 544 | } 545 | str.append( 546 | when (this) { 547 | parent?.realChildren?.last() -> "└─" 548 | parent?.realChildren?.first() -> "├─" 549 | else -> { 550 | when { 551 | parent != null -> "├─" 552 | else -> "" 553 | } 554 | } 555 | } 556 | ) 557 | val n = if (resolveName) getRealName(context) else name 558 | 559 | if (isDir || !templateFile) { 560 | str.append(n) 561 | } else { 562 | str.append("$n ${getTemplateFile()}") 563 | } 564 | str.append("\n") 565 | 566 | if (realChildren.isNotEmpty()) { 567 | head.push( 568 | when { 569 | parent == null -> "" 570 | parent?.realChildren?.last() != this -> "│\t" 571 | else -> "\t" 572 | } 573 | ) 574 | realChildren.forEach { 575 | str.append(it.getNodeGraph(context, head, StringBuilder(), templateFile)) 576 | } 577 | head.pop() 578 | } 579 | return str 580 | } 581 | 582 | private fun createChild() { 583 | realChildren.forEach { 584 | val file = File(it.getPath()) 585 | if (file.exists()) { 586 | Logger.d(TAG, "${file.absolutePath} already exists.") 587 | } else { 588 | Logger.d(TAG, "create ${file.absolutePath}") 589 | if (it.isDir) { 590 | file.mkdir() 591 | } else { 592 | file.createNewFile() 593 | } 594 | } 595 | if (it.isDir) { 596 | it.createChild() 597 | } 598 | } 599 | } 600 | 601 | private fun getModuleInherit(): Module? { 602 | return module ?: parent?.getModuleInherit() 603 | } 604 | 605 | private fun getLabel(): String { 606 | return "${name}_$isDir" 607 | } 608 | 609 | override fun toString(): String { 610 | return "FileTreeNode(path='${getPath()}' isDir=$isDir, fileTemplate=${getTemplateFile()}, children=${children.size})" 611 | } 612 | 613 | protected fun String.getPlaceholder(): List { 614 | val result = mutableListOf() 615 | try { 616 | val nameMatcher = sPlaceholderPattern.matcher(this) 617 | while (nameMatcher.find()) { 618 | result.add(nameMatcher.group(1)) 619 | } 620 | } catch (e: Exception) { 621 | Logger.e("FileTreeNode", e) 622 | } 623 | return result 624 | } 625 | 626 | private fun getResolveTemplate( 627 | velocityContext: VelocityContext? = null, 628 | template: String, 629 | variables: Map?, 630 | capitalize: Boolean = false 631 | ): String { 632 | if (velocityContext != null) { 633 | val writer = StringWriter() 634 | Velocity.evaluate(velocityContext, writer, "FileTreeNode", template) 635 | return writer.toString() 636 | } 637 | 638 | var after = template 639 | if (variables.isNullOrEmpty()) { 640 | return template 641 | } 642 | variables.forEach { (k, v) -> 643 | var replacement = v 644 | if (capitalize) { 645 | replacement = v.lowercase(Locale.getDefault()) 646 | .replaceFirstChar { 647 | it.toString().uppercase() 648 | } 649 | } 650 | after = after.replace("\${$k}", replacement) 651 | } 652 | return if (after == template || !after.contains('$')) { 653 | after 654 | } else { 655 | getResolveTemplate(velocityContext, after, variables, capitalize) 656 | } 657 | } 658 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/model/Module.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.model 2 | 3 | import com.dengzii.plugin.template.template.AucTemplate 4 | import com.dengzii.plugin.template.template.Template 5 | import com.dengzii.plugin.template.utils.Logger 6 | import java.util.* 7 | 8 | class Module( 9 | var template: FileTreeNode, 10 | var language: String, 11 | var templateName: String, 12 | var lowercaseDir: Boolean = true, 13 | var capitalizeFile: Boolean = true, 14 | var packageNameToDir: Boolean = true, 15 | var enableApacheVelocity: Boolean = true, 16 | ) { 17 | 18 | companion object { 19 | 20 | fun create(template: FileTreeNode, templateName: String): Module { 21 | Logger.i(Module::class.java.simpleName, "create module. templateName=$templateName") 22 | return Module( 23 | template, 24 | "java", 25 | templateName, 26 | lowercaseDir = false, 27 | capitalizeFile = false, 28 | packageNameToDir = false 29 | ).apply { 30 | template.init(this) 31 | } 32 | } 33 | 34 | fun getAndroidApplication(): Module { 35 | return create(Template.ANDROID_APP.clone(), "AndroidApp") 36 | } 37 | 38 | fun getEmpty(): Module { 39 | return create(Template.EMPTY.clone(), "EmptyModule") 40 | } 41 | 42 | fun getAucModule(): Module { 43 | return create(AucTemplate.MODULE.clone(), "Auc Module") 44 | } 45 | 46 | fun getAucApp(): Module { 47 | return create(AucTemplate.APP.clone(), "Auc App") 48 | } 49 | 50 | fun getAucPkg(): Module { 51 | return create(AucTemplate.PKG.clone(), "Auc Pkg") 52 | } 53 | 54 | fun getAucExport(): Module { 55 | return create(AucTemplate.EXPORT.clone(), "Auc Export") 56 | } 57 | 58 | fun getAndroidMvp(): Module { 59 | return create(Template.ANDROID_MVP, "Android MVP") 60 | } 61 | 62 | fun getLangList() = Language.values().map { it.name.lowercase(Locale.getDefault()) }.toTypedArray() 63 | } 64 | 65 | enum class Language { 66 | JAVA; 67 | } 68 | 69 | fun initTemplate(node: FileTreeNode = template) { 70 | node.init(this) 71 | } 72 | 73 | fun clone(): Module { 74 | return Module(template.clone(), language, templateName, lowercaseDir, capitalizeFile, packageNameToDir) 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/template/AucTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.template 2 | 3 | import com.dengzii.plugin.template.model.FileTreeDsl 4 | 5 | /** 6 | *
  7 |  * author : dengzi
  8 |  * e-mail : dengzixx@gmail.com
  9 |  * github : https://github.com/dengzii
 10 |  * time   : 2019/1/1
 11 |  * desc   :
 12 | 
*/ 13 | object AucTemplate { 14 | 15 | private val aucFileTemplates: () -> MutableMap = { 16 | mutableMapOf( 17 | Pair("AndroidManifest.xml", "Template Manifest.xml"), 18 | Pair("build.gradle", "Template build.gradle"), 19 | Pair("MainActivity.java", "Template MainActivity.java"), 20 | Pair("\${FEATURE_NAME}App.java", "Template Application.java"), 21 | Pair("\${FEATURE_NAME}Api.java", "Template AucApiClass.java"), 22 | Pair("\${FEATURE_NAME}ApiImpl.java", "Template AucApiImplClass.java") 23 | ) 24 | } 25 | 26 | private val aucPlaceholders: () -> MutableMap = { 27 | mutableMapOf( 28 | Pair("PACKAGE_NAME", "com.example"), 29 | Pair("FEATURE_NAME", "Feature1") 30 | ) 31 | } 32 | 33 | val APP = FileTreeDsl { 34 | addFileTemplates(aucFileTemplates()) 35 | putPlaceholders(aucPlaceholders()) 36 | placeholder("MODULE_NAME", "app") 37 | 38 | dir("app") { 39 | dir("src") { 40 | dir("main") { 41 | dir("java") { 42 | dir("\${PACKAGE_NAME}") { 43 | dir("\${FEATURE_NAME}") { 44 | dir("app") { 45 | file("MainActivity.java") 46 | file("\${FEATURE_NAME}App.java") 47 | } 48 | } 49 | } 50 | } 51 | include(Template.ANDROID_RES) 52 | file("AndroidManifest.xml") 53 | } 54 | } 55 | file(".gitignore") 56 | file("build.gradle") 57 | file("proguard-rules.pro") 58 | } 59 | } 60 | 61 | val PKG = FileTreeDsl { 62 | addFileTemplates(aucFileTemplates()) 63 | putPlaceholders(aucPlaceholders()) 64 | placeholder("MODULE_NAME", "pkg") 65 | dir("pkg") { 66 | dir("src") { 67 | dir("main") { 68 | dir("java") { 69 | dir("\${PACKAGE_NAME}") { 70 | dir("\${FEATURE_NAME}") { 71 | dir("pkg") { 72 | file("\${FEATURE_NAME}ApiImpl.java") 73 | } 74 | } 75 | } 76 | } 77 | include(Template.ANDROID_RES) 78 | file("AndroidManifest.xml") 79 | } 80 | } 81 | file(".gitignore") 82 | file("build.gradle") 83 | file("proguard-rules.pro") 84 | } 85 | } 86 | 87 | val EXPORT = FileTreeDsl { 88 | addFileTemplates(aucFileTemplates()) 89 | putPlaceholders(aucPlaceholders()) 90 | placeholder("MODULE_NAME", "export") 91 | dir("export") { 92 | dir("src") { 93 | dir("main") { 94 | dir("java") { 95 | dir("\${PACKAGE_NAME}") { 96 | dir("\${FEATURE_NAME}") { 97 | dir("export") { 98 | file("\${FEATURE_NAME}Api.java") 99 | } 100 | } 101 | } 102 | } 103 | include(Template.ANDROID_RES) 104 | file("AndroidManifest.xml") 105 | } 106 | } 107 | file(".gitignore") 108 | file("build.gradle") 109 | file("proguard-rules.pro") 110 | } 111 | } 112 | 113 | val MODULE = FileTreeDsl { 114 | addFileTemplates(aucFileTemplates()) 115 | putPlaceholders(aucPlaceholders()) 116 | dir("\${FEATURE_NAME}") { 117 | 118 | include(APP { 119 | placeholders?.remove("\${FEATURE_NAME}") 120 | placeholders?.remove("\${PACKAGE_NAME}") 121 | }) 122 | include(PKG { 123 | placeholders?.remove("\${FEATURE_NAME}") 124 | placeholders?.remove("\${PACKAGE_NAME}") 125 | }) 126 | include(EXPORT { 127 | placeholders?.remove("\${FEATURE_NAME}") 128 | placeholders?.remove("\${PACKAGE_NAME}") 129 | }) 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/template/FileTemplateFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.template 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.ide.fileTemplates.FileTemplateDescriptor 5 | import com.intellij.ide.fileTemplates.FileTemplateGroupDescriptor 6 | import com.intellij.ide.fileTemplates.FileTemplateGroupDescriptorFactory 7 | import com.intellij.openapi.fileTypes.FileTypeManager 8 | import javax.swing.Icon 9 | 10 | 11 | /** 12 | *
13 |  * author : dengzi
14 |  * e-mail : dengzixx@gmail.com
15 |  * github : https://github.com/dengzii
16 |  * time   : 2020/1/2
17 |  * desc   :
18 |  * 
19 | */ 20 | class FileTemplateFactory : FileTemplateGroupDescriptorFactory { 21 | 22 | override fun getFileTemplatesDescriptor(): FileTemplateGroupDescriptor { 23 | 24 | val descriptor = FileTemplateGroupDescriptor("Module Template Plugin Descriptor", AllIcons.Nodes.Plugin) 25 | 26 | descriptor.addTemplate(getDescriptor("MainActivity.java", getFileIconByExt("java"))) 27 | descriptor.addTemplate(getDescriptor("Manifest.xml", getFileIconByExt("xml"))) 28 | descriptor.addTemplate(getDescriptor("Application.java", getFileIconByExt("java"))) 29 | descriptor.addTemplate(getDescriptor("build.gradle", getFileIconByExt("gradle"))) 30 | return descriptor 31 | } 32 | 33 | private fun getFileIconByExt(ext: String): Icon { 34 | return FileTypeManager.getInstance().getFileTypeByExtension(ext).icon ?: AllIcons.FileTypes.Unknown 35 | } 36 | 37 | private fun getDescriptor(templateName: String, icon: Icon?): FileTemplateDescriptor { 38 | return FileTemplateDescriptor(templateName, icon) 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/template/Template.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.template 2 | 3 | import com.dengzii.plugin.template.model.FileTreeDsl 4 | 5 | /** 6 | *
 7 |  * author : dengzi
 8 |  * e-mail : dengzixx@gmail.com
 9 |  * github : https://github.com/dengzii
10 |  * time   : 2020/1/14
11 |  * desc   :
12 |  * 
13 | */ 14 | object Template { 15 | 16 | val ANDROID_RES = FileTreeDsl { 17 | dir("res") { 18 | dir("drawable") { } 19 | dir("layout") { } 20 | dir("values") { } 21 | } 22 | } 23 | val ANDROID_TEST = FileTreeDsl { 24 | dir("AndroidTest") { 25 | 26 | } 27 | } 28 | 29 | val JUNIT_TEST = FileTreeDsl { 30 | dir("test") { 31 | 32 | } 33 | } 34 | 35 | val EMPTY = FileTreeDsl { 36 | 37 | } 38 | 39 | val ANDROID_MVP = FileTreeDsl { 40 | placeholder("MVP_NAME", "Example") 41 | 42 | file("\${MVP_NAME}Contract.java") 43 | file("\${MVP_NAME}View.java") 44 | file("\${MVP_NAME}Presenter.java") 45 | file("\${MVP_NAME}Model.java") 46 | } 47 | 48 | val ANDROID_APP = FileTreeDsl { 49 | placeholder("MODULE_NAME", "app") 50 | placeholder("PACKAGE_NAME", "com.example") 51 | 52 | fileTemplate("MainActivity.java", "Template MainActivity.java") 53 | fileTemplate("AndroidManifest.xml", "Template Manifest.xml") 54 | fileTemplate("build.gradle", "Template build.gradle") 55 | 56 | dir("\${MODULE_NAME}") { 57 | dir("src") { 58 | include(ANDROID_TEST) 59 | dir("main") { 60 | dir("java") { 61 | dir("\${PACKAGE_NAME}") { 62 | dir("\${MODULE_NAME}") { 63 | file("MainActivity.java") 64 | } 65 | } 66 | } 67 | include(ANDROID_RES) 68 | file("AndroidManifest.xml") 69 | } 70 | include(JUNIT_TEST) 71 | } 72 | file(".gitignore") 73 | file("build.gradle") 74 | file("ProGuard.pro") 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.notification.Notifications 6 | import com.intellij.openapi.Disposable 7 | import com.intellij.openapi.application.ApplicationManager 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.Disposer 10 | import com.intellij.util.Alarm 11 | 12 | /** 13 | *
14 |  * author : dengzi
15 |  * e-mail : dengzii@foxmail.com
16 |  * github : https://github.com/dengzii
17 |  * time   : 2020/11/18
18 |  * desc   : Utils about Notification.
19 | 
* 20 | */ 21 | object NotificationUtils { 22 | 23 | var defaultTitle = "Notification" 24 | var defaultGroupId = "Untitled_Group" 25 | 26 | fun getInfo(msg: String, title: String = defaultTitle, groupId: String = defaultGroupId): Notification { 27 | return Notification(groupId, null, NotificationType.INFORMATION).also { 28 | it.setContent(msg) 29 | it.setTitle(title) 30 | Notifications.Bus.notify(it, null) 31 | } 32 | } 33 | 34 | fun getError(msg: String, title: String = defaultTitle, groupId: String = defaultGroupId): Notification { 35 | return Notification(groupId, null, NotificationType.ERROR).also { 36 | it.setContent(msg) 37 | it.setTitle(title) 38 | it.isImportant = true 39 | Notifications.Bus.notify(it, null) 40 | } 41 | } 42 | 43 | fun getWarning(msg: String, title: String = defaultTitle, groupId: String = defaultGroupId): Notification { 44 | return Notification(groupId, null, NotificationType.WARNING).also { 45 | it.setContent(msg) 46 | it.setTitle(title) 47 | Notifications.Bus.notify(it, null) 48 | } 49 | } 50 | 51 | fun showInfo(msg: String, title: String = defaultTitle): Notification { 52 | return getInfo(msg, title).show() 53 | } 54 | 55 | fun showError(msg: String, title: String = defaultTitle): Notification { 56 | return getError(msg, title).show() 57 | } 58 | 59 | fun showWarning(msg: String, title: String = defaultTitle): Notification { 60 | return getWarning(msg, title).show() 61 | } 62 | 63 | fun Notification.show(expireMillis: Long? = null, project: Project? = null): Notification { 64 | Notifications.Bus.notify(this, project) 65 | if (expireMillis != null) { 66 | val alarm = Alarm(((project ?: ApplicationManager.getApplication()) as Disposable)) 67 | alarm.addRequest({ 68 | expire() 69 | Disposer.dispose(alarm) 70 | }, expireMillis) 71 | } 72 | return this 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/PersistentConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools 2 | 3 | import com.dengzii.plugin.template.tools.PersistentConfig.ObjectSerializer 4 | import com.google.gson.Gson 5 | import com.google.gson.JsonParseException 6 | import com.intellij.ide.util.PropertiesComponent 7 | import com.intellij.openapi.project.Project 8 | import kotlin.properties.ReadWriteProperty 9 | import kotlin.reflect.KClass 10 | import kotlin.reflect.KProperty 11 | 12 | /** 13 | * Make data persistence more easy. 14 | * 15 | * Delegate field's getter/setter to [PropertiesComponent], the complex type will serialize/deserialize 16 | * use gson by default, you can custom [ObjectSerializer] instead it. 17 | * 18 | * @param propertiesComp The [PropertiesComponent] 19 | * @param project The project use for obtain [PropertiesComponent], ignored when propertiesComp is non-null. 20 | * @param keySuffix The key suffix use for persist. 21 | * @param objectSerializer The serialize/deserialize factory for complex type. 22 | * 23 | * @author https://github.com/dengzii 24 | */ 25 | open class PersistentConfig( 26 | propertiesComp: PropertiesComponent? = null, 27 | project: Project? = null, 28 | private val keySuffix: String = "KEY", 29 | private val objectSerializer: ObjectSerializer = JsonObjectSerializer() 30 | ) { 31 | 32 | private val propertiesComponent: PropertiesComponent = propertiesComp ?: if (project == null) { 33 | PropertiesComponent.getInstance() 34 | } else { 35 | PropertiesComponent.getInstance(project) 36 | } 37 | 38 | /** 39 | * Return the delegate for field need to persist. 40 | * 41 | * @param defaultValue The default when load value failed. 42 | * @param keyName The key name use for persist. 43 | * 44 | * @return [PropertyDelegate] 45 | */ 46 | inline fun persistentProperty(defaultValue: T, keyName: String? = null): PropertyDelegate { 47 | return when (defaultValue) { 48 | is Int?, 49 | is Boolean?, 50 | is Float?, 51 | is String?, 52 | is Array<*>? -> { 53 | PropertyDelegate(defaultValue, T::class, keyName) 54 | } 55 | else -> PropertyDelegate(defaultValue, T::class, keyName) 56 | } 57 | } 58 | 59 | /** 60 | * The interface defines how to serializer/deserializer the object not primitive type. 61 | */ 62 | interface ObjectSerializer { 63 | fun serialize(obj: Any, clazz: KClass<*>): String 64 | fun deserialize(str: String, clazz: KClass<*>): Any 65 | } 66 | 67 | /** 68 | * The json serializer/deserializer use gson. 69 | */ 70 | class JsonObjectSerializer : ObjectSerializer { 71 | 72 | private val gson: Gson = Gson().newBuilder() 73 | .setLenient() 74 | .serializeNulls() 75 | .create() 76 | 77 | @Throws(JsonParseException::class) 78 | override fun serialize(obj: Any, clazz: KClass<*>): String { 79 | return gson.toJson(obj) 80 | } 81 | 82 | @Throws(JsonParseException::class) 83 | override fun deserialize(str: String, clazz: KClass<*>): Any { 84 | return gson.fromJson(str, clazz.java) 85 | } 86 | } 87 | 88 | /** 89 | * This class is a delegate class for a field need to persist. 90 | * 91 | * @param default The default when read property failed, the following situation will return [default]: 92 | * 1, The key does not exist. 93 | * 2, Read value successful but serialize/deserialize failed. 94 | * 3, An exception was caught. 95 | * @param clazz The KClass of field. 96 | * @param keyName The key name, the field name is used when null. 97 | */ 98 | inner class PropertyDelegate 99 | constructor( 100 | private val default: T, 101 | private val clazz: KClass<*>, 102 | private val keyName: String? = null 103 | ) : ReadWriteProperty { 104 | 105 | @Suppress("UNCHECKED_CAST") 106 | override fun getValue(thisRef: PersistentConfig, property: KProperty<*>): T? { 107 | val keyName = getKeyName(property) 108 | return try { 109 | with(thisRef.propertiesComponent) { 110 | when (clazz) { 111 | Int::class -> getInt(keyName, default as Int) 112 | Boolean::class -> getBoolean(keyName, default as Boolean) 113 | String::class -> getValue(keyName, default as String) 114 | Float::class -> getFloat(keyName, default as Float) 115 | Array::class -> getValues(keyName) 116 | // deserialize to object 117 | else -> { 118 | val v = getValue(keyName) 119 | if (v != null) { 120 | objectSerializer.deserialize(v, clazz) 121 | } else { 122 | default 123 | } 124 | } 125 | } 126 | } as T 127 | } catch (ignore: TypeCastException) { 128 | default 129 | } catch (e: Throwable) { 130 | e.printStackTrace() 131 | default 132 | } 133 | } 134 | 135 | override fun setValue(thisRef: PersistentConfig, property: KProperty<*>, value: T?) { 136 | val keyName = getKeyName(property) 137 | with(thisRef.propertiesComponent) { 138 | when (value) { 139 | is Int -> setValue(keyName, value, default as Int) 140 | is Boolean -> setValue(keyName, value, default as Boolean) 141 | is String -> setValue(keyName, value, default as String) 142 | is Float -> setValue(keyName, value, default as Float) 143 | is Array<*> -> { 144 | val arr = value.filterIsInstance().toTypedArray() 145 | setValues(keyName, arr) 146 | } 147 | null -> unsetValue(keyName) 148 | else -> { 149 | try { 150 | val serialized = objectSerializer.serialize(value, clazz) 151 | setValue(keyName, serialized) 152 | } catch (e: Throwable) { 153 | throw RuntimeException("Type unsupported.", e) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | private fun getKeyName(property: KProperty<*>): String { 161 | return "${keySuffix}_${keyName ?: property.name}" 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/ActionToolBarUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.impl.ActionButton 6 | import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl 7 | import com.intellij.openapi.actionSystem.impl.PresentationFactory 8 | import com.intellij.util.ui.JBUI 9 | import java.awt.Dimension 10 | import java.awt.FlowLayout 11 | import javax.swing.Icon 12 | import javax.swing.JComponent 13 | import javax.swing.JPanel 14 | 15 | object ActionToolBarUtils { 16 | 17 | fun create(place: String, action: List): JComponent { 18 | 19 | val panel = JPanel() 20 | panel.layout = FlowLayout().also { 21 | it.alignment = FlowLayout.LEFT 22 | it.hgap = 0 23 | it.vgap = 0 24 | } 25 | val factory = PresentationFactory() 26 | action.forEach { 27 | val presentation = factory.getPresentation(it).also { p -> 28 | p.icon = it.icon 29 | p.isEnabled = it.isEnabled 30 | p.description = it.desc 31 | } 32 | val bt = ActionButton(it, presentation, place, Dimension(22, 24)) 33 | bt.setIconInsets(JBUI.insets(0)) 34 | panel.add(bt) 35 | } 36 | return panel 37 | } 38 | 39 | class Action(var icon: Icon, 40 | var isEnabled: Boolean = true, 41 | var desc: String = "", 42 | var action: () -> Unit) : AnAction() { 43 | 44 | override fun actionPerformed(p0: AnActionEvent) { 45 | action.invoke() 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/ColumnInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import com.intellij.ui.components.JBLabel 4 | import com.intellij.ui.components.JBTextField 5 | import java.awt.Component 6 | 7 | 8 | /** 9 | * The model of the column of [JTable]. 10 | * 11 | * @author https://github.com/dengzii 12 | */ 13 | open class ColumnInfo(val colName: String) { 14 | 15 | private var editable: Boolean = false 16 | 17 | open val columnClass: Class<*> get() = String::class.java 18 | 19 | open var columnWidth: Int? = null 20 | 21 | constructor(colName: String, editable: Boolean) : this(colName) { 22 | this.editable = editable 23 | } 24 | 25 | companion object { 26 | fun of(vararg columns: String): List> { 27 | return columns.map { ColumnInfo(it) } 28 | } 29 | } 30 | 31 | /** 32 | * Return the value of column when edit finish, if this column is editable. 33 | * 34 | * @param component The edit component. 35 | * @param oldValue The old value before edit. 36 | * @return A new value. 37 | */ 38 | open fun getEditorValue(component: Component, oldValue: Item?, row: Int, col: Int): Item? { 39 | return null 40 | } 41 | 42 | /** 43 | * Whether this column editable. 44 | * @param item The item value. 45 | * @return True is editable, otherwise not. 46 | */ 47 | open fun isCellEditable(item: Item?): Boolean { 48 | return editable 49 | } 50 | 51 | /** 52 | * Return a [Component] use for display item value. 53 | * @param item The item value. 54 | * @return The component. 55 | */ 56 | open fun getRendererComponent(item: Item?, row: Int, col: Int): Component { 57 | return JBLabel(item?.toString().orEmpty()) 58 | } 59 | 60 | /** 61 | * Return a [Component] use for edit this column. 62 | * 63 | * Working just when [isCellEditable] returns true. 64 | * 65 | * @param item The item value. 66 | * @return The component. 67 | */ 68 | open fun getEditComponent(item: Item?, row: Int, col: Int): Component { 69 | return JBTextField(item?.toString().orEmpty()) 70 | } 71 | 72 | override fun equals(other: Any?): Boolean { 73 | return if (this === other) { 74 | true 75 | } else if (other != null && this.javaClass == other.javaClass) { 76 | val that = other as ColumnInfo<*> 77 | colName == that.colName 78 | } else { 79 | false 80 | } 81 | } 82 | 83 | override fun hashCode(): Int { 84 | return colName.hashCode() 85 | } 86 | 87 | override fun toString(): String { 88 | return colName 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/ComponentExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import java.awt.Component 4 | import java.awt.event.MouseAdapter 5 | import java.awt.event.MouseEvent 6 | 7 | 8 | /** 9 | * Add right mouse button clicked listener to component. 10 | * 11 | * @param action The click event callback. 12 | */ 13 | inline fun Component.onRightMouseButtonClicked(crossinline action: (MouseEvent) -> Unit) { 14 | onMouseButtonClicked(MouseEvent.BUTTON3, action) 15 | } 16 | 17 | 18 | /** 19 | * Add mouse pressed listener. 20 | * 21 | * @param action The click event callback. 22 | */ 23 | inline fun Component.onClick(crossinline action: (MouseEvent?) -> Unit){ 24 | addMouseListener(object : MouseAdapter() { 25 | override fun mousePressed(e: MouseEvent?) { 26 | super.mousePressed(e) 27 | action(e) 28 | } 29 | }) 30 | } 31 | 32 | /** 33 | * Add a listener to component when specified [button] clicked. 34 | * 35 | * @param button The mouse button code. 36 | * @param action The click event callback. 37 | */ 38 | inline fun Component.onMouseButtonClicked(button: Int, crossinline action: (MouseEvent) -> Unit) { 39 | addMouseListener(object : MouseAdapter() { 40 | override fun mouseClicked(e: MouseEvent?) { 41 | super.mouseClicked(e) 42 | if (e?.button == button) { 43 | action(e) 44 | } 45 | } 46 | }) 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/PopMenuUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import com.intellij.openapi.ui.JBMenuItem 4 | import com.intellij.openapi.ui.JBPopupMenu 5 | import java.awt.Component 6 | import java.awt.event.MouseEvent 7 | 8 | /** 9 | *
10 |  * author : dengzi
11 |  * e-mail : dengzii@foxmail.com
12 |  * github : https://github.com/dengzii
13 |  * time   : 2020/1/16
14 |  * desc   :
15 |  * 
16 | */ 17 | object PopMenuUtils { 18 | 19 | /** 20 | * Return a [JBPopupMenu] instance with [menu]. 21 | * 22 | * @param menu The menu item and click event callback pair. 23 | */ 24 | fun create(menu: Map Unit?>): JBPopupMenu { 25 | 26 | val popMenu = JBPopupMenu() 27 | menu.forEach { (k, v) -> 28 | if (k.isBlank()) { 29 | popMenu.addSeparator() 30 | return@forEach 31 | } 32 | val item = JBMenuItem(k) 33 | item.addActionListener { 34 | v.invoke() 35 | } 36 | popMenu.add(item) 37 | } 38 | return popMenu 39 | } 40 | 41 | /** 42 | * Show a [JBPopupMenu] that location depends on [event]'s x, y. 43 | * 44 | * @param event The mouse event. 45 | * @param menu The menu item and click event callback pair. 46 | */ 47 | fun show(event: MouseEvent, menu: Map Unit?>) { 48 | create(menu).show(event.source as Component, event.x, event.y) 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/TableAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import com.intellij.ui.components.JBTextField 4 | import com.intellij.util.ui.AbstractTableCellEditor 5 | import java.awt.Component 6 | import javax.swing.JTable 7 | import javax.swing.table.AbstractTableModel 8 | import javax.swing.table.TableCellRenderer 9 | import javax.swing.table.TableColumn 10 | import javax.swing.table.TableColumnModel 11 | 12 | /** 13 | * ## The JTable Adapter 14 | * 15 | * This is an adapter use for auto manager JTable data model. 16 | * 17 | * @param tableData The table data of each column and each row, you should ensure each row has the same size. 18 | * @param columnInfo The column model express how to display and edit. 19 | * 20 | * @author https://github.com/dengzii 21 | */ 22 | open class TableAdapter(private val tableData: MutableList>, 23 | private val columnInfo: MutableList>) : AbstractTableModel() { 24 | 25 | private lateinit var jTable: JTable 26 | 27 | // the map of table column header value and ColumnInfo 28 | // using for find ColumnInfo when don't know column index. 29 | private var columnInfoMap = mutableMapOf>() 30 | 31 | // the table cell adapter 32 | var cellAdapter: CellAdapter = DefaultCellAdapter() 33 | 34 | fun setup(table: JTable) { 35 | jTable = table 36 | table.model = this 37 | table.columnModel = DelegateTableColumnModel(table.columnModel) 38 | } 39 | 40 | fun eachColumn(action: (TableColumn, Int) -> Unit) { 41 | for (i in 0 until jTable.columnModel.columnCount) { 42 | action(jTable.columnModel.getColumn(i), i) 43 | } 44 | } 45 | 46 | /** 47 | * Wrapper of JTable's default column model. 48 | * 49 | * Using for custom column renderer and editor. 50 | */ 51 | inner class DelegateTableColumnModel(columnModel: TableColumnModel) : TableColumnModelDecorator(columnModel) { 52 | 53 | override fun getColumn(columnIndex: Int): TableColumn { 54 | val column = super.getColumn(columnIndex) 55 | column.cellRenderer = cellAdapter 56 | column.cellEditor = cellAdapter 57 | columnInfoMap[column.headerValue]?.columnWidth?.takeIf { 58 | it > 0 59 | }?.let { 60 | column.preferredWidth = it 61 | } 62 | return column 63 | } 64 | } 65 | 66 | abstract class CellAdapter : AbstractTableCellEditor(), TableCellRenderer 67 | 68 | /** 69 | * Definition how cell render and edit. 70 | */ 71 | inner class DefaultCellAdapter : CellAdapter() { 72 | 73 | private lateinit var editorComponent: Component 74 | private lateinit var rendererComponent: Component 75 | private var value: Any? = null 76 | private var editLocation = Pair(-1, -1) 77 | 78 | override fun getCellEditorValue(): Any? { 79 | val v = columnInfo[editLocation.second].getEditorValue( 80 | editorComponent, value, editLocation.first, editLocation.second) 81 | return v ?: (editorComponent as? JBTextField)?.text ?: value 82 | } 83 | 84 | override fun getTableCellEditorComponent(table: JTable?, value: Any?, isSelected: Boolean, 85 | row: Int, column: Int): Component? { 86 | // temp the old value before edit cell 87 | this.value = value 88 | editLocation = Pair(row, column) 89 | val header = jTable.columnModel.getColumn(column).headerValue 90 | // get the edit component from ColumnInfo 91 | editorComponent = columnInfoMap[header]?.getEditComponent(value, row, column) ?: return null 92 | return editorComponent 93 | } 94 | 95 | override fun getTableCellRendererComponent(table: JTable?, value: Any?, isSelected: Boolean, 96 | hasFocus: Boolean, row: Int, column: Int): Component? { 97 | val header = jTable.columnModel.getColumn(column).headerValue 98 | rendererComponent = columnInfoMap[header]?.getRendererComponent(value, row, column) ?: return null 99 | return rendererComponent 100 | } 101 | } 102 | 103 | override fun fireTableStructureChanged() { 104 | // when table structure changed, the columns may changed. 105 | columnInfoMap.clear() 106 | columnInfo.forEach { 107 | columnInfoMap[it.colName] = it 108 | } 109 | super.fireTableStructureChanged() 110 | } 111 | 112 | override fun getColumnClass(columnIndex: Int): Class<*> { 113 | return columnInfo[columnIndex].columnClass 114 | } 115 | 116 | override fun getColumnName(column: Int): String { 117 | return columnInfo[column].colName 118 | } 119 | 120 | override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean { 121 | return columnInfo[columnIndex].isCellEditable(tableData[rowIndex].getOrNull(columnIndex)) 122 | } 123 | 124 | override fun setValueAt(aValue: Any?, rowIndex: Int, columnIndex: Int) { 125 | tableData[rowIndex][columnIndex] = aValue 126 | } 127 | 128 | override fun getRowCount(): Int { 129 | return tableData.size 130 | } 131 | 132 | override fun getColumnCount(): Int { 133 | return columnInfo.size 134 | } 135 | 136 | override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? { 137 | return tableData[rowIndex].getOrNull(columnIndex) 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/TableColumnModelDecorator.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import java.util.* 4 | import javax.swing.ListSelectionModel 5 | import javax.swing.event.TableColumnModelListener 6 | import javax.swing.table.TableColumn 7 | import javax.swing.table.TableColumnModel 8 | 9 | open class TableColumnModelDecorator(private val wrapper: TableColumnModel) : TableColumnModel { 10 | 11 | override fun addColumn(aColumn: TableColumn?) { 12 | wrapper.addColumn(aColumn) 13 | } 14 | 15 | override fun removeColumn(column: TableColumn?) { 16 | wrapper.removeColumn(column) 17 | } 18 | 19 | override fun moveColumn(columnIndex: Int, newIndex: Int) { 20 | wrapper.moveColumn(columnIndex, newIndex) 21 | } 22 | 23 | override fun setColumnMargin(newMargin: Int) { 24 | wrapper.columnMargin = newMargin 25 | } 26 | 27 | override fun getColumnCount(): Int { 28 | return wrapper.columnCount 29 | } 30 | 31 | override fun getColumns(): Enumeration { 32 | return wrapper.columns 33 | } 34 | 35 | override fun getColumnIndex(columnIdentifier: Any?): Int { 36 | return wrapper.getColumnIndex(columnIdentifier) 37 | } 38 | 39 | override fun getColumn(columnIndex: Int): TableColumn { 40 | return wrapper.getColumn(columnIndex) 41 | } 42 | 43 | override fun getColumnMargin(): Int { 44 | return wrapper.columnMargin 45 | } 46 | 47 | override fun getColumnIndexAtX(xPosition: Int): Int { 48 | return wrapper.getColumnIndexAtX(xPosition) 49 | } 50 | 51 | override fun getTotalColumnWidth(): Int { 52 | return wrapper.totalColumnWidth 53 | } 54 | 55 | override fun setColumnSelectionAllowed(flag: Boolean) { 56 | wrapper.columnSelectionAllowed = flag 57 | } 58 | 59 | override fun getColumnSelectionAllowed(): Boolean { 60 | return wrapper.columnSelectionAllowed 61 | } 62 | 63 | override fun getSelectedColumns(): IntArray { 64 | return wrapper.selectedColumns 65 | } 66 | 67 | override fun getSelectedColumnCount(): Int { 68 | return wrapper.selectedColumnCount 69 | } 70 | 71 | override fun setSelectionModel(newModel: ListSelectionModel?) { 72 | wrapper.selectionModel = newModel 73 | } 74 | 75 | override fun getSelectionModel(): ListSelectionModel { 76 | return wrapper.selectionModel 77 | } 78 | 79 | override fun addColumnModelListener(x: TableColumnModelListener?) { 80 | wrapper.addColumnModelListener(x) 81 | } 82 | 83 | override fun removeColumnModelListener(x: TableColumnModelListener?) { 84 | wrapper.removeColumnModelListener(x) 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/XDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.util.WindowStateService 5 | import java.awt.Point 6 | import java.awt.Rectangle 7 | import java.awt.Toolkit 8 | import java.awt.event.KeyEvent 9 | import java.awt.event.WindowAdapter 10 | import java.awt.event.WindowEvent 11 | import javax.swing.JComponent 12 | import javax.swing.JDialog 13 | import javax.swing.JPanel 14 | import javax.swing.KeyStroke 15 | 16 | /** 17 | * The class implements some frequently-used feature of [JDialog]. 18 | * 19 | * - Add dialog lifecycle callback, like opened, closed etc. 20 | * - Default be modal. 21 | * - Default dispose on dialog close. 22 | * - Persist the size, location, and restore it the next time opened. 23 | * 24 | * @author https://github.com/dengzii 25 | */ 26 | abstract class XDialog() : JDialog() { 27 | 28 | var persistDialogState = true 29 | var project: Project? = null 30 | 31 | private val keyWindowStatePersist = this::class.java.name 32 | 33 | constructor(title: String) : this() { 34 | this.title = title 35 | } 36 | 37 | init { 38 | isModal = true 39 | defaultCloseOperation = DISPOSE_ON_CLOSE 40 | addWindowListener(object : WindowAdapter() { 41 | 42 | override fun windowOpened(e: WindowEvent?) { 43 | onOpened() 44 | (contentPane as? JPanel)?.registerKeyboardAction({ 45 | onClosing() 46 | hideAndDispose() 47 | }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 48 | super.windowOpened(e) 49 | } 50 | 51 | override fun windowClosing(e: WindowEvent) { 52 | onClosing() 53 | } 54 | 55 | override fun windowClosed(e: WindowEvent?) { 56 | super.windowClosed(e) 57 | onClosed() 58 | } 59 | }) 60 | } 61 | 62 | fun getLocationCenterOfScreen(): Point { 63 | val screen = toolkit.screenSize 64 | val x = screen.width / 2 - width / 2 65 | val y = screen.height / 2 - height /2 66 | return Point(x, y) 67 | } 68 | 69 | fun packAndShow() { 70 | pack() 71 | isVisible = true 72 | } 73 | 74 | fun hideAndDispose() { 75 | isVisible = false 76 | dispose() 77 | } 78 | 79 | open fun onOpened() { 80 | if (persistDialogState) { 81 | restoreState() 82 | } 83 | } 84 | 85 | 86 | open fun onClosing() { 87 | if (persistDialogState) { 88 | persistState() 89 | } 90 | } 91 | 92 | 93 | open fun onClosed() { 94 | 95 | } 96 | 97 | /** 98 | * Doing some work about persistent. 99 | */ 100 | open fun persistState() { 101 | WindowStateService.getInstance().putBounds(keyWindowStatePersist, Rectangle(x, y, width, height)) 102 | } 103 | 104 | /** 105 | * Restore the state of dialog. 106 | */ 107 | open fun restoreState() { 108 | val bounds = WindowStateService.getInstance().getBounds(keyWindowStatePersist) 109 | ?: Rectangle().apply { 110 | val screenSize = Toolkit.getDefaultToolkit().screenSize 111 | height = 300 112 | width = 500 113 | y = screenSize.height / 2 - height 114 | x = screenSize.width / 2 - width / 2 115 | } 116 | setBounds(bounds) 117 | } 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/XMenu.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import java.awt.event.MouseAdapter 4 | import java.awt.event.MouseEvent 5 | import javax.swing.JMenu 6 | import javax.swing.JMenuItem 7 | 8 | /** 9 | *
10 |  * author : dengzi
11 |  * e-mail : dengzii@foxmail.com
12 |  * github : https://github.com/dengzii
13 |  * time   : 2019/11/23
14 |  * desc   :
15 |  * 
16 | */ 17 | class XMenu(name: String?, private val block: XMenu.() -> Unit) : JMenu(name) { 18 | 19 | constructor(name: String?) : this(name, {}) 20 | 21 | operator fun invoke(): XMenu { 22 | block(this) 23 | return this 24 | } 25 | 26 | fun addItem(title: String?, onItemClick: OnItemClick) { 27 | val menuItem = JMenuItem(title) 28 | menuItem.onClick { 29 | onItemClick.onItemClick() 30 | } 31 | add(menuItem) 32 | } 33 | 34 | fun item(title: String, onClick: () -> Unit) { 35 | addItem(title, object : OnItemClick { 36 | override fun onItemClick() { 37 | onClick() 38 | } 39 | }) 40 | } 41 | 42 | interface OnItemClick { 43 | fun onItemClick() 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/tools/ui/XMenuBar.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.tools.ui 2 | 3 | import javax.swing.JMenuBar 4 | 5 | class XMenuBar(private val block: XMenuBar.() -> Unit) : JMenuBar() { 6 | 7 | init { 8 | block.invoke(this) 9 | } 10 | 11 | operator fun invoke(): XMenuBar { 12 | block.invoke(this) 13 | return this 14 | } 15 | 16 | fun menu(title: String, block: XMenu.() -> Unit) { 17 | val menu = XMenu(title) 18 | block(menu) 19 | add(menu) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/ui/EditToolbar.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.ui 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.actionSystem.ActionGroup 5 | import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl 6 | import java.awt.Dimension 7 | import java.awt.FlowLayout 8 | import java.awt.event.ActionEvent 9 | import java.awt.event.MouseAdapter 10 | import java.awt.event.MouseEvent 11 | import javax.swing.Icon 12 | import javax.swing.JButton 13 | import javax.swing.JPanel 14 | 15 | /** 16 | *
17 |  * author : dengzi
18 |  * e-mail : dengzixx@gmail.com
19 |  * github : https://github.com/dengzii
20 |  * time   : 2020/1/14
21 |  * desc   :
22 |  * 
23 | */ 24 | class EditToolbar : JPanel() { 25 | 26 | private val btAdd = JButton("Add") 27 | private val btRemove = JButton("Remove") 28 | private val btCopy = JButton("Copy") 29 | private val btShare = JButton("Export") 30 | 31 | init { 32 | initLayout() 33 | initButton() 34 | } 35 | 36 | fun onAdd(listener: MouseAdapter) { 37 | btAdd.addMouseListener(listener) 38 | } 39 | 40 | fun onAdd(listener: (MouseEvent?) -> Unit) { 41 | btAdd.addMouseListener(object :MouseAdapter(){ 42 | override fun mouseClicked(e: MouseEvent?) { 43 | super.mouseClicked(e) 44 | listener(e) 45 | } 46 | }) 47 | } 48 | 49 | fun onRemove(listener: () -> Unit) { 50 | btRemove.addActionListener { 51 | listener() 52 | } 53 | } 54 | 55 | fun onCopy(listener: () -> Unit) { 56 | btCopy.addActionListener { 57 | listener() 58 | } 59 | } 60 | 61 | fun onExport(listener: () -> Unit) { 62 | btShare.addActionListener { 63 | listener() 64 | } 65 | } 66 | 67 | private fun initLayout() { 68 | 69 | val flowLayout = FlowLayout() 70 | flowLayout.vgap = 0 71 | flowLayout.hgap = 5 72 | flowLayout.alignment = FlowLayout.LEFT 73 | layout = flowLayout 74 | } 75 | 76 | private fun initButton() { 77 | setIconButton(btAdd, AllIcons.General.Add) 78 | setIconButton(btRemove, AllIcons.General.Remove) 79 | setIconButton(btCopy, AllIcons.General.CopyHovered) 80 | setIconButton(btShare, AllIcons.Actions.Download) 81 | } 82 | 83 | private fun setIconButton(button: JButton, icon: Icon) { 84 | add(button) 85 | button.toolTipText = button.text 86 | button.icon = icon 87 | button.text = "" 88 | button.preferredSize = Dimension(25, 25) 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/ui/EditableTable.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.ui 2 | 3 | import com.dengzii.plugin.template.tools.ui.ActionToolBarUtils 4 | import com.intellij.icons.AllIcons 5 | import com.intellij.ui.components.JBScrollPane 6 | import com.intellij.ui.table.JBTable 7 | import java.awt.BorderLayout 8 | import javax.swing.JComponent 9 | import javax.swing.JPanel 10 | import javax.swing.event.TableModelEvent 11 | import javax.swing.table.DefaultTableModel 12 | 13 | /** 14 | *
 15 |  * author : dengzi
 16 |  * e-mail : dengzixx@gmail.com
 17 |  * github : https://github.com/dengzii
 18 |  * time   : 2020/1/14
 19 |  * desc   :
 20 |  * 
21 | */ 22 | class EditableTable(header: Array, colEditable: Array = emptyArray()) : JPanel() { 23 | 24 | private val scrollPanel = JBScrollPane() 25 | private val editToolbar: JComponent 26 | private val table = JBTable() 27 | private var tableModel = TableModel(header, colEditable) 28 | 29 | init { 30 | layout = BorderLayout() 31 | editToolbar = ActionToolBarUtils.create("Edit1", listOf( 32 | ActionToolBarUtils.Action(AllIcons.General.Add) { 33 | tableModel.add() 34 | table.updateUI() 35 | }, 36 | ActionToolBarUtils.Action(AllIcons.General.Remove) { 37 | if (table.selectedRow == -1) { 38 | return@Action 39 | } 40 | tableModel.remove(table.selectedRow) 41 | table.updateUI() 42 | }, 43 | ActionToolBarUtils.Action(AllIcons.General.CopyHovered) { 44 | if (table.selectedRow == -1) { 45 | return@Action 46 | } 47 | tableModel.copy(table.selectedRow) 48 | table.updateUI() 49 | } 50 | )) 51 | add(editToolbar, BorderLayout.NORTH) 52 | scrollPanel.setViewportView(table) 53 | add(scrollPanel, BorderLayout.CENTER) 54 | table.fillsViewportHeight = true 55 | table.showHorizontalLines = true 56 | table.showVerticalLines = true 57 | table.model = tableModel 58 | table.putClientProperty("terminateEditOnFocusLost", true) 59 | } 60 | 61 | fun addChangeListener(listener: (TableModelEvent) -> Unit) { 62 | tableModel.addTableModelListener { 63 | listener.invoke(it) 64 | } 65 | } 66 | 67 | fun setToolBarVisible(visible: Boolean) { 68 | editToolbar.isVisible = visible 69 | } 70 | 71 | fun setData(data: MutableList>) { 72 | tableModel.setData(data) 73 | tableModel.fireTableDataChanged() 74 | table.updateUI() 75 | } 76 | 77 | fun setPairData(data: Map?) { 78 | val dataList = mutableListOf>() 79 | data?.forEach { (t, u) -> 80 | dataList.add(mutableListOf(t, u)) 81 | } 82 | tableModel.setData(dataList) 83 | tableModel.fireTableDataChanged() 84 | table.updateUI() 85 | } 86 | 87 | fun stopEdit() { 88 | if (table.isEditing) { 89 | table.cellEditor.stopCellEditing() 90 | } 91 | } 92 | 93 | fun getPairResult(): MutableMap { 94 | val result = mutableMapOf() 95 | for (i in 0 until table.rowCount) { 96 | val key = table.getValueAt(i, 0).toString() 97 | val value = table.getValueAt(i, 1).toString() 98 | if (key.isBlank()) { 99 | continue 100 | } 101 | result[key] = value 102 | } 103 | return result 104 | } 105 | 106 | internal class TableModel(private val header: Array, private val colEditable: Array) : 107 | DefaultTableModel() { 108 | 109 | fun setData(fileTemp: MutableList>?) { 110 | while (rowCount > 0) { 111 | removeRow(0) 112 | } 113 | if (fileTemp == null) { 114 | return 115 | } 116 | fileTemp.forEach { 117 | addRow(it.toTypedArray()) 118 | } 119 | } 120 | 121 | fun add() { 122 | addRow(Array(header.size) { "" }) 123 | fireTableDataChanged() 124 | } 125 | 126 | fun remove(row: Int) { 127 | removeRow(row) 128 | fireTableDataChanged() 129 | } 130 | 131 | fun copy(row: Int) { 132 | val r = mutableListOf() 133 | for (c in 0 until columnCount) { 134 | r.add(getValueAt(row, c).toString()) 135 | } 136 | addRow(r.toTypedArray()) 137 | fireTableDataChanged() 138 | } 139 | 140 | override fun isCellEditable(row: Int, column: Int): Boolean { 141 | return if (colEditable.isEmpty()) super.isCellEditable(row, column) else colEditable[column] 142 | } 143 | 144 | override fun getColumnClass(columnIndex: Int): Class<*> { 145 | return String::class.java 146 | } 147 | 148 | override fun getColumnCount(): Int { 149 | return header.size 150 | } 151 | 152 | override fun getColumnName(column: Int): String { 153 | return if (header.isEmpty()) super.getColumnName(column) else header[column] 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/ui/PreviewPanel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.ui 2 | 3 | import com.dengzii.plugin.template.model.FileTreeNode 4 | import com.dengzii.plugin.template.model.Module 5 | import com.dengzii.plugin.template.tools.ui.PopMenuUtils 6 | import com.dengzii.plugin.template.tools.ui.onRightMouseButtonClicked 7 | import com.dengzii.plugin.template.utils.Logger 8 | import com.dengzii.plugin.ui.FileDialog 9 | import com.intellij.icons.AllIcons 10 | import com.intellij.openapi.fileTypes.FileTypeManager 11 | import com.intellij.packageDependencies.ui.TreeModel 12 | import com.intellij.ui.ColoredTreeCellRenderer 13 | import com.intellij.ui.components.JBCheckBox 14 | import com.intellij.ui.components.JBScrollPane 15 | import com.intellij.ui.treeStructure.Tree 16 | import org.apache.velocity.VelocityContext 17 | import java.awt.BorderLayout 18 | import java.awt.event.KeyAdapter 19 | import java.awt.event.KeyEvent 20 | import java.awt.event.MouseEvent 21 | import java.util.function.Consumer 22 | import javax.swing.Icon 23 | import javax.swing.JPanel 24 | import javax.swing.JTree 25 | import javax.swing.tree.DefaultMutableTreeNode 26 | import javax.swing.tree.TreeNode 27 | import javax.swing.tree.TreePath 28 | 29 | class PreviewPanel(preview: Boolean) : JPanel() { 30 | 31 | private val fileTree: Tree = Tree() 32 | private val showPlaceholder: JBCheckBox = JBCheckBox("Show placeholders") 33 | 34 | private lateinit var module: Module 35 | private var replacePlaceholder = true 36 | private var onTreeUpdateListener: (() -> Unit)? = null 37 | private var onPlaceholderUpdateListener: (() -> Unit)? = null 38 | 39 | private var context: VelocityContext = VelocityContext().apply { 40 | put("StringUtils", org.apache.velocity.util.StringUtils::class.java) 41 | } 42 | 43 | // render tree node icon, title 44 | private val treeCellRenderer = object : ColoredTreeCellRenderer() { 45 | override fun customizeCellRenderer( 46 | jTree: JTree, value: Any, b: Boolean, b1: Boolean, b2: Boolean, i: Int, b3: Boolean 47 | ) { 48 | if (value is DefaultMutableTreeNode) { 49 | val node = value.userObject 50 | if (node is FileTreeNode) { 51 | val name = if (replacePlaceholder) { 52 | node.getRealName(context) 53 | } else { 54 | node.name 55 | } 56 | this.append(name) 57 | icon = if (node.isDir) { 58 | AllIcons.Nodes.Package 59 | } else { 60 | getIconByFileName(name) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | init { 68 | layout = BorderLayout() 69 | add(JBScrollPane().apply { 70 | setViewportView(fileTree) 71 | }, BorderLayout.CENTER) 72 | showPlaceholder.isSelected = !replacePlaceholder 73 | if (preview) { 74 | add(showPlaceholder, BorderLayout.NORTH) 75 | showPlaceholder.addChangeListener { 76 | replacePlaceholder = !showPlaceholder.isSelected 77 | setModuleConfig(module) 78 | } 79 | } 80 | initPanel() 81 | } 82 | 83 | fun setPreviewMode(preview: Boolean) { 84 | if (preview != replacePlaceholder) { 85 | replacePlaceholder = preview 86 | showPlaceholder.isSelected = !replacePlaceholder 87 | fileTree.updateUI() 88 | } 89 | } 90 | 91 | fun onPlaceholderUpdate(l: (() -> Unit)?) { 92 | onPlaceholderUpdateListener = l 93 | } 94 | 95 | fun updateTree() { 96 | fileTree.updateUI() 97 | } 98 | 99 | fun setModuleConfig(module: Module) { 100 | Logger.i("PreviewPanel", "setModuleConfig") 101 | this.module = module 102 | fileTree.model = getTreeModel(this.module.template) 103 | context.apply { 104 | put("StringUtils", org.apache.velocity.util.StringUtils::class.java) 105 | module.template.getPlaceholderInherit()?.forEach { 106 | put(it.key, it.value) 107 | } 108 | } 109 | fileTree.doLayout() 110 | fileTree.updateUI() 111 | expandAll(fileTree, TreePath(fileTree.model.root), true) 112 | } 113 | 114 | /** 115 | * Set the callback of tree edit, delete, add event. 116 | */ 117 | fun setOnTreeUpdateListener(listener: (() -> Unit)?) { 118 | onTreeUpdateListener = listener 119 | } 120 | 121 | private fun getIconByFileName(fileName: String): Icon { 122 | return FileTypeManager.getInstance().getFileTypeByExtension(fileName.split(".").last()).icon 123 | ?: AllIcons.FileTypes.Text 124 | } 125 | 126 | /** 127 | * init Tree icon, node title, mouse listener 128 | */ 129 | private fun initPanel() { 130 | fileTree.onRightMouseButtonClicked { e -> 131 | val row = fileTree.getRowForLocation(e.x, e.y) 132 | if (row != -1) { 133 | val treeNode = fileTree.lastSelectedPathComponent as DefaultMutableTreeNode 134 | val nodes = treeNode.userObjectPath 135 | // has selected node and the node is FileTreeNode 136 | if (nodes.isEmpty() || nodes[0] !is FileTreeNode) { 137 | return@onRightMouseButtonClicked 138 | } 139 | val selectedNode = nodes[nodes.size - 1] as FileTreeNode 140 | showEditMenu(e, selectedNode) 141 | Logger.d("PreviewPanel", selectedNode.toString()) 142 | } 143 | } 144 | fileTree.addKeyListener(object : KeyAdapter() { 145 | override fun keyPressed(e: KeyEvent?) { 146 | if (e?.keyCode == KeyEvent.VK_DELETE) { 147 | fileTree.getSelectedNodes(DefaultMutableTreeNode::class.java, null).forEach { 148 | (it.userObject as? FileTreeNode)?.removeFromParent() 149 | it.removeFromParent() 150 | } 151 | updateFileTreeUI() 152 | } 153 | } 154 | }) 155 | fileTree.isEditable = true 156 | fileTree.cellRenderer = treeCellRenderer 157 | } 158 | 159 | private fun showEditMenu(anchor: MouseEvent, current: FileTreeNode) { 160 | val item = if (current.isDir) { 161 | val i = mutableMapOf("New Directory" to { 162 | FileDialog.showForCreate(current, true) { fileTreeNode: FileTreeNode -> 163 | addTreeNode(current, fileTreeNode) 164 | } 165 | }, "New File" to { 166 | FileDialog.showForCreate(current, false) { fileTreeNode: FileTreeNode -> 167 | addTreeNode(current, fileTreeNode) 168 | } 169 | }) 170 | val parent = current.parent 171 | if (!current.isRoot() && current.children.isNotEmpty() && parent != null) { 172 | i["Remove This Node Only"] = { 173 | current.removeFromParent() 174 | for (child in current.children) { 175 | addTreeNode(parent, child) 176 | } 177 | // TODO optimize update 178 | setModuleConfig(module) 179 | } 180 | } 181 | i 182 | } else { 183 | mutableMapOf() 184 | } 185 | if (!current.isRoot()) { 186 | item["Rename"] = { 187 | val oldP = current.placeholders.orEmpty() 188 | val olds = current.placeholders?.keys?.joinToString() 189 | FileDialog.showForRefactor(current) { fileTreeNode: FileTreeNode -> 190 | (fileTree.lastSelectedPathComponent as DefaultMutableTreeNode).userObject = fileTreeNode 191 | updateFileTreeUI() 192 | val newPlaceholder = fileTreeNode.getPlaceholderInNodeName() 193 | if (olds != newPlaceholder.sorted().joinToString()) { 194 | val appendPlaceholder = newPlaceholder.filter { !oldP.containsKey(it) }.associateWith { "" } 195 | module.template.putPlaceholders(appendPlaceholder) 196 | onPlaceholderUpdateListener?.invoke() 197 | } 198 | } 199 | } 200 | item["Delete"] = { 201 | if (current.removeFromParent()) { 202 | (fileTree.lastSelectedPathComponent as DefaultMutableTreeNode).removeFromParent() 203 | ((fileTree.lastSelectedPathComponent as DefaultMutableTreeNode).userObject as? FileTreeNode)?.removeFromParent() 204 | updateFileTreeUI() 205 | } 206 | } 207 | } 208 | PopMenuUtils.show(anchor, item) 209 | } 210 | 211 | private fun updateFileTreeUI() { 212 | fileTree.updateUI() 213 | onTreeUpdateListener?.invoke() 214 | } 215 | 216 | private fun addTreeNode(parent: FileTreeNode, node: FileTreeNode) { 217 | parent.addChild(node, true) 218 | module.template.putPlaceholders(node.getPlaceholderInNodeName().toTypedArray()) 219 | module.template.putPlaceholders(node.placeholders.orEmpty()) 220 | module.template.addFileTemplates(node.fileTemplates.orEmpty()) 221 | node.fileTemplates?.clear() 222 | node.placeholders?.clear() 223 | (fileTree.lastSelectedPathComponent as DefaultMutableTreeNode).add(DefaultMutableTreeNode(node)) 224 | updateFileTreeUI() 225 | } 226 | 227 | private fun getTreeModel(fileTreeNode: FileTreeNode): TreeModel { 228 | return TreeModel(getTree(fileTreeNode)) 229 | } 230 | 231 | // convert FileTreeNode to JTree TreeNode 232 | private fun getTree(treeNode: FileTreeNode): DefaultMutableTreeNode { 233 | val result = DefaultMutableTreeNode(treeNode, true) 234 | if (treeNode.isDir) { 235 | treeNode.children.forEach(Consumer { i: FileTreeNode -> result.add(getTree(i)) }) 236 | } 237 | return result 238 | } 239 | 240 | // expand all nodes 241 | private fun expandAll(tree: JTree, parent: TreePath, expand: Boolean) { 242 | val node = parent.lastPathComponent as TreeNode 243 | if (node.childCount >= 0) { 244 | val e = node.children() 245 | while (e.hasMoreElements()) { 246 | val n = e.nextElement() as TreeNode 247 | val path = parent.pathByAddingChild(n) 248 | expandAll(tree, path, expand) 249 | } 250 | } 251 | if (expand) { 252 | tree.expandPath(parent) 253 | } else { 254 | tree.collapsePath(parent) 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/ui/RealConfigurePanel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.ui 2 | 3 | import com.dengzii.plugin.template.Config 4 | import com.dengzii.plugin.template.Config.GSON 5 | import com.dengzii.plugin.template.CreateModuleAction.Companion.project 6 | import com.dengzii.plugin.template.model.FileTreeNode 7 | import com.dengzii.plugin.template.model.Module 8 | import com.dengzii.plugin.template.model.Module.Companion.getAndroidApplication 9 | import com.dengzii.plugin.template.model.Module.Companion.getAndroidMvp 10 | import com.dengzii.plugin.template.model.Module.Companion.getAucApp 11 | import com.dengzii.plugin.template.model.Module.Companion.getAucExport 12 | import com.dengzii.plugin.template.model.Module.Companion.getAucModule 13 | import com.dengzii.plugin.template.model.Module.Companion.getAucPkg 14 | import com.dengzii.plugin.template.model.Module.Companion.getEmpty 15 | import com.dengzii.plugin.template.tools.ui.ActionToolBarUtils 16 | import com.dengzii.plugin.template.tools.ui.PopMenuUtils 17 | import com.dengzii.plugin.ui.ConfigurePanel 18 | import com.intellij.icons.AllIcons 19 | import com.intellij.openapi.fileChooser.FileChooser 20 | import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory 21 | import com.intellij.ui.DocumentAdapter 22 | import java.awt.BorderLayout 23 | import java.io.* 24 | import java.util.function.Consumer 25 | import javax.swing.DefaultListModel 26 | import javax.swing.ListSelectionModel 27 | import javax.swing.event.DocumentEvent 28 | 29 | class RealConfigurePanel : ConfigurePanel() { 30 | 31 | private var configs: MutableList? = null 32 | private var templateListModel: DefaultListModel? = null 33 | 34 | private var currentConfig: Module? = null 35 | 36 | private lateinit var panelPreview: PreviewPanel 37 | 38 | private lateinit var tablePlaceholder: EditableTable 39 | private lateinit var tableFileTemp: EditableTable 40 | private var modified = false 41 | 42 | init { 43 | panelActionBar.add(ActionToolBarUtils.create("ActionBar1", listOf( 44 | ActionToolBarUtils.Action(AllIcons.General.Add, desc = "Create template") { 45 | onAddConfig() 46 | }, 47 | ActionToolBarUtils.Action(AllIcons.General.Remove, desc = "Remove selected template") { 48 | onRemoveConfig() 49 | }, 50 | ActionToolBarUtils.Action(AllIcons.General.CopyHovered, desc = "Copy selected template") { 51 | onCopyConfig() 52 | }, 53 | ActionToolBarUtils.Action(AllIcons.ToolbarDecorator.Import, desc = "Import template from file") { 54 | onImportTemplate() 55 | }, 56 | ActionToolBarUtils.Action(AllIcons.Actions.MenuSaveall, desc = "Export selected template") { 57 | onExportTemplate() 58 | } 59 | ))) 60 | initComponent() 61 | loadConfig() 62 | initData() 63 | } 64 | 65 | fun isModified() = modified 66 | 67 | fun cacheConfig() { 68 | currentConfig ?: return 69 | currentConfig!!.template.run { 70 | removeAllPlaceHolderInTree() 71 | removeAllTemplateInTree() 72 | fileTemplates = tableFileTemp.getPairResult() 73 | placeholders = tablePlaceholder.getPairResult() 74 | } 75 | } 76 | 77 | fun saveConfig() { 78 | Config.saveModuleTemplates(configs!!) 79 | modified = false 80 | } 81 | 82 | fun createTempFromDir(node: FileTreeNode) { 83 | val module = Module.create(node, node.children.firstOrNull()?.name ?: "-") 84 | addModuleTemplate(module) 85 | } 86 | 87 | private fun initComponent() { 88 | layout = BorderLayout() 89 | add(contentPane) 90 | panelPreview = PreviewPanel(false) 91 | tablePlaceholder = EditableTable(arrayOf("Placeholder", "Default Value"), arrayOf(true, true)) 92 | tableFileTemp = EditableTable(arrayOf("FileName", "Template"), arrayOf(true, true)) 93 | panelPreview.onPlaceholderUpdate { 94 | tablePlaceholder.setPairData(currentConfig!!.template.getAllPlaceholdersMap().toMutableMap()) 95 | } 96 | panelStructure.add(panelPreview) 97 | panelPlaceholder.add(tablePlaceholder, BorderLayout.CENTER) 98 | panelFileTemp.add(tableFileTemp, BorderLayout.CENTER) 99 | } 100 | 101 | private fun initData() { 102 | tableFileTemp.addChangeListener { 103 | modified = true 104 | currentConfig?.template?.removeAllTemplateInTree() 105 | currentConfig?.template?.fileTemplates = tableFileTemp.getPairResult() 106 | } 107 | tablePlaceholder.addChangeListener { 108 | modified = true 109 | currentConfig 110 | currentConfig?.template?.removeAllPlaceHolderInTree() 111 | currentConfig?.template?.placeholders = tablePlaceholder.getPairResult() 112 | } 113 | panelPreview.setPreviewMode(cbPlaceholder.isSelected) 114 | panelPreview.setOnTreeUpdateListener { 115 | modified = true 116 | } 117 | cbPlaceholder.addChangeListener { 118 | panelPreview.setPreviewMode(cbPlaceholder.isSelected) 119 | } 120 | cbLowercaseDir.addChangeListener { 121 | currentConfig?.lowercaseDir = cbLowercaseDir.isSelected 122 | panelPreview.updateTree() 123 | } 124 | cbCapitalizeFile.addChangeListener { 125 | currentConfig?.capitalizeFile = cbCapitalizeFile.isSelected 126 | panelPreview.updateTree() 127 | } 128 | cbEnableVelocity.addChangeListener { 129 | currentConfig?.enableApacheVelocity = cbEnableVelocity.isSelected 130 | panelPreview.updateTree() 131 | } 132 | cbExpandPkgName.addChangeListener { 133 | currentConfig?.packageNameToDir = cbExpandPkgName.isSelected 134 | } 135 | tabbedPane.addChangeListener { 136 | currentConfig ?: return@addChangeListener 137 | when (tabbedPane.selectedIndex) { 138 | 1 -> tableFileTemp.setPairData(currentConfig!!.template.getAllTemplateMap()) 139 | 2 -> tablePlaceholder.setPairData(currentConfig!!.template.getAllPlaceholdersMap()) 140 | } 141 | panelPreview.setModuleConfig(currentConfig!!) 142 | } 143 | listTemplate.selectionMode = ListSelectionModel.SINGLE_SELECTION 144 | listTemplate.addListSelectionListener { 145 | if (isNoConfigSelected()) return@addListSelectionListener 146 | onConfigListSelected() 147 | } 148 | listTemplate.setModel(templateListModel) 149 | tfName.document.addDocumentListener(object : DocumentAdapter() { 150 | override fun textChanged(documentEvent: DocumentEvent) { 151 | currentConfig?.templateName = tfName.text 152 | } 153 | }) 154 | if (templateListModel!!.size() > 1) { 155 | listTemplate.selectedIndex = 0 156 | } 157 | } 158 | 159 | private fun addModuleTemplate(module: Module) { 160 | configs!!.forEach { 161 | if (it.templateName == module.templateName) { 162 | module.templateName += "_New" 163 | } 164 | } 165 | configs!!.add(module) 166 | templateListModel!!.addElement(module.templateName) 167 | listTemplate.doLayout() 168 | listTemplate.selectedIndex = configs!!.indexOf(module) 169 | onConfigListSelected() 170 | } 171 | 172 | private fun onRemoveConfig() { 173 | if (isNoConfigSelected()) { 174 | return 175 | } 176 | val selectedIndex = getSelectedConfigIndex() 177 | configs!!.removeAt(selectedIndex) 178 | templateListModel!!.remove(selectedIndex) 179 | if (!listTemplate.isEmpty) { 180 | panelPreview.isEnabled = true 181 | listTemplate.selectedIndex = 0 182 | onConfigListSelected() 183 | } else { 184 | currentConfig = null 185 | tablePlaceholder.setPairData(mapOf()) 186 | tableFileTemp.setPairData(mapOf()) 187 | panelPreview.setModuleConfig(getEmpty()) 188 | panelPreview.isEnabled = false 189 | } 190 | listTemplate.doLayout() 191 | } 192 | 193 | private fun onCopyConfig() { 194 | if (isNoConfigSelected()) { 195 | return 196 | } 197 | val newConfig = currentConfig!!.clone() 198 | newConfig.templateName += "_Copy" 199 | configs!!.add(newConfig) 200 | templateListModel!!.addElement(newConfig.templateName) 201 | listTemplate.doLayout() 202 | listTemplate.selectedIndex = configs!!.indexOf(newConfig) 203 | } 204 | 205 | private fun loadConfig() { 206 | configs = Config.loadModuleTemplates() 207 | templateListModel = DefaultListModel() 208 | configs!!.forEach(Consumer { module: Module -> templateListModel!!.addElement(module.templateName) }) 209 | if (templateListModel!!.size() > 0) { 210 | listTemplate.selectedIndex = 0 211 | } 212 | } 213 | 214 | private fun onConfigListSelected() { 215 | val index = getSelectedConfigIndex() 216 | if (currentConfig == configs!![index]) { 217 | return 218 | } 219 | cacheConfig() 220 | currentConfig = configs!![index] 221 | currentConfig?.apply { 222 | tfName.text = templateName 223 | cbCapitalizeFile.isSelected = capitalizeFile 224 | cbLowercaseDir.isSelected = lowercaseDir 225 | cbExpandPkgName.isSelected = packageNameToDir 226 | cbEnableVelocity.isSelected = enableApacheVelocity 227 | // update tree, file template and placeholder table 228 | panelPreview.setModuleConfig(this) 229 | tableFileTemp.setPairData(template.fileTemplates) 230 | tablePlaceholder.setPairData(template.placeholders) 231 | } 232 | } 233 | 234 | private fun getSelectedConfigIndex() = listTemplate.selectedIndex 235 | 236 | private fun isNoConfigSelected() = getSelectedConfigIndex() == -1 237 | 238 | private fun onAddConfig() { 239 | PopMenuUtils.create(linkedMapOf( 240 | "Empty Template" to { addModuleTemplate(getEmpty()) }, 241 | "Android Application" to { addModuleTemplate(getAndroidApplication()) }, 242 | "Android Mvp" to { addModuleTemplate(getAndroidMvp()) }, 243 | // "Auc Module" to { addModuleTemplate(getAucModule()) }, 244 | // "Auc App" to { addModuleTemplate(getAucApp()) }, 245 | // "Auc Pkg" to { addModuleTemplate(getAucPkg()) }, 246 | // "Auc Export" to { addModuleTemplate(getAucExport()) } 247 | )).show(panelActionBar, panelActionBar.x, panelActionBar.y) 248 | } 249 | 250 | private fun onExportTemplate() { 251 | val descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() 252 | descriptor.title = "Save template ${currentConfig!!.templateName} to File" 253 | val vf = FileChooser.chooseFile(descriptor, project, null) 254 | if (vf != null && vf.isWritable) { 255 | val config = GSON.toJson(currentConfig) 256 | val file = File(vf.path, currentConfig!!.templateName + ".json") 257 | var outputStream: OutputStreamWriter? = null 258 | try { 259 | outputStream = OutputStreamWriter(FileOutputStream(file)) 260 | outputStream.write(config) 261 | } catch (e: IOException) { 262 | e.printStackTrace() 263 | } finally { 264 | try { 265 | outputStream?.flush() 266 | outputStream?.close() 267 | } catch (e: IOException) { 268 | e.printStackTrace() 269 | } 270 | 271 | } 272 | } 273 | } 274 | 275 | private fun onImportTemplate() { 276 | val descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor("json") 277 | descriptor.title = "Import Template From File" 278 | val vf = FileChooser.chooseFile(descriptor, project, null) 279 | if (vf != null && vf.exists()) { 280 | val file = File(vf.path) 281 | var inputStream: BufferedInputStream? = null 282 | try { 283 | inputStream = BufferedInputStream(FileInputStream(file)) 284 | val bytes = ByteArray(1024) 285 | var len = 0 286 | val stringBuilder = StringBuilder() 287 | while (inputStream.read(bytes).also { len = it } > 0) { 288 | stringBuilder.append(String(bytes, 0, len)) 289 | } 290 | val template = GSON.fromJson(stringBuilder.toString(), Module::class.java) 291 | template.initTemplate(template.template) 292 | addModuleTemplate(template) 293 | } catch (e: IOException) { 294 | e.printStackTrace() 295 | } finally { 296 | if (inputStream != null) { 297 | try { 298 | inputStream.close() 299 | } catch (e: IOException) { 300 | e.printStackTrace() 301 | } 302 | } 303 | } 304 | } 305 | } 306 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.utils 2 | 3 | import com.jetbrains.rd.util.printlnError 4 | import java.text.SimpleDateFormat 5 | 6 | /** 7 | *
 8 |  * author : dengzi
 9 |  * e-mail : dengzixx@gmail.com
10 |  * github : https://github.com/dengzii
11 |  * time   : 2019/12/31
12 |  * desc   :
13 |  * 
14 | */ 15 | object Logger { 16 | 17 | var enable: Boolean = true 18 | var timeFormatter = SimpleDateFormat("MM-dd hh:mm:ss") 19 | 20 | fun e(tag: String, log: String) { 21 | log("e", tag, log) 22 | } 23 | 24 | fun e(tag: String, e: Throwable) { 25 | log("e", tag, e.toString()) 26 | e.printStackTrace() 27 | } 28 | 29 | fun i(tag: String, log: String) { 30 | log("i", tag, log) 31 | } 32 | 33 | fun d(tag: String, log: String) { 34 | log("d", tag, log) 35 | } 36 | 37 | fun w(tag: String, log: String) { 38 | log("w", tag, log) 39 | } 40 | 41 | private fun log(level: String, tag: String, log: String) { 42 | if (!enable) { 43 | return 44 | } 45 | 46 | val logStr = "${getTime()} ${level.toUpperCase()}/$tag: $log" 47 | if (level == "e") { 48 | printlnError(logStr) 49 | return 50 | } 51 | println(logStr) 52 | } 53 | 54 | private fun getTime(): String { 55 | return timeFormatter.format(System.currentTimeMillis()) 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/utils/PluginKit.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.utils 2 | 3 | import com.dengzii.plugin.template.tools.NotificationUtils 4 | import com.intellij.ide.fileTemplates.FileTemplate 5 | import com.intellij.ide.fileTemplates.FileTemplateManager 6 | import com.intellij.ide.fileTemplates.FileTemplateUtil 7 | import com.intellij.ide.fileTemplates.impl.FileTemplateManagerImpl 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.PlatformDataKeys 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.project.ProjectManager 12 | import com.intellij.openapi.vfs.VirtualFile 13 | import com.intellij.openapi.vfs.VirtualFileManager 14 | import com.intellij.psi.PsiDirectory 15 | import com.intellij.psi.PsiElement 16 | import com.intellij.psi.PsiFile 17 | import com.intellij.psi.PsiManager 18 | import java.util.* 19 | 20 | /** 21 | *
 22 |  * author : dengzi
 23 |  * e-mail : dengzixx@gmail.com
 24 |  * github : https://github.com/dengzii
 25 |  * time   : 2019/12/31
 26 |  * desc   :
 27 |  * 
28 | */ 29 | class PluginKit private constructor(e: AnActionEvent) { 30 | 31 | private val event = e 32 | lateinit var project: Project 33 | 34 | init { 35 | if (e.project != null) { 36 | project = e.project!! 37 | } 38 | } 39 | 40 | private inline fun requireProject(action: () -> Unit) { 41 | if (!isProjectValid()) { 42 | NotificationUtils.showError("Plugin GenerateModuleFromTemplate require a project.") 43 | action.invoke() 44 | } 45 | } 46 | 47 | fun isProjectValid(): Boolean { 48 | return project.isOpen && project.isInitialized 49 | } 50 | 51 | fun getCurrentPsiFile(): PsiFile? { 52 | return event.getData(PlatformDataKeys.PSI_FILE) 53 | } 54 | 55 | fun getCurrentPsiDirectory(): PsiDirectory? { 56 | val e = event.getData(PlatformDataKeys.PSI_ELEMENT); 57 | if (e is PsiDirectory) { 58 | return e 59 | } 60 | return null 61 | } 62 | 63 | fun getPsiDirectoryByPath(path: String): PsiDirectory? { 64 | val vf = VirtualFileManager.getInstance().findFileByUrl("") ?: return null 65 | return PsiManager.getInstance(project).findDirectory(vf) 66 | } 67 | 68 | fun getPsiDirectoryByVirtualFile(vf: VirtualFile): PsiDirectory? { 69 | if (!vf.isDirectory) { 70 | Logger.e(TAG, "${vf.path} is not a directory.") 71 | return null 72 | } 73 | return PsiManager.getInstance(project).findDirectory(vf) 74 | } 75 | 76 | fun getVirtualFile(): VirtualFile? { 77 | return event.getData(PlatformDataKeys.VIRTUAL_FILE) 78 | } 79 | 80 | fun createDir(name: String, vf: VirtualFile? = getVirtualFile()): VirtualFile? { 81 | if (vf == null || !vf.isDirectory) { 82 | Logger.e(TAG, "target is null or is not a directory.") 83 | return null 84 | } 85 | if (vf.findChild(name)?.exists() == true) { 86 | return vf.findChild(name) 87 | } 88 | return vf.createChildDirectory(null, name) 89 | } 90 | 91 | fun createFile(name: String, vf: VirtualFile? = getVirtualFile()): Boolean { 92 | if (!checkCreateFile(name, vf)) { 93 | return false 94 | } 95 | return try { 96 | vf!!.createChildData(null, name) 97 | true 98 | } catch (e: Exception) { 99 | e.printStackTrace() 100 | false 101 | } 102 | } 103 | 104 | fun calculateTemplate(templateContent: String): Array? { 105 | requireProject { 106 | return null 107 | } 108 | return FileTemplateUtil.calculateAttributes(templateContent, Properties(), true, project) 109 | } 110 | 111 | @Throws(Throwable::class) 112 | fun createFileFromTemplate( 113 | fileName: String, 114 | templateName: String, 115 | propertiesMap: Map, 116 | directory: VirtualFile 117 | ): PsiElement { 118 | 119 | val fileTemplateManager = FileTemplateManager.getInstance(project) 120 | val properties = Properties(fileTemplateManager.defaultProperties) 121 | propertiesMap.forEach { (t, u) -> 122 | properties.setProperty(t.trim(), u) 123 | } 124 | val template = 125 | fileTemplateManager.getTemplate(templateName) ?: throw IllegalArgumentException("template not found.") 126 | val psiDirectory = 127 | getPsiDirectoryByVirtualFile(directory) ?: throw IllegalArgumentException("directory not found.") 128 | return try { 129 | FileTemplateUtil.createFromTemplate(template, fileName, properties, psiDirectory) 130 | } catch (e: Throwable) { 131 | throw e 132 | } 133 | } 134 | 135 | fun addTemplate(name: String, template: String) { 136 | FileTemplateManager.getInstance(project).addTemplate(name, template) 137 | } 138 | 139 | private fun String.strip(): String { 140 | var result = this 141 | if (startsWith("\${") && endsWith("}")) { 142 | result = result.substring(2, length - 1) 143 | } 144 | return result 145 | } 146 | 147 | private fun checkCreateFile(name: String, vf: VirtualFile?): Boolean { 148 | if (vf == null || !vf.isDirectory) { 149 | Logger.e(TAG, "target is null or is not a directory.") 150 | return false 151 | } 152 | if (vf.findChild(name)?.exists() == true) { 153 | Logger.e(TAG, "directory already exists.") 154 | return false 155 | } 156 | return true 157 | } 158 | 159 | companion object { 160 | 161 | private val TAG = PluginKit::class.java.simpleName 162 | 163 | private lateinit var INSTANCE: PluginKit 164 | 165 | fun init(e: AnActionEvent): PluginKit { 166 | INSTANCE = PluginKit(e) 167 | return INSTANCE 168 | } 169 | 170 | fun getAllFileTemplate(): Array { 171 | val ts = FileTemplateManagerImpl.getDefaultInstance().allTemplates 172 | return ts 173 | } 174 | 175 | fun getFileTemplate(name: String): FileTemplate? = 176 | FileTemplateManagerImpl.getDefaultInstance().getTemplate(name) 177 | } 178 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dengzii/plugin/template/utils/PopMenuUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.utils 2 | 3 | import com.intellij.openapi.ui.JBMenuItem 4 | import com.intellij.openapi.ui.JBPopupMenu 5 | 6 | /** 7 | *
 8 |  * author : dengzi
 9 |  * e-mail : dengzixx@gmail.com
10 |  * github : https://github.com/dengzii
11 |  * time   : 2020/1/16
12 |  * desc   :
13 |  * 
14 | */ 15 | object PopMenuUtils { 16 | 17 | fun create(menu: Map): JBPopupMenu { 18 | 19 | val popMenu = JBPopupMenu() 20 | menu.forEach { (k, v) -> 21 | if (k.isBlank()) { 22 | popMenu.addSeparator() 23 | return@forEach 24 | } 25 | val item = JBMenuItem(k) 26 | item.addActionListener { 27 | v?.onClick() 28 | } 29 | popMenu.add(item) 30 | } 31 | return popMenu 32 | } 33 | 34 | interface PopMenuListener { 35 | fun onClick() 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.dengzii.plugin.template 3 | Generate Module From Template 4 | 1.6.8 5 | dengzi 6 | 7 | Create a directory structure from a highly customizable template
9 |
10 | Video Tutorial - YouTube 11 |
12 | Feature
13 | 1. Custom directory/file tree structure.
14 | 2. Support placeholders / ApacheVelocity template lang in file name.
15 | 3. Specify file templates from IDE custom/build-in templates.
16 | 4. Passing placeholders to file template as variables.
17 | 5. Export / Import template, share template by file.
18 |
19 | Usage
20 | 1. Configure template in plugin settings: File > Settings > Tools > Module Template Settings.
21 | 2. Create directories from the 'Structure' tab, click the right mouse button to operate the file tree.
22 | 3. FileTree can use placeholders or ApacheVelocity template lang, the placeholder should like this -> ${YOUR_PLACEHOLDER_HERE}.
23 | 4. The 'File Template' tab lists which template the specified file uses, you can also use placeholders for FileName field, 24 | for same name file, you can use path to distinguish them, for example: c/x.js is match a/b/c/x.js
25 | 5. The 'Placeholder' tab's table defines placeholders for replacing filenames and file templates
26 | More information: README
27 |
28 | NOTE
29 | - The existing files will be skipped.
30 | - The Java class file name may depend on ClassName, you better keep the class name and file name consistent, else the file name in the template will not effective.
31 | - The placeholders are best not the same as the built-in property of `Apache Velocity`.
32 | - The dot(.) in the directory name will split and expand as several directories.
33 |
34 | Contribute
35 | GitHub
36 | Any question please create issue
37 |
38 | Screenshot
39 | 40 | 41 | 42 |
43 | ]]>
44 | 45 | Version 1.6.8 (2024/02/21)
47 | 1. Fix: file tree node creation problem.
48 | Version 1.6.5 (2023/12/11)
49 | 1. Update: File template support match by file path.
50 | 2. Fix: ApacheVelocity template lang not work.
51 | Version 1.6.4 (2023/07/06)
52 | 1. Feature: Support ApacheVelocity in file name.
53 |
54 | ]]> 55 |
56 | 57 | 58 | 59 | com.intellij.modules.platform 60 | com.intellij.modules.lang 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 |
84 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/Template MainActivity.java.ft: -------------------------------------------------------------------------------- 1 | package ${PACKAGE_NAME}; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.annotation.Nullable; 5 | import android.os.Bundle; 6 | 7 | #parse("File Header.java") 8 | public class MainActivity extends AppCompatActivity { 9 | 10 | @Override 11 | protected void onCreate(@Nullable Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/Template Manifest.xml.ft: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/Template build.gradle.ft: -------------------------------------------------------------------------------- 1 | dependencies { 2 | 3 | } -------------------------------------------------------------------------------- /test/AucTemplateTest.kt: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import com.dengzii.plugin.template.template.AucTemplate 4 | import org.junit.Test 5 | 6 | class AucTemplateTest { 7 | 8 | @Test 9 | fun aucAppModuleTest() { 10 | val app = AucTemplate.APP 11 | app { 12 | placeholder("PACKAGE_NAME", "com.dengzii.plugin") 13 | } 14 | println(app.placeholders) 15 | // println(app.getAllPlaceholderInTree()) 16 | // app.expandPath() 17 | // app.expandPkgName(true) 18 | println(app) 19 | println(app.getTreeGraph()) 20 | } 21 | 22 | @Test 23 | fun aucModuleTemplateTest() { 24 | val module = AucTemplate.MODULE 25 | module { 26 | placeholder("FEATURE_NAME", "plugin") 27 | placeholder("PACKAGE_NAME", "com.dengzi") 28 | } 29 | module.expandPath() 30 | println(module.getTreeGraph()) 31 | } 32 | } -------------------------------------------------------------------------------- /test/FileTreeDslTest.kt: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import com.dengzii.plugin.template.model.FileTreeDsl 4 | import org.apache.velocity.VelocityContext 5 | import org.apache.velocity.util.StringUtils 6 | import org.junit.Test 7 | 8 | class FileTreeDslTest { 9 | 10 | @Test 11 | fun createSimpleFileTreeTest() { 12 | val tree = FileTreeDsl { 13 | file("file") 14 | dir("dir1") { 15 | file("file2") 16 | dir("dir2") { 17 | file("_${this.name}_file") 18 | dir("dir3") { 19 | dir("dir4") 20 | } 21 | dir("dir5") { 22 | dir("dir6") 23 | dir("dir9") 24 | } 25 | } 26 | file("file3") 27 | } 28 | dir("dir7") { 29 | dir("dir8") 30 | } 31 | } 32 | println(tree.getTreeGraph()) 33 | } 34 | 35 | @Test 36 | fun createPackageDirTest() { 37 | 38 | val tree = FileTreeDsl { 39 | dir("src") { 40 | dir("com/example/app") { 41 | file("Main.java") 42 | } 43 | dir("com/example/app1") 44 | } 45 | file("README.md") 46 | } 47 | println(tree.getTreeGraph()) 48 | } 49 | 50 | 51 | @Test 52 | fun fileNamePlaceholderTest() { 53 | val tree = FileTreeDsl { 54 | placeholder("FILE_1", "first_file") 55 | placeholder("FILE_2", "second_file") 56 | file("\${FILE_1}.java") 57 | dir("com/example") { 58 | println(name) 59 | file("\${FILE_2}.java") 60 | } 61 | } 62 | println(tree.getTreeGraph()) 63 | } 64 | 65 | @Test 66 | fun expandDirectoriesTest() { 67 | val tree = FileTreeDsl { 68 | dir("src") { 69 | dir("com.dengzii.plugin") { 70 | dir("model") 71 | dir("template") 72 | } 73 | dir("test") 74 | } 75 | } 76 | tree.expandPath() 77 | println(tree.getTreeGraph()) 78 | 79 | val tree2 = FileTreeDsl { 80 | dir("src") { 81 | dir("com.dengzii.plugin") { 82 | dir("dir1/dir2/") { 83 | dir("model") 84 | dir("template") 85 | } 86 | dir("model") 87 | dir("template") 88 | } 89 | dir("test") 90 | } 91 | } 92 | tree2.expandPath() 93 | println(tree2.getTreeGraph()) 94 | } 95 | 96 | @Test 97 | fun expandDirectoriesInPlaceholderTest() { 98 | 99 | val tree = FileTreeDsl { 100 | placeholder("PACKAGE_NAME", "com.dengzii.plugin") 101 | dir("src") { 102 | dir("\${PACKAGE_NAME}") { 103 | dir("model") 104 | dir("template") 105 | } 106 | dir("test") 107 | } 108 | } 109 | tree.expandPath() 110 | println(tree.getTreeGraph()) 111 | } 112 | 113 | @Test 114 | fun getAllPlaceholderInTreeNodeNameTest() { 115 | val tree = FileTreeDsl { 116 | placeholder("PACKAGE_NAME", "com.dengzii.plugin") 117 | dir("src") { 118 | dir("\${PACKAGE_NAME}") { 119 | dir("model") 120 | dir("template"){ 121 | file("\${TEST}") 122 | file("\${TEST2}") 123 | } 124 | } 125 | dir("test") 126 | } 127 | } 128 | println(tree.getTreeGraph()) 129 | } 130 | 131 | 132 | @Test 133 | fun aucPkgTemplateTest() { 134 | val s = VelocityContext().apply { 135 | put("StringUtils", StringUtils::class.java) 136 | put("A","a_b_c") 137 | } 138 | val d = FileTreeDsl { 139 | file("\${StringUtils.removeAndHump(\${A})}") 140 | dir("\${StringUtils.sub(\${A}, \"_\", '')}") 141 | } 142 | 143 | println(d.getTreeGraph(s)) 144 | } 145 | } -------------------------------------------------------------------------------- /test/com/dengzii/plugin/template/model/FileTreeNodeTest.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.model 2 | 3 | import com.dengzii.plugin.template.Config 4 | import junit.framework.TestCase 5 | import org.apache.velocity.VelocityContext 6 | import org.apache.velocity.util.StringUtils 7 | import org.junit.Test 8 | 9 | /** 10 | * 11 | * @author https://github.com/dengzii 12 | */ 13 | class FileTreeNodeTest : TestCase() { 14 | 15 | @Test 16 | fun testGetPlaceholders() { 17 | val root = FileTreeNode(null, "\${AAA}_\${BBB}_\${CCC_\${DDD}}", true) 18 | println(root.name) 19 | println(root.getPlaceholderInNodeName().joinToString(",")) 20 | root.putPlaceholders( 21 | mapOf( 22 | Pair("AAA", "1"), 23 | Pair("BBB", "2"), 24 | Pair("CCC_4", "3"), 25 | Pair("DDD", "4"), 26 | ) 27 | ) 28 | println(root.getRealName()) 29 | } 30 | 31 | @Test 32 | fun testPlaceholderCycle() { 33 | 34 | } 35 | 36 | @Test 37 | fun testFileTemplate() { 38 | FileTreeDsl { 39 | placeholder("PKG", "com.example") 40 | placeholder("F3", "f3") 41 | 42 | fileTemplate("f1.java", "file1.java") 43 | fileTemplate("f2.java", "ff2.java") 44 | fileTemplate("\${PKG}/f2.java", "file2.java") 45 | fileTemplate("f3.java", "file3.java") 46 | 47 | dir("root") { 48 | dir("a/b") { 49 | dir("\${PKG}") { 50 | file("f1.java") 51 | file("f2.java") 52 | file("\${F3}.java") 53 | 54 | } 55 | dir("c") { 56 | file("f2.java") 57 | } 58 | } 59 | } 60 | 61 | // parse file template file name use placeholder, and support mat 62 | 63 | resolveFileTemplate() 64 | println(getAllTemplateMap()) 65 | println(getTreeGraph(templateFile = true)) 66 | } 67 | } 68 | 69 | @Test 70 | fun testExpandPath() { 71 | val dsl = FileTreeDsl { 72 | dir("root") { 73 | dir("a/b") { 74 | dir("com.example") { 75 | 76 | } 77 | } 78 | } 79 | } 80 | val m = Module.create(dsl, "test") 81 | m.packageNameToDir = true 82 | println(m.template.getTreeGraph()) 83 | m.template.expandPath() 84 | println(m.template.getTreeGraph()) 85 | m.template.expandPkgName(true) 86 | println(m.template.getTreeGraph()) 87 | } 88 | 89 | 90 | fun testAppcacheVelocity() { 91 | val r = """ 92 | !{StringUtils.removeAndHump(FEATURE_NAME.replaceAll("[^\w]", "_"))}Module.kt 93 | """.replace("!", "\$") 94 | 95 | val dsl = FileTreeDsl { 96 | placeholder("FT", "com-example") 97 | file("\${StringUtils.removeAndHump(\${FT.replaceAll(\"[_-]\", \"_\")})}") 98 | file(r) 99 | file("\${FT.replaceAll(\"[-]\", \"_\")}") 100 | } 101 | val module = Config.GSON.fromJson("", Module::class.java) 102 | 103 | val m = Module.create(dsl, "test") 104 | m.enableApacheVelocity = true 105 | m.template.context = VelocityContext().apply { 106 | put("StringUtils", StringUtils::class.java) 107 | put("FT", "com-example") 108 | } 109 | m.template.resolve() 110 | println(m.template.getTreeGraph()) 111 | } 112 | 113 | fun testExpandPkg() { 114 | val dsl = FileTreeDsl { 115 | placeholder("FT", "com_demo") 116 | fileTemplate("\${FT.replaceAll(\"[_]\", \".\")}.java", "Example") 117 | fileTemplate("\${StringUtils.removeAndHump(\${FT.replaceAll(\"[_]\", \"-\")})}Module.kt", "ExampleModule") 118 | dir("root") { 119 | dir("\${FT.replaceAll(\"[_]\", \".\")}") { 120 | file("a.txt") 121 | } 122 | file("\${FT.replaceAll(\"[_]\", \".\")}.java") 123 | file("\${StringUtils.removeAndHump(\${FT.replaceAll(\"[_]\", \"-\")})}Module.kt") 124 | } 125 | } 126 | 127 | val m = Module.create(dsl, "test") 128 | m.packageNameToDir = true 129 | m.packageNameToDir = true 130 | m.enableApacheVelocity = true 131 | 132 | m.template.context = VelocityContext().apply { 133 | put("StringUtils", StringUtils::class.java) 134 | m.template.getPlaceholderInherit()?.forEach { (k, v) -> 135 | put(k, v) 136 | } 137 | } 138 | println("\ntemplates") 139 | m.template.placeholders?.forEach { (k, v) -> 140 | println("$k, $v") 141 | } 142 | println("\nplaceholders") 143 | m.template.fileTemplates?.forEach { (k, v) -> 144 | println("$k, $v") 145 | } 146 | println(m.template.getTreeGraph(templateFile = true)) 147 | 148 | m.template.resolveTreeFileName() 149 | m.template.resolveFileTemplate() 150 | 151 | println("\ntemplates") 152 | m.template.placeholders?.forEach { (k, v) -> 153 | println("$k, $v") 154 | } 155 | println("\nplaceholders") 156 | m.template.fileTemplates?.forEach { (k, v) -> 157 | println("$k, $v") 158 | } 159 | 160 | m.template.expandPkgName(true) 161 | println(m.template.getTreeGraph(templateFile = true)) 162 | } 163 | } -------------------------------------------------------------------------------- /test/com/dengzii/plugin/template/model/ModuleTest.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.template.model 2 | 3 | import com.dengzii.plugin.template.Config 4 | import junit.framework.TestCase 5 | 6 | /** 7 | * 8 | * @author https://github.com/dengzii 9 | */ 10 | class ModuleTest : TestCase() { 11 | 12 | fun testPersist() { 13 | val t = FileTreeDsl { 14 | dir("root") { 15 | 16 | } 17 | } 18 | val create = Module.create(t, "test") 19 | create.lowercaseDir = true 20 | val j = Config.GSON.toJson(create) 21 | println(j) 22 | } 23 | } --------------------------------------------------------------------------------