├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── WindowLayoutManager.iml ├── appveyor.yml ├── build.gradle ├── config └── checkstyle │ └── checkstyle.xml ├── doc ├── dev-setup.md └── images │ ├── add-gradle-task.png │ ├── configure-gradle-task.png │ └── edit-configuration.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── com │ └── layoutmanager │ ├── cleanup │ └── EmptyLayoutRemoverService.java │ ├── layout │ ├── LayoutAction.java │ ├── delete │ │ └── DeleteLayoutAction.java │ ├── restore │ │ └── RestoreLayoutAction.java │ └── store │ │ ├── LayoutCreator.java │ │ ├── create │ │ └── NewLayoutAction.java │ │ ├── overwrite │ │ └── OverwriteLayoutAction.java │ │ ├── smartdock │ │ ├── SmartDocker.java │ │ ├── SmartDockerFactory.java │ │ ├── ToolWindowDocking.java │ │ └── dockers │ │ │ ├── ScreenBorderDocker.java │ │ │ ├── ToolWindowDocker.java │ │ │ └── ToolWindowToScreenShrinker.java │ │ └── validation │ │ └── LayoutValidationHelper.java │ ├── localization │ └── MessagesHelper.java │ ├── migration │ └── LayoutMigratorService.java │ ├── persistence │ ├── Layout.java │ ├── LayoutConfig.java │ ├── LayoutSettings.java │ └── ToolWindowInfo.java │ ├── startup │ └── PluginBootstrapper.java │ └── ui │ ├── action │ ├── ActionNameGenerator.java │ └── ActionRegistry.java │ ├── dialogs │ ├── LayoutNameDialog.java │ └── LayoutNameValidator.java │ ├── helpers │ ├── BalloonNotificationHelper.java │ ├── ComponentNotificationHelper.java │ ├── ScreenSizeHelper.java │ └── ToolWindowHelper.java │ ├── icons │ └── Icons.java │ ├── menu │ └── WindowMenuService.java │ └── settings │ ├── EditLayout.java │ ├── ImportExportConstants.java │ ├── LayoutDuplicator.java │ ├── LayoutManagerSettingsPanel.form │ ├── LayoutManagerSettingsPanel.java │ ├── LayoutSerializer.java │ ├── SettingsPage.java │ ├── WindowMenuChangesApplier.java │ ├── exporting │ ├── ExportDialog.form │ └── ExportDialog.java │ └── importing │ ├── ImportDialog.form │ └── ImportDialog.java └── resources ├── META-INF └── plugin.xml └── com └── layoutmanager └── ui ├── icons ├── DeleteLayout.svg ├── NewLayout.svg ├── OverwriteLayout.svg └── RestoreLayout.svg └── messages.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/**/* 2 | /out 3 | /build 4 | *.jar 5 | !gradle/wrapper/gradle-wrapper.jar 6 | .gradle/**/* 7 | .DS_Store 8 | src/.idea/ 9 | src/WindowLayoutManager.iml 10 | 11 | src/out/ 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "interactive" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Michael Estermann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WindowLayoutManager 2 | 3 | A plugin for managing window layout in IntelliJ based IDEs. 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | ## Build 8 | 9 | **Master** [![Build status](https://ci.appveyor.com/api/projects/status/hdj1eip606egxrvq/branch/master?svg=true)](https://ci.appveyor.com/project/michaelestermann/windowlayoutmanager/branch/master) 10 | **Overall** [![Build status](https://ci.appveyor.com/api/projects/status/hdj1eip606egxrvq?svg=true)](https://ci.appveyor.com/project/michaelestermann/windowlayoutmanager) 11 | **Quality** [![Codacy Badge](https://app.codacy.com/project/badge/Grade/81171f50435741c294ff193317a9d7d8)](https://app.codacy.com/gh/michaelestermann/WindowLayoutManager/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 12 | 13 | ## Contribute 14 | 15 | Contribution to the plugin is very much appreciated. 16 | The [documentation](doc/dev-setup.md) describes how-to set up your development environment. 17 | -------------------------------------------------------------------------------- /WindowLayoutManager.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | version: 1.5.1.{build} 3 | environment: 4 | JAVA_HOME: C:\Program Files\Java\jdk17 5 | # 6 | 7 | build_script: 8 | - gradlew.bat buildPlugin --warning-mode none 9 | # 10 | 11 | artifacts: 12 | - path: 'build\distributions\*.zip' 13 | name: window-layout-manager 14 | # 15 | 16 | deploy_script: 17 | - ps: >- 18 | if ($env:APPVEYOR_REPO_TAG -eq $TRUE -And $env:APPVEYOR_REPO_TAG_NAME -match '^V\d+\.\d+(\.\d+)?$') { 19 | .\gradlew publishPlugin --warning-mode none 20 | } 21 | # -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.intellij' version '1.17.4' 4 | id 'checkstyle' 5 | } 6 | 7 | dependencies { 8 | implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' 9 | implementation group: 'com.github.tommyettinger', name: 'blazingchain', version: '1.4.4.4' 10 | } 11 | 12 | group 'com.layoutmanager' 13 | version '1.5.1' 14 | 15 | java { 16 | sourceCompatibility = JavaVersion.VERSION_17 17 | targetCompatibility = JavaVersion.VERSION_17 18 | } 19 | 20 | buildSearchableOptions.enabled = false 21 | 22 | repositories { 23 | mavenLocal() 24 | mavenCentral() 25 | } 26 | 27 | // https://www.jetbrains.org/intellij/sdk/docs/reference_guide/intellij_artifacts.html 28 | // https://www.jetbrains.com/intellij-repository/releases/ 29 | // https://www.jetbrains.com/intellij-repository/snapshots/ 30 | intellij { 31 | type = 'RD' 32 | // Update the version if required for testing compatibility with newer versions 33 | // This will raise a a build warning, since the version 231 is 2023.1 (2023.1 fails on the build server!) 34 | version = "2024.1.5" 35 | updateSinceUntilBuild = false 36 | } 37 | 38 | publishPlugin { 39 | token = System.getenv("ORG_GRADLE_PROJECT_intellijPublishToken") 40 | } 41 | 42 | wrapper { 43 | gradleVersion = '8.6' 44 | distributionUrl = "https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" 45 | } 46 | 47 | checkstyle { 48 | project.ext.checkstyleVersion = '10.13.0' 49 | } 50 | 51 | checkstyleMain { 52 | source ='src/main/java' 53 | } -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 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 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 83 | 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 | 127 | 128 | 129 | 131 | 132 | 133 | 134 | 136 | 137 | 138 | 139 | 141 | 142 | 143 | 144 | 146 | 147 | 148 | 149 | 151 | 152 | 153 | 154 | 155 | 157 | 158 | 159 | 160 | 162 | 163 | 164 | 165 | 167 | 168 | 169 | 170 | 172 | 173 | 174 | 175 | 177 | 179 | 181 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 207 | 208 | 209 | 210 | 211 | 212 | 215 | 216 | 217 | 218 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /doc/dev-setup.md: -------------------------------------------------------------------------------- 1 | # Developer Setup 2 | The following instructions will show you how to set up your development environment to start developing the plugin. 3 | Thank you for your interest in the further development of this plugin. Have fun tinkering around and don't be afraid to make a pull request for your cool extensions. 4 | 5 | ## Setup your Environment 6 | 7 | ### Download IDE 8 | When it comes to the Java development, the IntelliJ IDE is a good fit. The community version is free and can be downloaded from [here](https://www.jetbrains.com/de-de/idea/download/#section=windows). 9 | 10 | ### Installing Java 11 | For the development of Java, the JDK (or Open JDK) is required, which can be downloaded either from [Oracle](https://www.oracle.com/ch-de/java/technologies/javase-downloads.html) or the [Open JDK Website](https://openjdk.java.net/). For this repository, Java 8 is the minimum. 12 | If you have already installed a java development kit (JDK), ensure that the JAVA_HOME environment variable is set and points to the path where the JDK is installed. A step-by-step instruction can be found [here](https://javatutorial.net/set-java-home-windows-10). 13 | 14 | ## Download the code 15 | First of all, you need a GitHub account If you don't have any yet. Afterward you can fork the repository and make it your own. A step-by-step guide can be found [here](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo). 16 | 17 | After forking, you can clone the repository. With the locally cloned repository, you can start with the development. hurray! :) 18 | 19 | ## Run it 20 | 21 | ### Command line 22 | This projects uses [gradle](https://gradle.org/) as its build foundation. The first execution of any task can take a while since it needs to download a few things like the IDE and libraries. A progressbar will keep you informed about the progress. 23 | 24 | Following build tasks are the most common task to is it in relation to the plugin development: 25 | 26 | * Build plugin 27 | ``` 28 | gradlew buildPlugin 29 | ``` 30 | 31 | * Run plugin in IDE 32 | ``` 33 | gradlew runIde 34 | ``` 35 | 36 | * Verify plugin 37 | ``` 38 | gradlew verifyPlugin 39 | ``` 40 | * Publish plugin 41 | ``` 42 | gradlew publishPlugin 43 | ``` 44 | 45 | A complete list of all task can be retrieved by executing the command: 46 | ``` 47 | gradlew tasks 48 | ``` 49 | 50 | ### IDE 51 | If your IDE supports gradle, like IntelliJ does, you can just execute the build. It should start the build process. 52 | It's recommended to configure the run plugin task in the IDE, otherwise the plugin must be started from the console as described in the former chapter. 53 | 54 | #### Setup IntelliJ 55 | 56 | The following steps are required to debug the plugin within an IDE: 57 | 58 | 1. Open the build configurations by navigation to the window menu [Run] -> [Edit Configurations] 59 | 60 | ![edit configurations](images/edit-configuration.png "Edit Configurations") 61 | 62 | 2. A dialog should be displayed with all configurations. Hit the "+" icon and select gradle task. 63 | 64 | ![add gradle task](images/add-gradle-task.png "Add gradle task") 65 | 66 | 3. Now you can configure the gradle task. 67 | 68 | ![configure gradle task](images/configure-gradle-task.png "Configure gradle task") 69 | 70 | 4. Finish the configuration by hitting the "apply" button. Now the configuration can be executed ([Run] -> [Run...] or [Debug...]). -------------------------------------------------------------------------------- /doc/images/add-gradle-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelestermann/WindowLayoutManager/1c50e645ce8d1e75dc8bbc1249f1ed0d1c58bade/doc/images/add-gradle-task.png -------------------------------------------------------------------------------- /doc/images/configure-gradle-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelestermann/WindowLayoutManager/1c50e645ce8d1e75dc8bbc1249f1ed0d1c58bade/doc/images/configure-gradle-task.png -------------------------------------------------------------------------------- /doc/images/edit-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelestermann/WindowLayoutManager/1c50e645ce8d1e75dc8bbc1249f1ed0d1c58bade/doc/images/edit-configuration.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelestermann/WindowLayoutManager/1c50e645ce8d1e75dc8bbc1249f1ed0d1c58bade/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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/subprojects/plugins/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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | url 'https://cache-redirector.jetbrains.com/plugins.gradle.org' 5 | } 6 | } 7 | } 8 | 9 | rootProject.name = 'window-layout-manager' 10 | includeBuild '..' 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/cleanup/EmptyLayoutRemoverService.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.cleanup; 2 | 3 | import com.layoutmanager.persistence.Layout; 4 | import com.layoutmanager.persistence.LayoutConfig; 5 | 6 | public class EmptyLayoutRemoverService { 7 | public void execute() { 8 | LayoutConfig layoutConfig = LayoutConfig.getInstance(); 9 | 10 | for (Layout layout : layoutConfig.getLayouts()) { 11 | if (this.isEmpty(layout)) { 12 | layoutConfig.removeLayout(layout); 13 | } 14 | } 15 | } 16 | 17 | private boolean isEmpty(Layout layout) { 18 | return layout.getToolWindows().length == 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/LayoutAction.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.layoutmanager.persistence.Layout; 5 | 6 | public abstract class LayoutAction extends AnAction { 7 | public abstract Layout getLayout(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/delete/DeleteLayoutAction.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.delete; 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.actionSystem.Presentation; 6 | import com.intellij.openapi.application.ApplicationManager; 7 | import com.intellij.openapi.project.DumbAware; 8 | import com.layoutmanager.layout.LayoutAction; 9 | import com.layoutmanager.localization.MessagesHelper; 10 | import com.layoutmanager.persistence.Layout; 11 | import com.layoutmanager.persistence.LayoutConfig; 12 | import com.layoutmanager.ui.helpers.BalloonNotificationHelper; 13 | import com.layoutmanager.ui.icons.Icons; 14 | import com.layoutmanager.ui.menu.WindowMenuService; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | 18 | public class DeleteLayoutAction 19 | extends LayoutAction 20 | implements DumbAware { 21 | 22 | private final Layout layout; 23 | 24 | public DeleteLayoutAction(Layout layout) { 25 | this.layout = layout; 26 | Presentation presentation = this.getTemplatePresentation(); 27 | presentation.setText(layout.getName()); 28 | presentation.setIcon(Icons.Menu.DeleteLayout); 29 | } 30 | 31 | @Override 32 | public @NotNull ActionUpdateThread getActionUpdateThread() { 33 | return ActionUpdateThread.BGT; 34 | } 35 | 36 | @Override 37 | public void actionPerformed(@NotNull AnActionEvent event) { 38 | this.deleteLayout(); 39 | this.updateWindowMenuItems(); 40 | this.showNotification(); 41 | } 42 | 43 | @Override 44 | public void update(AnActionEvent e) { 45 | e.getPresentation().setText(this.layout.getName()); 46 | } 47 | 48 | public Layout getLayout() { 49 | return this.layout; 50 | } 51 | 52 | private void deleteLayout() { 53 | LayoutConfig layoutConfig = LayoutConfig.getInstance(); 54 | layoutConfig.removeLayout(this.layout); 55 | } 56 | 57 | private void showNotification() { 58 | BalloonNotificationHelper.info( 59 | MessagesHelper.message("DeleteLayout.Notification.Title"), 60 | MessagesHelper.message("DeleteLayout.Notification.Content", this.layout.getName())); 61 | } 62 | 63 | private void updateWindowMenuItems() { 64 | WindowMenuService windowMenuService = ApplicationManager 65 | .getApplication() 66 | .getService(WindowMenuService.class); 67 | windowMenuService.deleteLayout(this.layout); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/restore/RestoreLayoutAction.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.restore; 2 | 3 | import com.intellij.ide.ui.UISettings; 4 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.actionSystem.Presentation; 7 | import com.intellij.openapi.project.DumbAware; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.util.Pair; 10 | import com.intellij.openapi.wm.ToolWindowAnchor; 11 | import com.intellij.openapi.wm.ToolWindowManager; 12 | import com.intellij.openapi.wm.ex.ToolWindowEx; 13 | import com.layoutmanager.layout.LayoutAction; 14 | import com.layoutmanager.localization.MessagesHelper; 15 | import com.layoutmanager.persistence.Layout; 16 | import com.layoutmanager.persistence.ToolWindowInfo; 17 | import com.layoutmanager.ui.helpers.BalloonNotificationHelper; 18 | import com.layoutmanager.ui.helpers.ToolWindowHelper; 19 | import com.layoutmanager.ui.icons.Icons; 20 | import java.util.Map; 21 | import java.util.Objects; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.Stream; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | public class RestoreLayoutAction 27 | extends LayoutAction 28 | implements DumbAware { 29 | 30 | private final Layout layout; 31 | 32 | public RestoreLayoutAction(Layout layout) { 33 | super(); 34 | this.layout = layout; 35 | 36 | Presentation presentation = this.getTemplatePresentation(); 37 | presentation.setText(layout.getName()); 38 | presentation.setIcon(Icons.Menu.RestoreLayout); 39 | } 40 | 41 | @Override 42 | public @NotNull ActionUpdateThread getActionUpdateThread() { 43 | return ActionUpdateThread.BGT; 44 | } 45 | 46 | @Override 47 | public void update(AnActionEvent actionEvent) { 48 | actionEvent 49 | .getPresentation() 50 | .setText(this.layout.getName()); 51 | } 52 | 53 | 54 | @Override 55 | public void actionPerformed(@NotNull AnActionEvent event) { 56 | if (this.isProjectLoaded(event)) { 57 | this.applyLayout(event, this.layout); 58 | this.showNotification(this.layout); 59 | } 60 | } 61 | 62 | public Layout getLayout() { 63 | return this.layout; 64 | } 65 | 66 | private void applyLayout(AnActionEvent event, Layout layout) { 67 | this.applyEditorTabPlacement(layout); 68 | ToolWindowManager toolWindowManager = this.getToolWindowManager(event); 69 | 70 | Map toolWindows = this.getToolWindows(toolWindowManager, layout.getToolWindows()); 71 | this.hideAllToolWindows(toolWindows); 72 | this.applyToolWindowLayout(toolWindows); 73 | } 74 | 75 | private Map getToolWindows(ToolWindowManager toolWindowManager, ToolWindowInfo[] toolWindows) { 76 | return Stream 77 | .of(toolWindows) 78 | .map(x -> new Pair<>(x, (ToolWindowEx) toolWindowManager.getToolWindow(x.getId()))) 79 | .filter(x -> x.second != null) 80 | .collect(Collectors.toMap(x -> x.first, x -> x.second)); 81 | } 82 | 83 | private void applyEditorTabPlacement(Layout layout) { 84 | if (layout.getEditorPlacement() >= 0) { 85 | UISettings uiSettings = UISettings.getInstance(); 86 | uiSettings.setEditorTabPlacement(layout.getEditorPlacement()); 87 | uiSettings.setWideScreenSupport(layout.getWideScreenSupport()); 88 | uiSettings.fireUISettingsChanged(); 89 | } 90 | } 91 | 92 | private void hideAllToolWindows(Map toolWindows) { 93 | toolWindows.forEach((info, toolWindow) -> { 94 | if (!info.isVisible()) { 95 | toolWindow.hide(null); 96 | } 97 | }); 98 | } 99 | 100 | private void applyToolWindowLayout(Map toolWindows) { 101 | toolWindows.forEach((info, toolWindow) -> { 102 | if (info.isVisible()) { 103 | // !! Workaround !! 104 | // decorator is not set and throws exception. When calling this method, the content manager lazy 105 | // variable will be loaded and therefore also the decorator... 106 | // See: https://github.com/JetBrains/intellij-community/blob/a63286c3b29fe467399fb353c71ed15cd65db8dd/ 107 | // platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowImpl.kt 108 | toolWindow.getComponent(); 109 | 110 | toolWindow.setAnchor(ToolWindowAnchor.fromText(info.getAnchor()), null); 111 | toolWindow.setType(info.getType(), null); 112 | toolWindow.setSplitMode(info.isToolWindow(), null); 113 | 114 | ToolWindowHelper.setBounds(toolWindow, info.getBounds()); 115 | 116 | toolWindow.show(); 117 | } 118 | }); 119 | } 120 | 121 | private ToolWindowManager getToolWindowManager(AnActionEvent event) { 122 | Project project = event.getProject(); 123 | return ToolWindowManager.getInstance( 124 | Objects.requireNonNull(project)); 125 | } 126 | 127 | private void showNotification(Layout updatedLayout) { 128 | BalloonNotificationHelper.info( 129 | MessagesHelper.message("RestoreLayout.Notification.Title"), 130 | MessagesHelper.message("RestoreLayout.Notification.Content", updatedLayout.getName())); 131 | } 132 | 133 | private boolean isProjectLoaded(AnActionEvent event) { 134 | return event.getProject() != null; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/LayoutCreator.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store; 2 | 3 | import com.intellij.ide.ui.UISettings; 4 | import com.intellij.openapi.wm.ToolWindowManager; 5 | import com.intellij.openapi.wm.ex.ToolWindowEx; 6 | import com.layoutmanager.layout.store.smartdock.SmartDocker; 7 | import com.layoutmanager.layout.store.smartdock.SmartDockerFactory; 8 | import com.layoutmanager.layout.store.validation.LayoutValidationHelper; 9 | import com.layoutmanager.localization.MessagesHelper; 10 | import com.layoutmanager.persistence.Layout; 11 | import com.layoutmanager.persistence.LayoutSettings; 12 | import com.layoutmanager.persistence.ToolWindowInfo; 13 | import com.layoutmanager.ui.dialogs.LayoutNameDialog; 14 | import com.layoutmanager.ui.helpers.BalloonNotificationHelper; 15 | import com.layoutmanager.ui.helpers.ToolWindowHelper; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Objects; 19 | import java.util.stream.Stream; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | public class LayoutCreator { 23 | private final LayoutSettings layoutSettings; 24 | private final SmartDockerFactory smartDockerFactory; 25 | private final LayoutNameDialog layoutNameDialog; 26 | 27 | public LayoutCreator( 28 | LayoutSettings layoutSettings, 29 | SmartDockerFactory smartDockerFactory, 30 | LayoutNameDialog layoutNameDialog) { 31 | this.layoutSettings = layoutSettings; 32 | this.smartDockerFactory = smartDockerFactory; 33 | this.layoutNameDialog = layoutNameDialog; 34 | } 35 | 36 | public Layout create( 37 | ToolWindowManager toolWindowManager, 38 | int id, 39 | String defaultName) { 40 | 41 | String name = this.layoutNameDialog.show(defaultName); 42 | return name != null ? 43 | this.createLayout(toolWindowManager, id, name) : 44 | null; 45 | } 46 | 47 | private Layout createLayout(ToolWindowManager toolWindowManager, int id, String name) { 48 | List toolWindows = getToolWindows(toolWindowManager); 49 | Layout layout = new Layout( 50 | id, 51 | name, 52 | toolWindows.toArray(ToolWindowInfo[]::new), 53 | getEditorPlacement(), 54 | getWideScreenSupport()); 55 | 56 | if (this.layoutSettings.getUseSmartDock()) { 57 | this.dock(toolWindowManager, layout); 58 | } 59 | 60 | validateLayout(layout); 61 | 62 | return layout; 63 | } 64 | 65 | @NotNull 66 | private static List getToolWindows(ToolWindowManager toolWindowManager) { 67 | String[] toolWindowIds = toolWindowManager.getToolWindowIds(); 68 | List toolWindows = new ArrayList<>(); 69 | for (String id : toolWindowIds) { 70 | ToolWindowEx toolWindow = (ToolWindowEx)toolWindowManager.getToolWindow(id); 71 | 72 | ToolWindowInfo info = new ToolWindowInfo( 73 | id, 74 | Objects.requireNonNull(toolWindow).getType(), 75 | toolWindow.getAnchor().toString(), 76 | ToolWindowHelper.getBounds(toolWindow), 77 | toolWindow.isVisible(), 78 | toolWindow.isSplitMode()); 79 | toolWindows.add(info); 80 | } 81 | 82 | return toolWindows; 83 | } 84 | 85 | private void dock(ToolWindowManager toolWindowManager, Layout layout) { 86 | SmartDocker smartDocker = this.smartDockerFactory.create(toolWindowManager); 87 | smartDocker.dock(layout); 88 | } 89 | 90 | private static void validateLayout(Layout layout) { 91 | ToolWindowInfo[] invalidToolWindows = LayoutValidationHelper.retrieveToolWindowsOutsideOfScreen(layout); 92 | if (invalidToolWindows.length != 0) { 93 | String invalidToolWindowNames = String.join( 94 | ", ", 95 | Stream.of(invalidToolWindows) 96 | .map(ToolWindowInfo::getId) 97 | .toArray(String[]::new)); 98 | 99 | BalloonNotificationHelper.warning( 100 | MessagesHelper.message("StoreLayout.Validation.ToolWindowOutOfScreen.Title"), 101 | MessagesHelper.message("StoreLayout.Validation.ToolWindowOutOfScreen.Content", invalidToolWindowNames)); 102 | } 103 | } 104 | 105 | private static int getEditorPlacement() { 106 | return UISettings 107 | .getInstance() 108 | .getEditorTabPlacement(); 109 | } 110 | 111 | private static boolean getWideScreenSupport() { 112 | return UISettings 113 | .getInstance() 114 | .getWideScreenSupport(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/create/NewLayoutAction.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.create; 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.actionSystem.Presentation; 7 | import com.intellij.openapi.application.ApplicationManager; 8 | import com.intellij.openapi.project.DumbAware; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.wm.ToolWindowManager; 11 | import com.layoutmanager.layout.store.LayoutCreator; 12 | import com.layoutmanager.localization.MessagesHelper; 13 | import com.layoutmanager.persistence.Layout; 14 | import com.layoutmanager.persistence.LayoutConfig; 15 | import com.layoutmanager.ui.helpers.BalloonNotificationHelper; 16 | import com.layoutmanager.ui.icons.Icons; 17 | import com.layoutmanager.ui.menu.WindowMenuService; 18 | import java.util.Objects; 19 | import org.jetbrains.annotations.NotNull; 20 | 21 | public class NewLayoutAction 22 | extends AnAction 23 | implements DumbAware { 24 | 25 | private final LayoutCreator layoutCreator; 26 | private final int id; 27 | 28 | public NewLayoutAction(LayoutCreator layoutCreator, int id) { 29 | this.layoutCreator = layoutCreator; 30 | this.id = id; 31 | Presentation presentation = this.getTemplatePresentation(); 32 | presentation.setText(MessagesHelper.message("StoreLayout.New.Menu")); 33 | presentation.setIcon(Icons.Menu.CreateNewLayout); 34 | } 35 | 36 | @Override 37 | public @NotNull ActionUpdateThread getActionUpdateThread() { 38 | return ActionUpdateThread.BGT; 39 | } 40 | 41 | @Override 42 | public void actionPerformed(@NotNull AnActionEvent event) { 43 | if (this.isProjectLoaded(event)) { 44 | ToolWindowManager toolWindowManager = this.getToolWindowManager(event); 45 | Layout newLayout = this.layoutCreator.create( 46 | toolWindowManager, 47 | this.id, 48 | ""); 49 | 50 | if (newLayout != null) { 51 | this.storeLayout(newLayout); 52 | this.updateWindowMenuItems(newLayout); 53 | this.showNotification(newLayout); 54 | } 55 | } 56 | } 57 | 58 | private void storeLayout(Layout layout) { 59 | LayoutConfig 60 | .getInstance() 61 | .addLayout(layout); 62 | } 63 | 64 | private void updateWindowMenuItems(Layout newLayout) { 65 | WindowMenuService windowMenuService = ApplicationManager 66 | .getApplication() 67 | .getService(WindowMenuService.class); 68 | windowMenuService.addLayout(newLayout); 69 | } 70 | 71 | private void showNotification(Layout newLayout) { 72 | BalloonNotificationHelper.info( 73 | MessagesHelper.message("StoreLayout.New.Notification.Title"), 74 | MessagesHelper.message("StoreLayout.New.Notification.Content", newLayout.getName())); 75 | } 76 | 77 | private ToolWindowManager getToolWindowManager(AnActionEvent event) { 78 | Project project = event.getProject(); 79 | return ToolWindowManager.getInstance( 80 | Objects.requireNonNull(project)); 81 | } 82 | 83 | private boolean isProjectLoaded(AnActionEvent event) { 84 | return event.getProject() != null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/overwrite/OverwriteLayoutAction.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.overwrite; 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.actionSystem.Presentation; 6 | import com.intellij.openapi.application.ApplicationManager; 7 | import com.intellij.openapi.project.DumbAware; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.wm.ToolWindowManager; 10 | import com.layoutmanager.layout.LayoutAction; 11 | import com.layoutmanager.layout.store.LayoutCreator; 12 | import com.layoutmanager.localization.MessagesHelper; 13 | import com.layoutmanager.persistence.Layout; 14 | import com.layoutmanager.ui.helpers.BalloonNotificationHelper; 15 | import com.layoutmanager.ui.icons.Icons; 16 | import com.layoutmanager.ui.menu.WindowMenuService; 17 | import java.util.Objects; 18 | import org.jetbrains.annotations.NotNull; 19 | 20 | public class OverwriteLayoutAction 21 | extends LayoutAction 22 | implements DumbAware { 23 | 24 | private final LayoutCreator layoutCreator; 25 | public final Layout layout; 26 | 27 | public OverwriteLayoutAction( 28 | LayoutCreator layoutCreator, 29 | Layout layout) { 30 | this.layoutCreator = layoutCreator; 31 | this.layout = layout; 32 | 33 | Presentation presentation = this.getTemplatePresentation(); 34 | presentation.setText(this.layout.getName()); 35 | presentation.setIcon(Icons.Menu.OverwriteLayout); 36 | } 37 | 38 | @Override 39 | public @NotNull ActionUpdateThread getActionUpdateThread() { 40 | return ActionUpdateThread.BGT; 41 | } 42 | 43 | @Override 44 | public void update(AnActionEvent e) { 45 | e.getPresentation().setText(this.layout.getName()); 46 | } 47 | 48 | @Override 49 | public void actionPerformed(@NotNull AnActionEvent event) { 50 | if (this.isProjectLoaded(event)) { 51 | ToolWindowManager toolWindowManager = this.getToolWindowManager(event); 52 | String previousName = this.layout.getName(); 53 | Layout updatedLayout = this.layoutCreator.create( 54 | toolWindowManager, 55 | this.layout.getId(), 56 | this.layout.getName()); 57 | 58 | if (updatedLayout != null) { 59 | this.updateLayout(updatedLayout); 60 | this.updateWindowMenu(previousName); 61 | this.showNotification(this.layout.getName(), previousName); 62 | } 63 | } 64 | } 65 | 66 | public Layout getLayout() { 67 | return this.layout; 68 | } 69 | 70 | 71 | private void updateLayout(Layout updatedLayout) { 72 | this.layout.setEditorPlacement(updatedLayout.getEditorPlacement()); 73 | this.layout.setWideScreenSupport(updatedLayout.getWideScreenSupport()); 74 | this.layout.setName(updatedLayout.getName()); 75 | this.layout.setToolWindows(updatedLayout.getToolWindows()); 76 | } 77 | 78 | private void updateWindowMenu(String previousName) { 79 | if (!previousName.equals(this.layout.getName())) { 80 | WindowMenuService windowMenuService = ApplicationManager 81 | .getApplication() 82 | .getService(WindowMenuService.class); 83 | windowMenuService.renameLayout(this.layout); 84 | } 85 | } 86 | 87 | private void showNotification(String currentLayoutName, String previousLayoutName) { 88 | BalloonNotificationHelper.info( 89 | MessagesHelper.message("StoreLayout.Overwrite.Notification.Title"), 90 | MessagesHelper.message("StoreLayout.Overwrite.Notification.Content", previousLayoutName, currentLayoutName)); 91 | } 92 | 93 | private ToolWindowManager getToolWindowManager(AnActionEvent event) { 94 | Project project = event.getProject(); 95 | return ToolWindowManager.getInstance( 96 | Objects.requireNonNull(project)); 97 | } 98 | 99 | private boolean isProjectLoaded(AnActionEvent event) { 100 | return event.getProject() != null; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/smartdock/SmartDocker.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.smartdock; 2 | 3 | import com.intellij.openapi.wm.ToolWindowManager; 4 | import com.intellij.openapi.wm.ToolWindowType; 5 | import com.layoutmanager.layout.store.smartdock.dockers.ScreenBorderDocker; 6 | import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowDocker; 7 | import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowToScreenShrinker; 8 | import com.layoutmanager.persistence.Layout; 9 | import com.layoutmanager.persistence.ToolWindowInfo; 10 | import com.layoutmanager.ui.helpers.ScreenSizeHelper; 11 | import java.util.Arrays; 12 | 13 | public class SmartDocker { 14 | private static final int THRESHOLD = 20; 15 | 16 | private final ToolWindowManager toolWindowManager; 17 | private final ToolWindowToScreenShrinker shrinker; 18 | private final ToolWindowDocker toolWindowDocker; 19 | private final ScreenBorderDocker screenBorderDocker; 20 | 21 | public SmartDocker( 22 | ToolWindowManager toolWindowManager, 23 | ToolWindowToScreenShrinker shrinker, 24 | ToolWindowDocker toolWindowDocker, 25 | ScreenBorderDocker screenBorderDocker) { 26 | this.toolWindowManager = toolWindowManager; 27 | this.shrinker = shrinker; 28 | this.toolWindowDocker = toolWindowDocker; 29 | this.screenBorderDocker = screenBorderDocker; 30 | } 31 | 32 | public void dock(Layout layout) { 33 | ToolWindowDocking[] floatedOrWindowsToolWindows = this.getFloatedOrWindowsToolWindows(layout); 34 | 35 | this.shrinker.shrink(floatedOrWindowsToolWindows); 36 | this.toolWindowDocker.dock(floatedOrWindowsToolWindows); 37 | this.screenBorderDocker.dock(floatedOrWindowsToolWindows, THRESHOLD); 38 | } 39 | 40 | 41 | private ToolWindowDocking[] getFloatedOrWindowsToolWindows(Layout layout) { 42 | return Arrays.stream(layout.getToolWindows()) 43 | .filter(this::isFloatingOrWindowedToolWindow) 44 | .map(x -> new ToolWindowDocking( 45 | x, 46 | ScreenSizeHelper.getContainingScreenBounds(x), 47 | THRESHOLD)) 48 | .toArray(ToolWindowDocking[]::new); 49 | } 50 | 51 | private boolean isFloatingOrWindowedToolWindow(ToolWindowInfo toolWindow) { 52 | return this.toolWindowManager.getToolWindow(toolWindow.getId()) != null && 53 | toolWindow.getType() == ToolWindowType.FLOATING || 54 | toolWindow.getType() == ToolWindowType.WINDOWED; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/smartdock/SmartDockerFactory.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.smartdock; 2 | 3 | import com.intellij.openapi.wm.ToolWindowManager; 4 | import com.layoutmanager.layout.store.smartdock.dockers.ScreenBorderDocker; 5 | import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowDocker; 6 | import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowToScreenShrinker; 7 | 8 | public class SmartDockerFactory { 9 | public SmartDocker create(ToolWindowManager toolWindowManager) { 10 | return new SmartDocker( 11 | toolWindowManager, 12 | new ToolWindowToScreenShrinker(), 13 | new ToolWindowDocker(), 14 | new ScreenBorderDocker()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/smartdock/ToolWindowDocking.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.smartdock; 2 | 3 | import com.layoutmanager.persistence.ToolWindowInfo; 4 | import java.awt.Rectangle; 5 | 6 | public class ToolWindowDocking { 7 | 8 | private final ToolWindowInfo toolWindowInfo; 9 | private final Rectangle containingScreen; 10 | private final int threshold; 11 | 12 | public ToolWindowDocking( 13 | ToolWindowInfo toolWindowInfo, 14 | Rectangle containingScreen, 15 | int threshold) { 16 | this.toolWindowInfo = toolWindowInfo; 17 | this.containingScreen = containingScreen; 18 | this.threshold = threshold; 19 | } 20 | 21 | public ToolWindowInfo getToolWindowInfo() { 22 | return this.toolWindowInfo; 23 | } 24 | 25 | public Rectangle getContainingScreen() { 26 | return this.containingScreen; 27 | } 28 | 29 | public Rectangle getBounds() { 30 | return this.toolWindowInfo.getBounds(); 31 | } 32 | 33 | public Rectangle getLeftDockingBounds() { 34 | return new Rectangle( 35 | (int) Math.max(0, this.toolWindowInfo.getBounds().getX() - this.threshold), 36 | (int) this.toolWindowInfo.getBounds().getY(), 37 | this.threshold * 2, 38 | (int) this.toolWindowInfo.getBounds().getHeight()); 39 | } 40 | 41 | public Rectangle getTopDockingBounds() { 42 | return new Rectangle( 43 | (int) this.toolWindowInfo.getBounds().getX(), 44 | (int) Math.max(0, this.toolWindowInfo.getBounds().getY() - this.threshold), 45 | (int) this.toolWindowInfo.getBounds().getWidth(), 46 | this.threshold * 2); 47 | } 48 | 49 | public Rectangle getRightDockingBounds() { 50 | return new Rectangle( 51 | (int) (this.toolWindowInfo.getBounds().getMaxX() - this.threshold), 52 | (int) this.toolWindowInfo.getBounds().getY(), 53 | (int) Math.min(this.threshold * 2, this.threshold + this.containingScreen.getMaxX() - this.toolWindowInfo.getBounds().getMaxX()), 54 | (int) this.toolWindowInfo.getBounds().getHeight()); 55 | } 56 | 57 | public Rectangle getBottomDockingBounds() { 58 | return new Rectangle( 59 | (int) this.toolWindowInfo.getBounds().getX(), 60 | (int) this.toolWindowInfo.getBounds().getMaxY() - this.threshold, 61 | (int) this.toolWindowInfo.getBounds().getWidth(), 62 | (int) Math.min(this.threshold * 2, this.threshold + this.containingScreen.getMaxY() - this.toolWindowInfo.getBounds().getMaxY())); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ScreenBorderDocker.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.smartdock.dockers; 2 | 3 | import com.layoutmanager.layout.store.smartdock.ToolWindowDocking; 4 | import java.awt.Rectangle; 5 | import java.util.function.BiFunction; 6 | import java.util.function.Function; 7 | 8 | public class ScreenBorderDocker { 9 | public void dock(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { 10 | this.pinLeft(floatedOrWindowsToolWindows, threshold); 11 | this.pinTop(floatedOrWindowsToolWindows, threshold); 12 | this.pinRight(floatedOrWindowsToolWindows, threshold); 13 | this.pinBottom(floatedOrWindowsToolWindows, threshold); 14 | } 15 | 16 | private void pinLeft(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { 17 | this.pinToScreenBorder( 18 | floatedOrWindowsToolWindows, 19 | threshold, 20 | toolWindowDocking -> (int)(toolWindowDocking.getBounds().getX() - toolWindowDocking.getContainingScreen().getX()), 21 | (toolWindowDocking, difference) -> new Rectangle( 22 | (int)toolWindowDocking.getContainingScreen().getX(), 23 | (int)toolWindowDocking.getBounds().getY(), 24 | (int)(toolWindowDocking.getBounds().getWidth() + difference), 25 | (int)toolWindowDocking.getBounds().getHeight())); 26 | } 27 | 28 | private void pinTop(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { 29 | this.pinToScreenBorder( 30 | floatedOrWindowsToolWindows, 31 | threshold, 32 | x -> (int)(x.getBounds().getY() - x.getContainingScreen().getY()), 33 | (toolWindowDocking, difference) -> new Rectangle( 34 | (int)toolWindowDocking.getBounds().getX(), 35 | (int)toolWindowDocking.getContainingScreen().getY(), 36 | (int)toolWindowDocking.getBounds().getWidth(), 37 | (int)(toolWindowDocking.getBounds().getHeight() + 5))); 38 | } 39 | 40 | private void pinRight(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { 41 | this.pinToScreenBorder( 42 | floatedOrWindowsToolWindows, 43 | threshold, 44 | toolWindowDocking -> (int)(toolWindowDocking.getContainingScreen().getMaxX() - toolWindowDocking.getBounds().getMaxX()), 45 | (toolWindowDocking, difference) -> new Rectangle( 46 | (int)toolWindowDocking.getBounds().getX(), 47 | (int)toolWindowDocking.getBounds().getY(), 48 | (int)(toolWindowDocking.getBounds().getWidth() + difference), 49 | (int)toolWindowDocking.getBounds().getHeight())); 50 | } 51 | 52 | private void pinBottom(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { 53 | this.pinToScreenBorder( 54 | floatedOrWindowsToolWindows, 55 | threshold, 56 | toolWindowDocking -> (int)(toolWindowDocking.getBounds().getY() - toolWindowDocking.getContainingScreen().getY()), 57 | (toolWindowDocking, difference) -> new Rectangle( 58 | (int)toolWindowDocking.getBounds().getX(), 59 | (int)toolWindowDocking.getBounds().getY(), 60 | (int)toolWindowDocking.getBounds().getWidth(), 61 | (int)(toolWindowDocking.getBounds().getHeight() + difference))); 62 | } 63 | 64 | private void pinToScreenBorder( 65 | ToolWindowDocking[] toolWindowDockings, 66 | int threshold, 67 | Function getDifference, 68 | BiFunction calculateBounds) { 69 | 70 | for (ToolWindowDocking toolWindowDocking : toolWindowDockings) { 71 | Integer difference = getDifference.apply(toolWindowDocking); 72 | if (difference != 0 && difference < threshold) { 73 | toolWindowDocking.getToolWindowInfo().setBounds(calculateBounds.apply(toolWindowDocking, difference)); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowDocker.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.smartdock.dockers; 2 | 3 | import com.layoutmanager.layout.store.smartdock.ToolWindowDocking; 4 | import java.awt.Rectangle; 5 | import java.util.Arrays; 6 | import java.util.Comparator; 7 | import java.util.function.BiFunction; 8 | import java.util.function.BiPredicate; 9 | 10 | public class ToolWindowDocker { 11 | public void dock(ToolWindowDocking[] floatedOrWindowsToolWindows) { 12 | this.dockLeft(floatedOrWindowsToolWindows); 13 | this.dockTop(floatedOrWindowsToolWindows); 14 | this.dockRight(floatedOrWindowsToolWindows); 15 | this.dockBottom(floatedOrWindowsToolWindows); 16 | } 17 | 18 | private void dockLeft(ToolWindowDocking[] floatedOrWindowsToolWindows) { 19 | this.dockToolWindow( 20 | floatedOrWindowsToolWindows, 21 | (x, y) -> (int) (x.getBounds().getX() - y.getBounds().getX()), 22 | (x, y) -> x.getRightDockingBounds().intersects(y.getLeftDockingBounds()), 23 | Comparator.comparingInt(x -> (int) x.getBounds().getY()), 24 | (toolWindow, boundsToDock) -> { 25 | int x = (int) toolWindow.getBounds().getX(); 26 | int newX = (int) boundsToDock.getMaxX(); 27 | return new Rectangle( 28 | newX, 29 | (int) toolWindow.getBounds().getY(), 30 | (int) (toolWindow.getBounds().getWidth() + (x - newX)), 31 | (int) toolWindow.getBounds().getHeight()); 32 | }); 33 | } 34 | 35 | private void dockTop(ToolWindowDocking[] floatedOrWindowsToolWindows) { 36 | this.dockToolWindow( 37 | floatedOrWindowsToolWindows, 38 | (x, y) -> (int) (x.getBounds().getY() - y.getBounds().getY()), 39 | (x, y) -> x.getBottomDockingBounds().intersects(y.getTopDockingBounds()), 40 | Comparator.comparingInt(x -> (int) x.getBounds().getX()), 41 | (toolWindow, boundsToDock) -> { 42 | int y = (int) toolWindow.getBounds().getY(); 43 | int newY = (int) boundsToDock.getMaxY(); 44 | return new Rectangle( 45 | (int) toolWindow.getBounds().getX(), 46 | newY, 47 | (int) toolWindow.getBounds().getWidth(), 48 | (int) (toolWindow.getBounds().getHeight() + (y - newY))); 49 | }); 50 | } 51 | 52 | private void dockRight(ToolWindowDocking[] floatedOrWindowsToolWindows) { 53 | this.dockToolWindow( 54 | floatedOrWindowsToolWindows, 55 | (x, y) -> (int) (x.getBounds().getMaxX() - y.getBounds().getMaxX()), 56 | (x, y) -> x.getLeftDockingBounds().intersects(y.getRightDockingBounds()), 57 | Comparator.comparingInt(x -> (int) x.getBounds().getY()), 58 | (toolWindow, boundsToDock) -> new Rectangle( 59 | (int) toolWindow.getBounds().getX(), 60 | (int) toolWindow.getBounds().getY(), 61 | (int) (boundsToDock.getX() - toolWindow.getBounds().getX()), 62 | (int) toolWindow.getBounds().getHeight())); 63 | } 64 | 65 | private void dockBottom(ToolWindowDocking[] floatedOrWindowsToolWindows) { 66 | this.dockToolWindow( 67 | floatedOrWindowsToolWindows, 68 | (x, y) -> (int) (x.getBounds().getMaxY() - y.getBounds().getMaxY()), 69 | (x, y) -> x.getTopDockingBounds().intersects(y.getBottomDockingBounds()), 70 | Comparator.comparingInt(x -> (int) x.getBounds().getY()), 71 | (toolWindow, boundsToDock) -> new Rectangle( 72 | (int) toolWindow.getBounds().getX(), 73 | (int) toolWindow.getBounds().getY(), 74 | (int) toolWindow.getBounds().getWidth(), 75 | (int) (boundsToDock.getY() - toolWindow.getBounds().getY()))); 76 | } 77 | 78 | private void dockToolWindow( 79 | ToolWindowDocking[] floatedOrWindowsToolWindows, 80 | Comparator sortForProcessing, 81 | BiPredicate isIntersecting, 82 | Comparator sortForClosestToDock, 83 | BiFunction calculateNewBounds) { 84 | 85 | ToolWindowDocking[] sortedByLeftPosition = Arrays.stream(floatedOrWindowsToolWindows) 86 | .sorted(sortForProcessing) 87 | .skip(1) 88 | .toArray(ToolWindowDocking[]::new); 89 | 90 | for (ToolWindowDocking toolWindowDocking : sortedByLeftPosition) { 91 | ToolWindowDocking dockingTarget = Arrays.stream(floatedOrWindowsToolWindows) 92 | .filter(x -> 93 | !x.getToolWindowInfo().getId().equals(toolWindowDocking.getToolWindowInfo().getId()) && 94 | isIntersecting.test(x, toolWindowDocking)) 95 | .min(sortForClosestToDock) 96 | .orElse(null); 97 | 98 | if (dockingTarget != null) { 99 | Rectangle newBounds = calculateNewBounds.apply(toolWindowDocking, dockingTarget.getBounds()); 100 | toolWindowDocking.getToolWindowInfo().setBounds(newBounds); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowToScreenShrinker.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.smartdock.dockers; 2 | 3 | import com.layoutmanager.layout.store.smartdock.ToolWindowDocking; 4 | import java.awt.Rectangle; 5 | 6 | public class ToolWindowToScreenShrinker { 7 | public void shrink(ToolWindowDocking[] floatedOrWindowsToolWindowDockings) { 8 | for (ToolWindowDocking toolWindowDocking : floatedOrWindowsToolWindowDockings) { 9 | Rectangle containingScreen = toolWindowDocking.getContainingScreen(); 10 | Rectangle toolWindowBounds = toolWindowDocking.getBounds(); 11 | 12 | if (!containingScreen.contains(toolWindowBounds)) { 13 | double x = Math.max(toolWindowBounds.getX(), containingScreen.getX()); 14 | double y = Math.max(toolWindowBounds.getY(), containingScreen.getY()); 15 | Rectangle newToolWindowBounds = new Rectangle( 16 | (int)x, 17 | (int)y, 18 | (int)Math.min(toolWindowBounds.getWidth(), containingScreen.getMaxX() - x), 19 | (int)Math.min(toolWindowBounds.getHeight(), containingScreen.getMaxY() - y)); 20 | 21 | toolWindowDocking.getToolWindowInfo().setBounds(newToolWindowBounds); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/layout/store/validation/LayoutValidationHelper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.layout.store.validation; 2 | 3 | import com.intellij.openapi.wm.ToolWindowType; 4 | import com.intellij.openapi.wm.WindowManager; 5 | import com.layoutmanager.persistence.Layout; 6 | import com.layoutmanager.persistence.ToolWindowInfo; 7 | import java.awt.Rectangle; 8 | import java.util.stream.Stream; 9 | 10 | public class LayoutValidationHelper { 11 | public static ToolWindowInfo[] retrieveToolWindowsOutsideOfScreen(Layout layout) { 12 | return Stream 13 | .of(layout.getToolWindows()) 14 | .filter(x -> x.isVisible() && isWindowType(x) && !isValid(x)) 15 | .toArray(ToolWindowInfo[]::new); 16 | } 17 | 18 | private static boolean isWindowType(ToolWindowInfo toolWindowInfo) { 19 | return toolWindowInfo.getType() == ToolWindowType.FLOATING || toolWindowInfo.getType() == ToolWindowType.WINDOWED; 20 | } 21 | 22 | private static boolean isValid(ToolWindowInfo toolWindowInfo) { 23 | Rectangle bounds = toolWindowInfo.getBounds(); 24 | return WindowManager.getInstance().isInsideScreenBounds(bounds.x, bounds.y, bounds.width); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/localization/MessagesHelper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.localization; 2 | 3 | import com.intellij.AbstractBundle; 4 | import java.lang.ref.SoftReference; 5 | import java.util.Locale; 6 | import java.util.MissingResourceException; 7 | import java.util.ResourceBundle; 8 | import org.jetbrains.annotations.NonNls; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.PropertyKey; 11 | 12 | public class MessagesHelper { 13 | @NonNls 14 | public static final String BUNDLE_NAME = "com.layoutmanager.ui.messages"; 15 | 16 | private static SoftReference thisBundle; 17 | 18 | private static ResourceBundle getBundle() { 19 | ResourceBundle bundle = thisBundle != null ? thisBundle.get() : null; 20 | if (bundle == null) { 21 | try { 22 | bundle = ResourceBundle.getBundle(BUNDLE_NAME); 23 | } catch (MissingResourceException e) { 24 | bundle = ResourceBundle.getBundle(BUNDLE_NAME, Locale.ENGLISH); 25 | } 26 | thisBundle = new SoftReference<>(bundle); 27 | } 28 | return bundle; 29 | } 30 | 31 | public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE_NAME) String key, 32 | @NotNull Object... params) { 33 | return AbstractBundle.message(getBundle(), key, params); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/migration/LayoutMigratorService.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.migration; 2 | 3 | import com.layoutmanager.persistence.Layout; 4 | import com.layoutmanager.persistence.LayoutConfig; 5 | import java.util.Arrays; 6 | 7 | public class LayoutMigratorService { 8 | 9 | public void migrate() { 10 | LayoutConfig layoutConfig = LayoutConfig.getInstance(); 11 | this.addMissingIds(layoutConfig); 12 | } 13 | 14 | private void addMissingIds(LayoutConfig layoutConfig) { 15 | for (int index = 1; index < layoutConfig.getLayoutCount(); index++) { 16 | 17 | Layout layout = layoutConfig.getLayout(index); 18 | if (this.isTaken(layoutConfig, layout.getId())) { 19 | layout.setId(layoutConfig.getNextAvailableId()); 20 | } 21 | } 22 | } 23 | 24 | private boolean isTaken(LayoutConfig config, int id) { 25 | return Arrays 26 | .stream(config.getLayouts()) 27 | .filter(x -> x.getId() == id) 28 | .count() > 1; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/persistence/Layout.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.persistence; 2 | 3 | 4 | import java.util.Arrays; 5 | import java.util.Objects; 6 | 7 | public class Layout { 8 | private int id; 9 | private String name; 10 | private ToolWindowInfo[] toolWindows; 11 | private int editorTabPlacement; 12 | private boolean wideScreenSupport; 13 | 14 | @SuppressWarnings({"unused", "Used for serialization."}) 15 | public Layout() { 16 | this.id = 0; 17 | this.name = ""; 18 | this.toolWindows = new ToolWindowInfo[0]; 19 | this.editorTabPlacement = -1; 20 | } 21 | 22 | public Layout( 23 | int id, 24 | String name, 25 | ToolWindowInfo[] toolWindows, 26 | int editorTabPlacement, 27 | boolean wideScreenSupport) { 28 | this.id = id; 29 | this.name = name; 30 | this.toolWindows = toolWindows; 31 | this.editorTabPlacement = editorTabPlacement; 32 | this.wideScreenSupport = wideScreenSupport; 33 | } 34 | 35 | public int getId() { 36 | return this.id; 37 | } 38 | 39 | public void setId(int id) { 40 | this.id = id; 41 | } 42 | 43 | public String getName() { 44 | return this.name; 45 | } 46 | 47 | public void setName(String name) { 48 | this.name = name; 49 | } 50 | 51 | public int getEditorPlacement() { 52 | return this.editorTabPlacement; 53 | } 54 | 55 | public void setEditorPlacement(int editorTabPlacement) { 56 | this.editorTabPlacement = editorTabPlacement; 57 | } 58 | 59 | public boolean getWideScreenSupport() { 60 | return this.wideScreenSupport; 61 | } 62 | 63 | public void setWideScreenSupport(boolean wideScreenSupport) { 64 | this.wideScreenSupport = wideScreenSupport; 65 | } 66 | 67 | public ToolWindowInfo[] getToolWindows() { 68 | return this.toolWindows; 69 | } 70 | 71 | public void setToolWindows(ToolWindowInfo[] toolWindows) { 72 | this.toolWindows = toolWindows; 73 | } 74 | 75 | @Override 76 | public boolean equals(Object other) { 77 | if (this == other) { 78 | return true; 79 | } 80 | 81 | if (other == null || this.getClass() != other.getClass()) { 82 | return false; 83 | } 84 | 85 | Layout that = (Layout) other; 86 | return this.editorTabPlacement == that.editorTabPlacement && 87 | this.name.equals(that.name) && 88 | Arrays.equals(this.toolWindows, that.toolWindows); 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | int result = Objects.hash(this.name, this.editorTabPlacement); 94 | result = 31 * result + Arrays.hashCode(this.toolWindows); 95 | return result; 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/persistence/LayoutConfig.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.persistence; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.components.PersistentStateComponent; 5 | import com.intellij.openapi.components.State; 6 | import com.intellij.openapi.components.Storage; 7 | import com.intellij.util.xmlb.XmlSerializerUtil; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | @State( 15 | name = "Layout", 16 | storages = { 17 | @Storage("layout.xml") 18 | } 19 | ) 20 | public class LayoutConfig implements PersistentStateComponent { 21 | private List layouts = new ArrayList<>(); 22 | private LayoutSettings settings = new LayoutSettings(); 23 | 24 | @Nullable 25 | @Override 26 | public LayoutConfig getState() { 27 | return this; 28 | } 29 | 30 | @Override 31 | public void loadState(@NotNull LayoutConfig layoutConfig) { 32 | XmlSerializerUtil.copyBean(layoutConfig, this); 33 | } 34 | 35 | @SuppressWarnings({"unused", "Used for serialization."}) 36 | public Layout getLayout(int number) { 37 | return this.layouts.get(number); 38 | } 39 | 40 | @SuppressWarnings({"unused", "Used for serialization."}) 41 | public Layout[] getLayouts() { 42 | return this.layouts.toArray(Layout[]::new); 43 | } 44 | 45 | @SuppressWarnings({"unused", "Used for serialization."}) 46 | public void setLayouts(Layout[] layouts) { 47 | this.layouts = new ArrayList<>(Arrays.asList(layouts)); 48 | } 49 | 50 | public LayoutSettings getSettings() { 51 | return this.settings; 52 | } 53 | 54 | public void setSettings(LayoutSettings settings) { 55 | this.settings = settings; 56 | } 57 | 58 | public int getLayoutCount() { 59 | return this.layouts.size(); 60 | } 61 | 62 | @SuppressWarnings({"unused", "Used for serialization."}) 63 | public void setLayout(int number, Layout layout) { 64 | this.layouts.set(number, layout); 65 | } 66 | 67 | public void addLayout(Layout layout) { 68 | this.layouts.add(layout); 69 | } 70 | 71 | public void removeLayout(Layout layout) { 72 | this.layouts.remove(layout); 73 | } 74 | 75 | public int getNextAvailableId() { 76 | for (int id = 0; id < Integer.MAX_VALUE; id++) { 77 | int finalId = id; 78 | if (this.layouts.stream().noneMatch(x -> x.getId() == finalId)) { 79 | return id; 80 | } 81 | } 82 | 83 | return Integer.MAX_VALUE; 84 | } 85 | 86 | public static LayoutConfig getInstance() { 87 | return ApplicationManager 88 | .getApplication() 89 | .getService(LayoutConfig.class); 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/persistence/LayoutSettings.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.persistence; 2 | 3 | public class LayoutSettings { 4 | private boolean useSmartDock = true; 5 | 6 | public boolean getUseSmartDock() { 7 | return this.useSmartDock; 8 | } 9 | 10 | public void setUseSmartDock(boolean useSmartDock) { 11 | this.useSmartDock = useSmartDock; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/persistence/ToolWindowInfo.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.persistence; 2 | 3 | import com.intellij.openapi.wm.ToolWindowType; 4 | import java.awt.Rectangle; 5 | import java.util.Objects; 6 | 7 | public class ToolWindowInfo { 8 | private String id; 9 | private ToolWindowType type; 10 | private String anchor; 11 | private Rectangle bounds; 12 | private boolean isVisible; 13 | private boolean isToolWindow; 14 | 15 | @SuppressWarnings({"unused", "Used for serialization."}) 16 | public ToolWindowInfo() { 17 | this.id = null; 18 | this.type = null; 19 | this.anchor = "top"; 20 | this.bounds = null; 21 | this.isVisible = false; 22 | this.isToolWindow = false; 23 | } 24 | 25 | public ToolWindowInfo( 26 | String id, 27 | ToolWindowType type, 28 | String anchor, 29 | Rectangle bounds, 30 | boolean isVisible, 31 | boolean isToolWindow) { 32 | this.id = id; 33 | this.type = type; 34 | this.anchor = anchor; 35 | this.bounds = bounds; 36 | this.isVisible = isVisible; 37 | this.isToolWindow = isToolWindow; 38 | } 39 | 40 | public String getId() { 41 | return this.id; 42 | } 43 | 44 | public ToolWindowType getType() { 45 | return this.type; 46 | } 47 | 48 | public String getAnchor() { 49 | return this.anchor; 50 | } 51 | 52 | public Rectangle getBounds() { 53 | return this.bounds; 54 | } 55 | 56 | public boolean isVisible() { 57 | return this.isVisible; 58 | } 59 | 60 | public boolean isToolWindow() { 61 | return this.isToolWindow; 62 | } 63 | 64 | @SuppressWarnings({"unused", "Used for serialization."}) 65 | public void setId(String id) { 66 | this.id = id; 67 | } 68 | 69 | @SuppressWarnings({"unused", "Used for serialization."}) 70 | public void setType(ToolWindowType type) { 71 | this.type = type; 72 | } 73 | 74 | @SuppressWarnings({"unused", "Used for serialization."}) 75 | public void setAnchor(String anchor) { 76 | this.anchor = anchor; 77 | } 78 | 79 | @SuppressWarnings({"unused", "Used for serialization."}) 80 | public void setBounds(Rectangle bounds) { 81 | this.bounds = bounds; 82 | } 83 | 84 | @SuppressWarnings({"unused", "Used for serialization."}) 85 | public void setVisible(boolean visible) { 86 | this.isVisible = visible; 87 | } 88 | 89 | @SuppressWarnings({"unused", "Used for serialization."}) 90 | public void setIsToolWindow(boolean isToolWindow) { 91 | this.isToolWindow = isToolWindow; 92 | } 93 | 94 | @Override 95 | public boolean equals(Object other) { 96 | if (this == other) { 97 | return true; 98 | } 99 | 100 | if (other == null || this.getClass() != other.getClass()) { 101 | return false; 102 | } 103 | 104 | ToolWindowInfo that = (ToolWindowInfo) other; 105 | return this.isVisible == that.isVisible && 106 | this.isToolWindow == that.isToolWindow && 107 | this.id.equals(that.id) && 108 | this.type == that.type && 109 | this.anchor.equals(that.anchor) && 110 | this.bounds.equals(that.bounds); 111 | } 112 | 113 | @Override 114 | public int hashCode() { 115 | return Objects.hash(this.id, this.type, this.anchor, this.bounds, this.isVisible, this.isToolWindow); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/startup/PluginBootstrapper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.startup; 2 | 3 | import com.intellij.ide.AppLifecycleListener; 4 | import com.intellij.openapi.application.ApplicationManager; 5 | import com.layoutmanager.cleanup.EmptyLayoutRemoverService; 6 | import com.layoutmanager.migration.LayoutMigratorService; 7 | import com.layoutmanager.ui.menu.WindowMenuService; 8 | 9 | public class PluginBootstrapper implements AppLifecycleListener { 10 | 11 | @Override 12 | public void welcomeScreenDisplayed() { 13 | EmptyLayoutRemoverService emptyLayoutRemoverService = ApplicationManager 14 | .getApplication() 15 | .getService(EmptyLayoutRemoverService.class); 16 | emptyLayoutRemoverService.execute(); 17 | 18 | LayoutMigratorService layoutMigratorService = ApplicationManager 19 | .getApplication() 20 | .getService(LayoutMigratorService.class); 21 | layoutMigratorService.migrate(); 22 | 23 | WindowMenuService windowMenuService = ApplicationManager 24 | .getApplication() 25 | .getService(WindowMenuService.class); 26 | windowMenuService.create(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/action/ActionNameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.action; 2 | 3 | import static com.layoutmanager.ui.action.ActionRegistry.ACTION_PREFIX; 4 | 5 | import com.layoutmanager.layout.LayoutAction; 6 | import com.layoutmanager.layout.restore.RestoreLayoutAction; 7 | import com.layoutmanager.persistence.Layout; 8 | 9 | public class ActionNameGenerator { 10 | public static String getActionNameForLayoutAction(LayoutAction layoutAction) { 11 | String className = layoutAction.getClass().getSimpleName(); 12 | String typeName = className.substring(0, className.length() - "LayoutAction".length()); 13 | 14 | return getActionName(layoutAction.getLayout().getId(), typeName); 15 | } 16 | 17 | public static String getActionNameForLayout(Layout layout) { 18 | String className = RestoreLayoutAction.class.getSimpleName(); 19 | String typeName = className.substring(0, className.length() - "LayoutAction".length()); 20 | 21 | return getActionName(layout.getId(), typeName); 22 | } 23 | 24 | private static String getActionName(int id, String typeName) { 25 | return ACTION_PREFIX + typeName + "." + id; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/action/ActionRegistry.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.action; 2 | 3 | import com.intellij.openapi.actionSystem.ActionManager; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.Shortcut; 6 | import com.intellij.openapi.extensions.PluginId; 7 | import com.intellij.openapi.keymap.Keymap; 8 | import com.intellij.openapi.keymap.KeymapManager; 9 | import com.layoutmanager.layout.LayoutAction; 10 | import java.util.Objects; 11 | 12 | public class ActionRegistry { 13 | private static final String PLUGIN_ID = "com.layoutmanager"; 14 | public static final String ACTION_PREFIX = "WindowLayoutManager."; 15 | 16 | public void register(LayoutAction layoutAction) { 17 | PluginId pluginId = this.getPluginId(); 18 | String actionName = ActionNameGenerator.getActionNameForLayoutAction(layoutAction); 19 | 20 | ActionManager.getInstance().registerAction(actionName, layoutAction, pluginId); 21 | } 22 | 23 | public void register(AnAction action, String name) { 24 | PluginId pluginId = this.getPluginId(); 25 | String actionName = ACTION_PREFIX + name; 26 | 27 | ActionManager 28 | .getInstance() 29 | .registerAction(actionName, action, pluginId); 30 | } 31 | 32 | public void unregister(LayoutAction layoutAction) { 33 | String actionName = ActionNameGenerator.getActionNameForLayoutAction(layoutAction); 34 | ActionManager.getInstance().unregisterAction(actionName); 35 | } 36 | 37 | public void rename(LayoutAction layoutAction) { 38 | ActionManager actionManager = ActionManager.getInstance(); 39 | Keymap activeKeymap = KeymapManager.getInstance().getActiveKeymap(); 40 | String previousActionId = actionManager.getId(layoutAction); 41 | Shortcut[] shortcuts = activeKeymap.getShortcuts(previousActionId); 42 | 43 | if (shortcuts.length > 0) { 44 | String newActionName = ActionNameGenerator.getActionNameForLayoutAction(layoutAction); 45 | this.reRegisterAction(actionManager, layoutAction, previousActionId, newActionName); 46 | activeKeymap.removeAllActionShortcuts(Objects.requireNonNull(previousActionId)); 47 | 48 | for (Shortcut shortcut : shortcuts) { 49 | activeKeymap.addShortcut(newActionName, shortcut); 50 | } 51 | } 52 | } 53 | 54 | private void reRegisterAction(ActionManager actionManager, LayoutAction layoutAction, String previousActionId, String newActionName) { 55 | actionManager.unregisterAction(previousActionId); 56 | actionManager.registerAction(newActionName, layoutAction, this.getPluginId()); 57 | } 58 | 59 | 60 | private PluginId getPluginId() { 61 | return PluginId.getId(PLUGIN_ID); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/dialogs/LayoutNameDialog.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.dialogs; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.ui.Messages; 5 | import com.layoutmanager.localization.MessagesHelper; 6 | 7 | public class LayoutNameDialog { 8 | 9 | private final LayoutNameValidator layoutNameValidator; 10 | 11 | public LayoutNameDialog(LayoutNameValidator layoutNameValidator) { 12 | 13 | this.layoutNameValidator = layoutNameValidator; 14 | } 15 | 16 | public String show(String defaultName) { 17 | String name; 18 | do { 19 | name = Messages.showInputDialog( 20 | MessagesHelper.message("StoreLayout.Dialog.Title"), 21 | MessagesHelper.message("StoreLayout.Dialog.Content"), 22 | AllIcons.Actions.Edit, 23 | defaultName, 24 | null); 25 | } while (name != null && !this.layoutNameValidator.isValid(name)); 26 | 27 | return name; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/dialogs/LayoutNameValidator.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.dialogs; 2 | 3 | public class LayoutNameValidator { 4 | public Boolean isValid(String name) { 5 | return name != null && !this.isBlank(name); 6 | } 7 | 8 | private boolean isBlank(String name) { 9 | return name.trim().isEmpty(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/helpers/BalloonNotificationHelper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.helpers; 2 | 3 | import com.intellij.notification.Notification; 4 | import com.intellij.notification.NotificationGroupManager; 5 | import com.intellij.notification.NotificationType; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.project.ProjectManager; 8 | import java.util.Arrays; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class BalloonNotificationHelper { 14 | private static final int NOTIFICATION_DISPLAY_TIME_IN_SECONDS = 6; 15 | 16 | public static void info(String title, String content) { 17 | Notification notification = NotificationGroupManager.getInstance() 18 | .getNotificationGroup("WindowLayoutManager") 19 | .createNotification(title, content, NotificationType.INFORMATION); 20 | notify(notification); 21 | } 22 | 23 | public static void warning(String title, String content) { 24 | Notification notification = NotificationGroupManager.getInstance() 25 | .getNotificationGroup("WindowLayoutManager") 26 | .createNotification(title, content, NotificationType.WARNING); 27 | notify(notification); 28 | } 29 | 30 | private static void notify(Notification notification) { 31 | Project currentProject = getCurrentProject(); 32 | notification.notify(currentProject); 33 | hideAfterTimeSpan(notification); 34 | } 35 | 36 | private static void hideAfterTimeSpan(Notification notification) { 37 | ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); 38 | scheduledExecutorService.schedule( 39 | notification::expire, 40 | NOTIFICATION_DISPLAY_TIME_IN_SECONDS, 41 | TimeUnit.SECONDS); 42 | } 43 | 44 | private static Project getCurrentProject() { 45 | Project[] openedProjects = ProjectManager.getInstance().getOpenProjects(); 46 | return Arrays.stream(openedProjects) 47 | .findFirst() 48 | .orElse(null); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/helpers/ComponentNotificationHelper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.helpers; 2 | 3 | import com.intellij.openapi.ui.MessageType; 4 | import com.intellij.openapi.ui.popup.Balloon; 5 | import com.intellij.openapi.ui.popup.JBPopupFactory; 6 | import com.intellij.ui.awt.RelativePoint; 7 | import javax.swing.JComponent; 8 | 9 | public class ComponentNotificationHelper { 10 | 11 | public static void info(JComponent component, String message) { 12 | show(component, message, MessageType.INFO); 13 | } 14 | 15 | public static void error(JComponent component, String message) { 16 | show(component, message, MessageType.ERROR); 17 | } 18 | 19 | private static void show(JComponent component, String message, MessageType type) { 20 | JBPopupFactory.getInstance() 21 | .createHtmlTextBalloonBuilder(message, type, null) 22 | .setFadeoutTime(7500) 23 | .createBalloon() 24 | .show( 25 | RelativePoint.getCenterOf(component), 26 | Balloon.Position.above); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/helpers/ScreenSizeHelper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.helpers; 2 | 3 | import com.layoutmanager.persistence.ToolWindowInfo; 4 | import java.awt.GraphicsDevice; 5 | import java.awt.GraphicsEnvironment; 6 | import java.awt.Insets; 7 | import java.awt.Rectangle; 8 | import java.awt.Toolkit; 9 | import java.util.Arrays; 10 | import java.util.Comparator; 11 | 12 | public class ScreenSizeHelper { 13 | public static Rectangle getContainingScreenBounds(ToolWindowInfo toolWindow) { 14 | return Arrays 15 | .stream(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) 16 | .map(ScreenSizeHelper::getScreenRectangle) 17 | .min(Comparator.comparingInt(x -> -getIntersectionSize(x, toolWindow.getBounds()))) 18 | .orElse(new Rectangle()); 19 | } 20 | 21 | private static Rectangle getScreenRectangle(GraphicsDevice device) { 22 | Rectangle bounds = device.getDefaultConfiguration().getBounds(); 23 | Insets insets = Toolkit 24 | .getDefaultToolkit() 25 | .getScreenInsets(device.getDefaultConfiguration()); 26 | 27 | int x = bounds.x + insets.left; 28 | int y = bounds.y + insets.top; 29 | int width = bounds.width - (insets.left + insets.right); 30 | int height = bounds.height - (insets.top + insets.bottom); 31 | 32 | return new Rectangle(x, y, width, height); 33 | } 34 | 35 | private static int getIntersectionSize(Rectangle screen, Rectangle window) { 36 | return (int)(Math.max(0, Math.min(screen.getMaxX(), window.getMaxX()) - Math.max(screen.getX(), window.getX())) * 37 | Math.max(0, Math.min(screen.getMaxY(), window.getMaxY()) - Math.max(screen.getY(), window.getY()))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/helpers/ToolWindowHelper.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.helpers; 2 | 3 | import com.intellij.openapi.wm.ToolWindowAnchor; 4 | import com.intellij.openapi.wm.ToolWindowType; 5 | import com.intellij.openapi.wm.ex.ToolWindowEx; 6 | import java.awt.Rectangle; 7 | import java.awt.Window; 8 | import java.awt.event.HierarchyEvent; 9 | import java.awt.event.HierarchyListener; 10 | import javax.swing.SwingUtilities; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class ToolWindowHelper { 14 | public static Rectangle getBounds(ToolWindowEx toolWindow) { 15 | if (toolWindow.isVisible()) { 16 | if (toolWindow.getType() == ToolWindowType.FLOATING || toolWindow.getType() == ToolWindowType.WINDOWED) { 17 | Window window = getWindow(toolWindow); 18 | return window != null ? window.getBounds() : new Rectangle(100, 100); 19 | } else { 20 | return toolWindow 21 | .getComponent() 22 | .getBounds(); 23 | } 24 | } 25 | return new Rectangle(0, 0, 0, 0); 26 | } 27 | 28 | public static void setBounds(ToolWindowEx toolWindow, Rectangle bounds) { 29 | if (toolWindow.getType() == ToolWindowType.FLOATING || toolWindow.getType() == ToolWindowType.WINDOWED) { 30 | setWindowBounds(toolWindow, bounds); 31 | } else { 32 | Rectangle currentBounds = toolWindow 33 | .getComponent() 34 | .getBounds(); 35 | if (toolWindow.getAnchor() == ToolWindowAnchor.TOP || toolWindow.getAnchor() == ToolWindowAnchor.BOTTOM) { 36 | toolWindow.stretchHeight(bounds.height - currentBounds.height); 37 | } else { 38 | toolWindow.stretchWidth(bounds.width - currentBounds.width); 39 | } 40 | } 41 | } 42 | 43 | private static void setWindowBounds(ToolWindowEx toolWindow, Rectangle bounds) { 44 | SwingUtilities.invokeLater(() -> { 45 | Window window = getWindow(toolWindow); 46 | if (window != null) { 47 | window.setBounds(bounds); 48 | } else { 49 | toolWindow 50 | .getComponent() 51 | .addHierarchyListener(new HierarchyListener() { 52 | 53 | @Override 54 | public void hierarchyChanged(HierarchyEvent e) { 55 | if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) { 56 | Window window = getWindow(toolWindow); 57 | if (window != null) { 58 | window.setBounds(bounds); 59 | toolWindow 60 | .getComponent() 61 | .removeHierarchyListener(this); 62 | } 63 | } 64 | } 65 | }); 66 | } 67 | }); 68 | } 69 | 70 | @Nullable 71 | private static Window getWindow(ToolWindowEx toolWindow) { 72 | return SwingUtilities.getWindowAncestor(toolWindow.getComponent()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/icons/Icons.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.icons; 2 | 3 | import com.intellij.openapi.util.IconLoader; 4 | import javax.swing.Icon; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | 8 | public class Icons { 9 | public static final class Menu { 10 | @NotNull 11 | public static final Icon CreateNewLayout = IconLoader.getIcon("com/layoutmanager/ui/icons/NewLayout.svg", Icons.class); 12 | 13 | @NotNull 14 | public static final Icon OverwriteLayout = IconLoader.getIcon("com/layoutmanager/ui/icons/OverwriteLayout.svg", Icons.class); 15 | 16 | @NotNull 17 | public static final Icon RestoreLayout = IconLoader.getIcon("com/layoutmanager/ui/icons/RestoreLayout.svg", Icons.class); 18 | 19 | @NotNull 20 | public static final Icon DeleteLayout = IconLoader.getIcon("com/layoutmanager/ui/icons/DeleteLayout.svg", Icons.class); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/menu/WindowMenuService.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.menu; 2 | 3 | import com.intellij.openapi.actionSystem.ActionManager; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 6 | import com.layoutmanager.layout.LayoutAction; 7 | import com.layoutmanager.layout.delete.DeleteLayoutAction; 8 | import com.layoutmanager.layout.restore.RestoreLayoutAction; 9 | import com.layoutmanager.layout.store.LayoutCreator; 10 | import com.layoutmanager.layout.store.create.NewLayoutAction; 11 | import com.layoutmanager.layout.store.overwrite.OverwriteLayoutAction; 12 | import com.layoutmanager.layout.store.smartdock.SmartDockerFactory; 13 | import com.layoutmanager.localization.MessagesHelper; 14 | import com.layoutmanager.persistence.Layout; 15 | import com.layoutmanager.persistence.LayoutConfig; 16 | import com.layoutmanager.ui.action.ActionRegistry; 17 | import com.layoutmanager.ui.dialogs.LayoutNameDialog; 18 | import com.layoutmanager.ui.dialogs.LayoutNameValidator; 19 | import java.util.Arrays; 20 | 21 | public class WindowMenuService { 22 | private final ActionRegistry actionRegistry; 23 | 24 | private DefaultActionGroup storeLayout; 25 | private DefaultActionGroup restoreLayout; 26 | private DefaultActionGroup deleteLayout; 27 | 28 | public WindowMenuService() { 29 | this.actionRegistry = new ActionRegistry(); 30 | } 31 | 32 | public void create() { 33 | DefaultActionGroup windowMenu = (DefaultActionGroup) ActionManager 34 | .getInstance() 35 | .getAction("WindowMenu"); 36 | this.createMainActions(windowMenu); 37 | this.createStoreRestoreActions(); 38 | } 39 | 40 | public void addLayout(Layout layout) { 41 | LayoutCreator layoutCreator = this.createLayoutCreator(); 42 | 43 | this.addStoreLayoutActionBeforeSeparator(layout, layoutCreator); 44 | this.addRestoreLayoutAction(layout); 45 | this.addDeleteLayout(layout); 46 | } 47 | 48 | public void deleteLayout(Layout layout) { 49 | this.deleteActionInGroup(layout, this.storeLayout, false); 50 | this.deleteActionInGroup(layout, this.restoreLayout, true); 51 | this.deleteActionInGroup(layout, this.deleteLayout, false); 52 | } 53 | 54 | public void renameLayout(Layout layout) { 55 | LayoutAction layoutAction = this.getActionForLayout(this.restoreLayout, layout); 56 | this.actionRegistry.rename(layoutAction); 57 | } 58 | 59 | private void deleteActionInGroup(Layout layout, DefaultActionGroup actionGroup, boolean unregister) { 60 | LayoutAction layoutAction = this.getActionForLayout(actionGroup, layout); 61 | 62 | if (unregister) { 63 | this.actionRegistry.unregister(layoutAction); 64 | } 65 | 66 | actionGroup.remove(layoutAction); 67 | } 68 | 69 | private LayoutAction getActionForLayout(DefaultActionGroup group, Layout layout) { 70 | return Arrays 71 | .stream(group.getChildActionsOrStubs()) 72 | .filter(x -> 73 | x instanceof LayoutAction && 74 | ((LayoutAction)x).getLayout() == layout) 75 | .map(x -> (LayoutAction)x) 76 | .findFirst() 77 | .orElse(null); 78 | } 79 | 80 | private LayoutCreator createLayoutCreator() { 81 | LayoutConfig config = LayoutConfig.getInstance(); 82 | return new LayoutCreator( 83 | config.getSettings(), 84 | new SmartDockerFactory(), 85 | new LayoutNameDialog(new LayoutNameValidator())); 86 | } 87 | 88 | private void createMainActions(DefaultActionGroup windowMenu) { 89 | this.storeLayout = this.createMainAction(MessagesHelper.message("StoreLayout.Menu"), windowMenu); 90 | this.restoreLayout = this.createMainAction(MessagesHelper.message("RestoreLayout.Menu"), windowMenu); 91 | this.deleteLayout = this.createMainAction(MessagesHelper.message("DeleteLayout.Menu"), windowMenu); 92 | } 93 | 94 | private DefaultActionGroup createMainAction(String name, DefaultActionGroup windowMenu) { 95 | DefaultActionGroup windowMenuItem = new DefaultActionGroup(name, true); 96 | windowMenu.add(windowMenuItem); 97 | 98 | return windowMenuItem; 99 | } 100 | 101 | private void createStoreRestoreActions() { 102 | LayoutConfig config = LayoutConfig.getInstance(); 103 | this.addStoreLayoutActions(config); 104 | this.addRestoreLayoutActions(config); 105 | this.addDeleteLayoutActions(config); 106 | } 107 | 108 | private void addStoreLayoutActions(LayoutConfig config) { 109 | LayoutCreator layoutCreator = this.createLayoutCreator(); 110 | 111 | for (Layout layout : config.getLayouts()) { 112 | this.addOverwriteLayoutActionAtTheEnd(layout, layoutCreator); 113 | } 114 | 115 | if (config.getLayoutCount() > 0) { 116 | this.storeLayout.addSeparator(); 117 | } 118 | 119 | int nextAvailableId = config.getNextAvailableId(); 120 | 121 | NewLayoutAction newLayoutAction = new NewLayoutAction(layoutCreator, nextAvailableId); 122 | this.storeLayout.add(newLayoutAction); 123 | this.actionRegistry.register(newLayoutAction, "NewLayout"); 124 | } 125 | 126 | private void addOverwriteLayoutActionAtTheEnd(Layout layout, LayoutCreator layoutCreator) { 127 | this.storeLayout.add(new OverwriteLayoutAction(layoutCreator, layout)); 128 | } 129 | 130 | private void addStoreLayoutActionBeforeSeparator(Layout layout, LayoutCreator layoutCreator) { 131 | AnAction[] actions = this.storeLayout.getChildActionsOrStubs(); 132 | AnAction newLayoutAction = actions[actions.length - 1]; 133 | 134 | this.removeSeparator(actions); 135 | this.storeLayout.remove(newLayoutAction); 136 | 137 | this.addOverwriteLayoutActionAtTheEnd(layout, layoutCreator); 138 | 139 | this.storeLayout.addSeparator(); 140 | this.storeLayout.add(newLayoutAction); 141 | } 142 | 143 | private void removeSeparator(AnAction[] actions) { 144 | boolean hasSeparator = actions.length > 1; 145 | if (hasSeparator) { 146 | AnAction separator = actions[actions.length - 2]; 147 | this.storeLayout.remove(separator); 148 | } 149 | } 150 | 151 | private void addRestoreLayoutActions(LayoutConfig config) { 152 | for (Layout layout : config.getLayouts()) { 153 | this.addRestoreLayoutAction(layout); 154 | } 155 | } 156 | 157 | private void addRestoreLayoutAction(Layout layout) { 158 | RestoreLayoutAction restoreLayoutAction = new RestoreLayoutAction(layout); 159 | this.restoreLayout.add(restoreLayoutAction); 160 | this.actionRegistry.register(restoreLayoutAction); 161 | } 162 | 163 | private void addDeleteLayoutActions(LayoutConfig config) { 164 | for (Layout layout : config.getLayouts()) { 165 | this.addDeleteLayout(layout); 166 | } 167 | } 168 | 169 | private void addDeleteLayout(Layout layout) { 170 | this.deleteLayout.add(new DeleteLayoutAction(layout)); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/EditLayout.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | import com.layoutmanager.persistence.Layout; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public record EditLayout(Layout originalLayout, Layout editedLayout) { 7 | public EditLayout( 8 | Layout originalLayout, 9 | @NotNull Layout editedLayout) { 10 | this.originalLayout = originalLayout; 11 | this.editedLayout = editedLayout; 12 | } 13 | 14 | public boolean nameHasChanged() { 15 | return this.originalLayout() == null || 16 | !this.originalLayout.getName().equals(this.editedLayout.getName()); 17 | } 18 | 19 | public void applyNameChange() { 20 | this.originalLayout.setName(this.editedLayout.getName()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/ImportExportConstants.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | public class ImportExportConstants { 4 | public static final String FILE_ENDING = "wl"; 5 | 6 | public static final String FILE_ENDING_WITH_DOT = "." + FILE_ENDING; 7 | 8 | public static final String FILE_TYPE_NAME = "Window layouts"; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/LayoutDuplicator.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | import com.layoutmanager.persistence.Layout; 4 | 5 | public class LayoutDuplicator { 6 | private final LayoutSerializer serializer; 7 | 8 | public LayoutDuplicator(LayoutSerializer serializer) { 9 | this.serializer = serializer; 10 | } 11 | 12 | public Layout duplicate(Layout origin) { 13 | return this.serializer.deserialize(this.serializer.serialize(origin)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.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 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | import static com.intellij.openapi.keymap.impl.ui.KeymapPanel.addKeyboardShortcut; 4 | import static javax.swing.JComponent.WHEN_FOCUSED; 5 | 6 | import com.intellij.ide.IdeBundle; 7 | import com.intellij.openapi.actionSystem.ActionManager; 8 | import com.intellij.openapi.actionSystem.AnActionEvent; 9 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 10 | import com.intellij.openapi.actionSystem.Separator; 11 | import com.intellij.openapi.actionSystem.Shortcut; 12 | import com.intellij.openapi.keymap.KeyMapBundle; 13 | import com.intellij.openapi.keymap.Keymap; 14 | import com.intellij.openapi.keymap.KeymapManager; 15 | import com.intellij.openapi.keymap.KeymapUtil; 16 | import com.intellij.openapi.keymap.impl.ActionShortcutRestrictions; 17 | import com.intellij.openapi.keymap.impl.ShortcutRestrictions; 18 | import com.intellij.openapi.keymap.impl.SystemShortcuts; 19 | import com.intellij.openapi.project.DumbAwareAction; 20 | import com.intellij.openapi.util.text.StringUtil; 21 | import com.layoutmanager.localization.MessagesHelper; 22 | import com.layoutmanager.persistence.Layout; 23 | import com.layoutmanager.persistence.LayoutConfig; 24 | import com.layoutmanager.ui.action.ActionNameGenerator; 25 | import com.layoutmanager.ui.dialogs.LayoutNameDialog; 26 | import com.layoutmanager.ui.dialogs.LayoutNameValidator; 27 | import com.layoutmanager.ui.helpers.ComponentNotificationHelper; 28 | import com.layoutmanager.ui.settings.exporting.ExportDialog; 29 | import com.layoutmanager.ui.settings.importing.ImportDialog; 30 | import java.awt.event.ActionEvent; 31 | import java.awt.event.KeyEvent; 32 | import java.awt.event.MouseAdapter; 33 | import java.awt.event.MouseEvent; 34 | import java.util.ArrayList; 35 | import java.util.Arrays; 36 | import java.util.Collections; 37 | import java.util.stream.Collectors; 38 | import javax.swing.AbstractAction; 39 | import javax.swing.ActionMap; 40 | import javax.swing.InputMap; 41 | import javax.swing.JButton; 42 | import javax.swing.JCheckBox; 43 | import javax.swing.JDialog; 44 | import javax.swing.JPanel; 45 | import javax.swing.JTable; 46 | import javax.swing.KeyStroke; 47 | import javax.swing.ListSelectionModel; 48 | import javax.swing.SwingUtilities; 49 | import javax.swing.event.TableModelEvent; 50 | import javax.swing.table.DefaultTableModel; 51 | import org.jetbrains.annotations.NotNull; 52 | import org.jetbrains.annotations.Nullable; 53 | 54 | public class LayoutManagerSettingsPanel { 55 | private final LayoutConfig layoutConfig; 56 | private final LayoutNameDialog layoutNameDialog; 57 | private final LayoutNameValidator layoutNameValidator; 58 | private final LayoutSerializer layoutSerializer; 59 | private final LayoutDuplicator layoutDuplicator; 60 | private final WindowMenuChangesApplier windowMenuChangesApplier; 61 | private final ArrayList editLayouts = new ArrayList<>(); 62 | 63 | private JCheckBox useSmartDockingCheckbox; 64 | private JButton deleteButton; 65 | private JButton renameButton; 66 | private JButton exportButton; 67 | private JButton importButton; 68 | private JPanel settingsPanel; 69 | private JTable layoutsTable; 70 | 71 | public LayoutManagerSettingsPanel( 72 | LayoutConfig layoutConfig, 73 | LayoutNameDialog layoutNameDialog, 74 | LayoutNameValidator layoutNameValidator, 75 | LayoutSerializer layoutSerializer, 76 | LayoutDuplicator layoutDuplicator, 77 | WindowMenuChangesApplier windowMenuChangesApplier) { 78 | this.layoutConfig = layoutConfig; 79 | this.layoutNameDialog = layoutNameDialog; 80 | this.layoutNameValidator = layoutNameValidator; 81 | this.layoutSerializer = layoutSerializer; 82 | this.layoutDuplicator = layoutDuplicator; 83 | this.windowMenuChangesApplier = windowMenuChangesApplier; 84 | 85 | this.loadSettings(layoutConfig); 86 | 87 | this.layoutsTable 88 | .getSelectionModel() 89 | .addListSelectionListener(listSelectionEvent -> this.selectedLayoutChanged()); 90 | this.deleteButton.addActionListener(e -> this.deleteLayout()); 91 | this.renameButton.addActionListener(e -> this.renameLayout()); 92 | this.exportButton.addActionListener(actionEvent -> this.exportLayout()); 93 | this.importButton.addActionListener(actionEvent -> this.importLayout()); 94 | } 95 | 96 | private void loadSettings(LayoutConfig layoutConfig) { 97 | Collections.addAll( 98 | this.editLayouts, 99 | Arrays 100 | .stream(layoutConfig.getLayouts()) 101 | .map(x -> new EditLayout(x, this.layoutDuplicator.duplicate(x))) 102 | .toArray(EditLayout[]::new)); 103 | 104 | DefaultTableModel table = this.createTableContent(); 105 | this.setKeyBindings(table); 106 | this.layoutsTable.setModel(table); 107 | this.layoutsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 108 | this.layoutsTable.addMouseListener(new DoubleClickOpenShortcutListener()); 109 | 110 | this.useSmartDockingCheckbox.setSelected(layoutConfig.getSettings().getUseSmartDock()); 111 | } 112 | 113 | private void setKeyBindings(DefaultTableModel tableModel) { 114 | InputMap inputMap = this.layoutsTable.getInputMap(WHEN_FOCUSED); 115 | ActionMap actionMap = this.layoutsTable.getActionMap(); 116 | 117 | inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"); 118 | actionMap.put("delete", new AbstractAction() { 119 | public void actionPerformed(ActionEvent evt) { 120 | int selectedRow = LayoutManagerSettingsPanel.this.layoutsTable.getSelectedRow(); 121 | if (selectedRow >= 0) { 122 | tableModel.removeRow(selectedRow); 123 | } 124 | } 125 | }); 126 | } 127 | 128 | private void selectedLayoutChanged() { 129 | this.deleteButton.setEnabled(this.layoutsTable.getSelectedRow() >= 0); 130 | this.renameButton.setEnabled(this.layoutsTable.getSelectedRow() >= 0); 131 | this.exportButton.setEnabled(this.layoutsTable.getSelectedRow() >= 0); 132 | } 133 | 134 | private void deleteLayout() { 135 | DefaultTableModel table = (DefaultTableModel) this.layoutsTable.getModel(); 136 | table.removeRow(this.layoutsTable.getSelectedRow()); 137 | } 138 | 139 | private void renameLayout() { 140 | int selectedRow = this.layoutsTable.getSelectedRow(); 141 | String newName = this.layoutNameDialog.show( 142 | this.editLayouts 143 | .get(selectedRow) 144 | .editedLayout() 145 | .getName()); 146 | 147 | if (newName != null) { 148 | this.layoutsTable.setValueAt(newName, selectedRow, 0); 149 | } 150 | } 151 | 152 | private void exportLayout() { 153 | int selectedRow = this.layoutsTable.getSelectedRow(); 154 | EditLayout selectedLayout = this.editLayouts.get(selectedRow); 155 | 156 | String encodedContent = this.layoutSerializer.serialize(selectedLayout.editedLayout()); 157 | this.showExportDialog(selectedLayout.editedLayout(), encodedContent); 158 | } 159 | 160 | private void showExportDialog(Layout selectedLayout, String encodedContent) { 161 | ExportDialog exportDialog = new ExportDialog(selectedLayout.getName(), encodedContent); 162 | JDialog parent = this.getParentDialog(); 163 | exportDialog.showDialogInCenterOf(parent); 164 | } 165 | 166 | private void importLayout() { 167 | ImportDialog importDialog = new ImportDialog(this.layoutNameValidator, this.layoutSerializer); 168 | JDialog parent = this.getParentDialog(); 169 | if (importDialog.showDialogInCenterOf(parent) == ImportDialog.OK_RESULT) { 170 | this.editLayouts.add(new EditLayout(null, importDialog.getImportedLayout())); 171 | ((DefaultTableModel) this.layoutsTable.getModel()).fireTableDataChanged(); 172 | } 173 | } 174 | 175 | @Nullable 176 | private JDialog getParentDialog() { 177 | return (JDialog) SwingUtilities.getAncestorOfClass(JDialog.class, this.settingsPanel); 178 | } 179 | 180 | public boolean hasChanged() { 181 | return this.useSmartDockingCheckbox.isSelected() != this.layoutConfig.getSettings().getUseSmartDock() || 182 | this.layoutsHasBeenAddedOrRemoved() || 183 | this.layoutsHasBeenRenamed(); 184 | } 185 | 186 | private boolean layoutsHasBeenAddedOrRemoved() { 187 | return !Arrays.equals( 188 | this.layoutConfig.getLayouts(), 189 | this.editLayouts 190 | .stream() 191 | .map(EditLayout::originalLayout) 192 | .toArray(Layout[]::new)); 193 | } 194 | 195 | private boolean layoutsHasBeenRenamed() { 196 | return this.editLayouts 197 | .stream() 198 | .anyMatch(x -> !x.originalLayout().getName().equals(x.editedLayout().getName())); 199 | } 200 | 201 | private void keymapChanged() { 202 | this.layoutsTable.repaint(); 203 | } 204 | 205 | public void apply() { 206 | this.windowMenuChangesApplier.apply(this.editLayouts, this.layoutConfig.getLayouts()); 207 | 208 | this.layoutConfig.getSettings().setUseSmartDock(this.useSmartDockingCheckbox.isSelected()); 209 | this.layoutConfig.setLayouts(this.editLayouts 210 | .stream() 211 | .map(x -> 212 | x.originalLayout() == null ? 213 | x.editedLayout() : 214 | x.originalLayout()) 215 | .toArray(Layout[]::new)); 216 | } 217 | 218 | public JPanel getPanel() { 219 | return this.settingsPanel; 220 | } 221 | 222 | @NotNull 223 | private DefaultTableModel createTableContent() { 224 | return new DefaultTableModel( 225 | new String[]{ 226 | MessagesHelper.message("SettingsPage.NameColumn"), 227 | MessagesHelper.message("SettingsPage.ConfiguredWindowsColumn"), 228 | MessagesHelper.message("SettingsPage.ShortcutColumn") 229 | }, 230 | this.editLayouts.size()) { 231 | 232 | @Override 233 | public boolean isCellEditable(int row, int column) { 234 | return column == 0; 235 | } 236 | 237 | @Override 238 | public void setValueAt(Object value, int row, int column) { 239 | String newLayoutName = value.toString(); 240 | if (LayoutManagerSettingsPanel.this.layoutNameValidator.isValid(newLayoutName)) { 241 | LayoutManagerSettingsPanel.this.editLayouts 242 | .get(row) 243 | .editedLayout() 244 | .setName(value.toString()); 245 | 246 | this.fireTableChanged(new TableModelEvent(this, row)); 247 | } else { 248 | ComponentNotificationHelper.error( 249 | LayoutManagerSettingsPanel.this.layoutsTable, 250 | MessagesHelper.message("LayoutNameValidation.InvalidName")); 251 | } 252 | } 253 | 254 | @Override 255 | public int getRowCount() { 256 | return LayoutManagerSettingsPanel.this.editLayouts.size(); 257 | } 258 | 259 | @Override 260 | public void removeRow(int row) { 261 | LayoutManagerSettingsPanel.this.editLayouts.remove(row); 262 | this.fireTableRowsDeleted(row, row); 263 | } 264 | 265 | @Override 266 | public Object getValueAt(int row, int column) { 267 | Layout layout = LayoutManagerSettingsPanel.this.editLayouts.get(row).editedLayout(); 268 | 269 | return switch (column) { 270 | case 0 -> layout.getName(); 271 | case 1 -> layout.getToolWindows().length; 272 | case 2 -> this.getKeyMap(layout); 273 | default -> null; 274 | }; 275 | } 276 | 277 | private String getKeyMap(Layout layout) { 278 | Keymap keymap = KeymapManager.getInstance().getActiveKeymap(); 279 | String actionName = ActionNameGenerator.getActionNameForLayout(layout); 280 | 281 | Shortcut[] shortcuts = keymap.getShortcuts(actionName); 282 | return Arrays.stream(shortcuts) 283 | .map(KeymapUtil::getShortcutText) 284 | .collect(Collectors.joining(", ")); 285 | } 286 | }; 287 | } 288 | 289 | private @NotNull DefaultActionGroup createEditActionGroup(@NotNull String actionId, Keymap selectedKeymap) { 290 | DefaultActionGroup group = new DefaultActionGroup(); 291 | final ShortcutRestrictions restrictions = ActionShortcutRestrictions.getInstance().getForActionId(actionId); 292 | if (restrictions.allowKeyboardShortcut) { 293 | group.add(new AddKeyboardShortcutAction(actionId, restrictions, selectedKeymap)); 294 | } 295 | 296 | group.addSeparator(); 297 | 298 | Shortcut[] shortcuts = selectedKeymap.getShortcuts(actionId); 299 | for (Shortcut shortcut : shortcuts) { 300 | group.add(new RemoveShortcutAction(shortcut, selectedKeymap, actionId)); 301 | } 302 | 303 | if (shortcuts.length > 2) { 304 | group.add(new Separator()); 305 | group.add(new RemoveAllShortcuts(selectedKeymap, actionId)); 306 | } 307 | 308 | return group; 309 | } 310 | 311 | private class DoubleClickOpenShortcutListener extends MouseAdapter { 312 | @Override 313 | public void mouseClicked(MouseEvent e) { 314 | if (e.getClickCount() == 2 || SwingUtilities.isRightMouseButton(e)) { 315 | JTable table = (JTable) e.getSource(); 316 | int column = table.getSelectedColumn(); 317 | if (column == 2) { 318 | EditLayout layout = LayoutManagerSettingsPanel.this.editLayouts.get(table.getSelectedRow()); 319 | String actionId = ActionNameGenerator.getActionNameForLayout(layout.editedLayout()); 320 | 321 | DefaultActionGroup group = LayoutManagerSettingsPanel.this.createEditActionGroup( 322 | actionId, 323 | KeymapManager.getInstance().getActiveKeymap()); 324 | ActionManager.getInstance() 325 | .createActionPopupMenu("popup@Keymap.ActionsTree.Menu", group) 326 | .getComponent() 327 | .show(e.getComponent(), e.getX(), e.getY()); 328 | } 329 | } 330 | } 331 | } 332 | 333 | private final class AddKeyboardShortcutAction extends DumbAwareAction { 334 | private final @NotNull String actionId; 335 | private final ShortcutRestrictions restrictions; 336 | private final Keymap keymap; 337 | 338 | private AddKeyboardShortcutAction(@NotNull String actionId, ShortcutRestrictions restrictions, Keymap keymap) { 339 | super(IdeBundle.messagePointer("action.Anonymous.text.add.keyboard.shortcut")); 340 | this.actionId = actionId; 341 | this.restrictions = restrictions; 342 | this.keymap = keymap; 343 | } 344 | 345 | @Override 346 | public void actionPerformed(@NotNull AnActionEvent e) { 347 | addKeyboardShortcut( 348 | this.actionId, 349 | this.restrictions, 350 | this.keymap, 351 | LayoutManagerSettingsPanel.this.layoutsTable, 352 | null, 353 | SystemShortcuts.getInstance()); 354 | LayoutManagerSettingsPanel.this.keymapChanged(); 355 | } 356 | } 357 | 358 | private final class RemoveShortcutAction extends DumbAwareAction { 359 | private final Shortcut shortcut; 360 | private final Keymap keymap; 361 | private final @NotNull String actionId; 362 | 363 | private RemoveShortcutAction(Shortcut shortcut, Keymap keymap, @NotNull String actionId) { 364 | super(IdeBundle.message("action.text.remove.0", KeymapUtil.getShortcutText(shortcut))); 365 | this.shortcut = shortcut; 366 | this.keymap = keymap; 367 | this.actionId = actionId; 368 | } 369 | 370 | @Override 371 | public void actionPerformed(@NotNull AnActionEvent e) { 372 | this.keymap.removeShortcut(this.actionId, this.shortcut); 373 | if (StringUtil.startsWithChar(this.actionId, '$')) { 374 | this.keymap.removeShortcut(KeyMapBundle.message("editor.shortcut", this.actionId.substring(1)), this.shortcut); 375 | } 376 | LayoutManagerSettingsPanel.this.keymapChanged(); 377 | } 378 | } 379 | 380 | private final class RemoveAllShortcuts extends DumbAwareAction { 381 | private final Keymap keymap; 382 | private final String actionId; 383 | 384 | private RemoveAllShortcuts(Keymap selectedKeymap, @NotNull String actionId) { 385 | super(IdeBundle.messagePointer("action.text.remove.all.shortcuts")); 386 | this.keymap = selectedKeymap; 387 | this.actionId = actionId; 388 | } 389 | 390 | @Override 391 | public void actionPerformed(@NotNull AnActionEvent event) { 392 | this.keymap.removeAllActionShortcuts(this.actionId); 393 | LayoutManagerSettingsPanel.this.keymapChanged(); 394 | } 395 | } 396 | } -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/LayoutSerializer.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | import blazing.chain.LZSEncoding; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.layoutmanager.persistence.Layout; 7 | 8 | public class LayoutSerializer { 9 | public String serialize(Layout layout) { 10 | Gson gson = new GsonBuilder().create(); 11 | String jsonContent = gson.toJson(layout); 12 | return LZSEncoding.compressToBase64(jsonContent); 13 | } 14 | 15 | public Layout deserialize(String encodedContent) { 16 | String jsonContent = LZSEncoding.decompressFromBase64(encodedContent); 17 | Gson gson = new GsonBuilder().create(); 18 | return gson.fromJson(jsonContent, Layout.class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/SettingsPage.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | import com.intellij.openapi.options.Configurable; 4 | import com.layoutmanager.localization.MessagesHelper; 5 | import com.layoutmanager.persistence.LayoutConfig; 6 | import com.layoutmanager.ui.dialogs.LayoutNameDialog; 7 | import com.layoutmanager.ui.dialogs.LayoutNameValidator; 8 | import javax.swing.JComponent; 9 | import org.jetbrains.annotations.Nls; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | public class SettingsPage implements Configurable { 13 | private final LayoutManagerSettingsPanel panel; 14 | 15 | public SettingsPage() { 16 | LayoutNameValidator layoutNameValidator = new LayoutNameValidator(); 17 | LayoutSerializer layoutSerializer = new LayoutSerializer(); 18 | this.panel = new LayoutManagerSettingsPanel( 19 | LayoutConfig.getInstance(), 20 | new LayoutNameDialog(layoutNameValidator), 21 | layoutNameValidator, 22 | layoutSerializer, 23 | new LayoutDuplicator(layoutSerializer), 24 | new WindowMenuChangesApplier()); 25 | } 26 | 27 | @Nls(capitalization = Nls.Capitalization.Title) 28 | @Override 29 | public String getDisplayName() { 30 | return MessagesHelper.message("SettingsPage.Title"); 31 | } 32 | 33 | @Nullable 34 | @Override 35 | public JComponent createComponent() { 36 | return this.panel.getPanel(); 37 | } 38 | 39 | @Override 40 | public boolean isModified() { 41 | return this.panel.hasChanged(); 42 | } 43 | 44 | @Override 45 | public void apply() { 46 | this.panel.apply(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/WindowMenuChangesApplier.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.layoutmanager.persistence.Layout; 5 | import com.layoutmanager.ui.menu.WindowMenuService; 6 | import java.util.List; 7 | 8 | public class WindowMenuChangesApplier { 9 | public void apply(List editedLayouts, Layout[] originalLayouts) { 10 | WindowMenuService windowMenuService = ApplicationManager 11 | .getApplication() 12 | .getService(WindowMenuService.class); 13 | 14 | this.addOrUpdateLayouts(editedLayouts, windowMenuService); 15 | 16 | this.removeObsoleteLayouts(editedLayouts, originalLayouts, windowMenuService); 17 | } 18 | 19 | private void addOrUpdateLayouts(List editedLayouts, WindowMenuService windowMenuService) { 20 | for (EditLayout editLayout : editedLayouts) { 21 | if (this.isNew(editLayout)) { 22 | windowMenuService.addLayout(editLayout.editedLayout()); 23 | } else if (editLayout.nameHasChanged()) { 24 | editLayout.applyNameChange(); 25 | windowMenuService.renameLayout(editLayout.originalLayout()); 26 | } 27 | } 28 | } 29 | 30 | private boolean isNew(EditLayout editLayout) { 31 | return editLayout.originalLayout() == null; 32 | } 33 | 34 | private void removeObsoleteLayouts(List editedLayouts, Layout[] originalLayouts, WindowMenuService windowMenuService) { 35 | for (Layout layout : originalLayouts) { 36 | if (editedLayouts.stream().noneMatch(x -> x.originalLayout().equals(layout))) { 37 | windowMenuService.deleteLayout(layout); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.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 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings.exporting; 2 | 3 | import com.layoutmanager.localization.MessagesHelper; 4 | import com.layoutmanager.ui.helpers.ComponentNotificationHelper; 5 | import com.layoutmanager.ui.settings.ImportExportConstants; 6 | import java.awt.Toolkit; 7 | import java.awt.datatransfer.StringSelection; 8 | import java.awt.event.KeyEvent; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import javax.swing.JButton; 15 | import javax.swing.JComponent; 16 | import javax.swing.JDialog; 17 | import javax.swing.JFileChooser; 18 | import javax.swing.JLabel; 19 | import javax.swing.JPanel; 20 | import javax.swing.JTextArea; 21 | import javax.swing.KeyStroke; 22 | import javax.swing.SwingUtilities; 23 | import javax.swing.filechooser.FileNameExtensionFilter; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | public class ExportDialog extends JDialog { 27 | private JTextArea exportTextBox; 28 | private JPanel contentPanel; 29 | private JButton exportToFileButton; 30 | private JButton exportToClipboardButton; 31 | private JButton closeButton; 32 | private JLabel layoutNameLabel; 33 | 34 | private final String layoutName; 35 | private final String content; 36 | 37 | public ExportDialog(String layoutName, String content) { 38 | this.layoutName = layoutName; 39 | this.content = content; 40 | 41 | this.setContentPane(this.contentPanel); 42 | this.setModal(true); 43 | this.getRootPane().setDefaultButton(this.closeButton); 44 | 45 | this.exportToClipboardButton.addActionListener(actionEvent -> this.exportToClipboard()); 46 | this.exportToFileButton.addActionListener(actionEvent -> this.exportToFile()); 47 | this.closeButton.addActionListener(actionEvent -> this.onClose()); 48 | 49 | this.layoutNameLabel.setText(layoutName); 50 | this.exportTextBox.setText(content); 51 | 52 | this.contentPanel.registerKeyboardAction( 53 | e -> this.onClose(), 54 | KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), 55 | JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 56 | } 57 | 58 | public void showDialogInCenterOf(JDialog parent) { 59 | this.setTitle(MessagesHelper.message("ExportDialog.Title")); 60 | this.setSize(this.getPreferredSize()); 61 | this.setLocationRelativeTo(parent); 62 | this.setVisible(true); 63 | } 64 | 65 | private void exportToClipboard() { 66 | this.exportTextBox.requestFocus(); 67 | this.exportTextBox.selectAll(); 68 | Toolkit 69 | .getDefaultToolkit() 70 | .getSystemClipboard() 71 | .setContents( 72 | new StringSelection(this.exportTextBox.getText()), 73 | null); 74 | ComponentNotificationHelper.info(this.exportToClipboardButton, MessagesHelper.message("ExportDialog.CopiedToClipboard")); 75 | } 76 | 77 | private void exportToFile() { 78 | File selectedFile = this.selectFile(); 79 | if (selectedFile != null) { 80 | Path path = this.getPath(selectedFile); 81 | this.writeContentToFile(path); 82 | } 83 | } 84 | 85 | private File selectFile() { 86 | JFileChooser fileChooser = new JFileChooser(); 87 | fileChooser.setDialogTitle(MessagesHelper.message("ExportDialog.SaveFileTitle")); 88 | fileChooser.setSelectedFile(new File(this.layoutName + ImportExportConstants.FILE_ENDING_WITH_DOT)); 89 | fileChooser.setAcceptAllFileFilterUsed(false); 90 | FileNameExtensionFilter filter = new FileNameExtensionFilter(ImportExportConstants.FILE_TYPE_NAME, ImportExportConstants.FILE_ENDING); 91 | fileChooser.setFileFilter(filter); 92 | 93 | int result = fileChooser.showSaveDialog(null); 94 | return result == JFileChooser.APPROVE_OPTION ? 95 | fileChooser.getSelectedFile() : 96 | null; 97 | } 98 | 99 | @NotNull 100 | private Path getPath(File selectedFile) { 101 | String fullPath = selectedFile.getAbsolutePath(); 102 | if (!fullPath.toLowerCase().endsWith(ImportExportConstants.FILE_ENDING_WITH_DOT)) { 103 | fullPath += ImportExportConstants.FILE_ENDING_WITH_DOT; 104 | } 105 | 106 | return Paths.get(fullPath); 107 | } 108 | 109 | private void writeContentToFile(Path path) { 110 | try { 111 | byte[] encodedContent = this.content.getBytes(); 112 | Files.write(path, encodedContent); 113 | ComponentNotificationHelper.info(this.exportToFileButton, MessagesHelper.message("ExportDialog.SavedTo", path.getFileName().toString())); 114 | } catch (IOException e) { 115 | ComponentNotificationHelper.error(this.exportToFileButton, MessagesHelper.message("ExportDialog.FailedToWriteFile", e.getMessage())); 116 | } 117 | } 118 | 119 | private void onClose() { 120 | JDialog window = (JDialog) SwingUtilities.getAncestorOfClass(JDialog.class, this.contentPanel); 121 | window.dispose(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.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 | -------------------------------------------------------------------------------- /src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.java: -------------------------------------------------------------------------------- 1 | package com.layoutmanager.ui.settings.importing; 2 | 3 | import com.layoutmanager.localization.MessagesHelper; 4 | import com.layoutmanager.persistence.Layout; 5 | import com.layoutmanager.ui.dialogs.LayoutNameValidator; 6 | import com.layoutmanager.ui.helpers.ComponentNotificationHelper; 7 | import com.layoutmanager.ui.settings.ImportExportConstants; 8 | import com.layoutmanager.ui.settings.LayoutSerializer; 9 | import java.awt.Toolkit; 10 | import java.awt.datatransfer.DataFlavor; 11 | import java.awt.event.KeyEvent; 12 | import java.awt.event.WindowAdapter; 13 | import java.awt.event.WindowEvent; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import javax.swing.JButton; 19 | import javax.swing.JComponent; 20 | import javax.swing.JDialog; 21 | import javax.swing.JFileChooser; 22 | import javax.swing.JLabel; 23 | import javax.swing.JPanel; 24 | import javax.swing.JTextField; 25 | import javax.swing.KeyStroke; 26 | import javax.swing.filechooser.FileNameExtensionFilter; 27 | 28 | public class ImportDialog extends JDialog { 29 | public static final int OK_RESULT = 1; 30 | public static final int ABORT_RESULT = 0; 31 | private final LayoutNameValidator layoutNameValidator; 32 | private final LayoutSerializer layoutSerializer; 33 | 34 | private JPanel contentPanel; 35 | private JButton importButton; 36 | private JButton abortButton; 37 | private JButton importFromFileButton; 38 | private JButton importFromClipboardButton; 39 | private JLabel layoutConfiguredWindowCountLabel; 40 | private JTextField layoutNameTextBox; 41 | 42 | private Layout importedLayout; 43 | private int result; 44 | 45 | public ImportDialog( 46 | LayoutNameValidator layoutNameValidator, 47 | LayoutSerializer layoutSerializer) { 48 | this.layoutNameValidator = layoutNameValidator; 49 | this.layoutSerializer = layoutSerializer; 50 | 51 | this.setContentPane(this.contentPanel); 52 | this.setModal(true); 53 | this.setResizable(false); 54 | this.getRootPane().setDefaultButton(this.importButton); 55 | 56 | this.importButton.addActionListener(e -> this.onOK()); 57 | this.abortButton.addActionListener(e -> this.onCancel()); 58 | this.importFromClipboardButton.addActionListener(actionEvent -> this.importFromClipboard()); 59 | this.importFromFileButton.addActionListener(actionEvent -> this.importFromFile()); 60 | 61 | // call onCancel() when cross is clicked 62 | this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 63 | this.addWindowListener(new WindowAdapter() { 64 | public void windowClosing(WindowEvent e) { 65 | ImportDialog.this.onCancel(); 66 | } 67 | }); 68 | 69 | this.contentPanel.registerKeyboardAction( 70 | e -> this.onCancel(), 71 | KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), 72 | JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 73 | } 74 | 75 | public Layout getImportedLayout() { 76 | return this.importedLayout; 77 | } 78 | 79 | public int showDialogInCenterOf(JDialog parent) { 80 | this.setTitle(MessagesHelper.message("ImportDialog.Title")); 81 | this.setSize(this.getPreferredSize()); 82 | this.setLocationRelativeTo(parent); 83 | this.setVisible(true); 84 | return this.result; 85 | } 86 | 87 | private void importFromFile() { 88 | File selectedFile = this.selectFile(); 89 | if (selectedFile != null) { 90 | this.importFile(selectedFile); 91 | } 92 | } 93 | 94 | private File selectFile() { 95 | JFileChooser fileChooser = new JFileChooser(); 96 | fileChooser.setDialogTitle(MessagesHelper.message("ImportDialog.SelectFileTitle")); 97 | fileChooser.setAcceptAllFileFilterUsed(false); 98 | FileNameExtensionFilter filter = new FileNameExtensionFilter( 99 | ImportExportConstants.FILE_TYPE_NAME, 100 | ImportExportConstants.FILE_ENDING); 101 | fileChooser.setFileFilter(filter); 102 | 103 | int result = fileChooser.showOpenDialog(null); 104 | return result == JFileChooser.APPROVE_OPTION && fileChooser.getSelectedFile().exists() ? 105 | fileChooser.getSelectedFile() : null; 106 | } 107 | 108 | private void importFile(File file) { 109 | try { 110 | String encodedContent = Files.readString(Paths.get(file.getPath())); 111 | this.importLayout(encodedContent); 112 | } catch (IOException e) { 113 | ComponentNotificationHelper.error( 114 | this.importFromFileButton, 115 | MessagesHelper.message("ImportDialog.IOException", file.getName(), e.getMessage())); 116 | this.deselectLayout(); 117 | } catch (Exception e) { 118 | ComponentNotificationHelper.error( 119 | this.importFromFileButton, 120 | MessagesHelper.message("ImportDialog.UnknownFormat")); 121 | this.deselectLayout(); 122 | } 123 | } 124 | 125 | private void importFromClipboard() { 126 | try { 127 | String lzEncodedContent = (String) Toolkit 128 | .getDefaultToolkit() 129 | .getSystemClipboard() 130 | .getData(DataFlavor.stringFlavor); 131 | 132 | this.importLayout(lzEncodedContent); 133 | } catch (Exception e) { 134 | ComponentNotificationHelper.error( 135 | this.importFromClipboardButton, 136 | MessagesHelper.message("ImportDialog.UnknownFormat")); 137 | this.deselectLayout(); 138 | } 139 | } 140 | 141 | private void importLayout(String encodedContent) { 142 | Layout selectedLayout = this.layoutSerializer.deserialize(encodedContent); 143 | this.selectLayout(selectedLayout); 144 | } 145 | 146 | private void selectLayout(Layout layout) { 147 | this.importedLayout = layout; 148 | 149 | this.layoutNameTextBox.setText(layout.getName()); 150 | this.layoutNameTextBox.requestFocus(); 151 | this.layoutConfiguredWindowCountLabel.setText(Integer.toString(layout.getToolWindows().length)); 152 | 153 | ComponentNotificationHelper.info( 154 | this.layoutNameTextBox, 155 | MessagesHelper.message("ImportDialog.SuccessfullyLoadedLayout", layout.getName())); 156 | 157 | this.importButton.setEnabled(true); 158 | this.layoutNameTextBox.setEnabled(true); 159 | } 160 | 161 | private void deselectLayout() { 162 | this.importedLayout = null; 163 | this.importButton.setEnabled(false); 164 | this.layoutNameTextBox.setText(""); 165 | this.layoutNameTextBox.setEnabled(false); 166 | this.layoutConfiguredWindowCountLabel.setText("-"); 167 | } 168 | 169 | private void onOK() { 170 | if (!this.layoutNameValidator.isValid(this.layoutNameTextBox.getText())) { 171 | ComponentNotificationHelper.error(this.importButton, MessagesHelper.message("LayoutNameValidation.InvalidName")); 172 | return; 173 | } 174 | this.importedLayout.setName(this.layoutNameTextBox.getText()); 175 | 176 | this.result = OK_RESULT; 177 | this.dispose(); 178 | } 179 | 180 | private void onCancel() { 181 | this.result = ABORT_RESULT; 182 | this.dispose(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.layoutmanager 3 | Window Layout Manager 4 | Michael Estermann 5 | 6 | 10 | Plugin is accessible via Window Menu: 11 |
    12 |
  • Store Layout: Stores the current window layout
  • 13 |
  • Restore Layout: Restores the selected layout immediately
  • 14 |
15 | Feel free to contribute at GitHub. 16 | ]]>
17 | 18 | 20 |
  • Sept 12, 2019 (ver 1.0) - Initial version
  • 21 |
  • Nov 29, 2019 (ver 1.1.1) - Support of the latest IDE versions
  • 22 |
  • Dec 11, 2019 (ver 1.2) - Support variable amount of windows
  • 23 |
  • Dec 11, 2019 (ver 1.2.1) - Support of other languages and notifications
  • 24 |
  • Dec 11, 2019 (ver 1.2.2) - Support of the latest IDE versions 2019.3
  • 25 |
  • Dec 11, 2019 (ver 1.2.3) - Various bug fixes
  • 26 |
  • Dec 19, 2019 (ver 1.3.0) - Store placement of editor tabs and tool window labels.
  • 27 |
  • Dec 26, 2019 (ver 1.3.1) - Fix support of Java 8.
  • 28 |
  • Jan 07, 2020 (ver 1.3.2) - Minor bugfixes.
  • 29 |
  • Feb 25, 2020 (ver 1.3.3) - Issues when storing and restoring invisible windows fixed.
  • 30 |
  • 31 | May 8, 2020 (ver 1.4.0) 32 | - Introduced settings page. 33 | - Introduced smart docking when saving a layout (Configurable). 34 | - Store docked tool window sizes and restore them accordingly. 35 |
  • 36 |
  • 37 | September 17, 2024 (ver 1.5.0) 38 | - Hide hints after a short time 39 | - Support key-shortcuts for layouts 40 | - Improved icons 41 | - Minor Bugfixes 42 | - Support of new UI 43 |
  • 44 | 45 | ]]>
    46 | 47 | 48 | 49 | 50 | 51 | com.intellij.modules.lang 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 |
    72 | -------------------------------------------------------------------------------- /src/main/resources/com/layoutmanager/ui/icons/DeleteLayout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/com/layoutmanager/ui/icons/NewLayout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/com/layoutmanager/ui/icons/OverwriteLayout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/com/layoutmanager/ui/icons/RestoreLayout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/com/layoutmanager/ui/messages.properties: -------------------------------------------------------------------------------- 1 | RestoreLayout.Menu=Apply Window Layout 2 | RestoreLayout.Notification.Title=Window layout restored 3 | RestoreLayout.Notification.Content=The window layout ''{0}'' has been successfully restored. 4 | 5 | StoreLayout.Menu=Store Window Layout 6 | StoreLayout.Dialog.Title=Layout name 7 | StoreLayout.Dialog.Content=Enter the Name for the Layout 8 | StoreLayout.Validation.ToolWindowOutOfScreen.Title=Layout contains invalid tool windows 9 | StoreLayout.Validation.ToolWindowOutOfScreen.Content=The tool windows ''{0}'' are (partially) outside the screen bounds. When reopening the project, the IDE will restore them in the center of the main window. Please adjust this windows or apply the layout every time the project gets opened in the IDE. 10 | StoreLayout.New.Menu=New Layout 11 | StoreLayout.New.Notification.Title=Window layout created 12 | StoreLayout.New.Notification.Content=The window layout ''{0}'' has been successfully created. 13 | StoreLayout.Overwrite.Notification.Title=Window layout overwritten 14 | StoreLayout.Overwrite.Notification.Content=The window layout ''{0}'' has been successfully overwritten with the window layout ''{1}''. 15 | 16 | DeleteLayout.Menu=Delete Layout 17 | DeleteLayout.Notification.Title=Window layout deleted 18 | DeleteLayout.Notification.Content=The window layout ''{0}'' has been successfully deleted. 19 | 20 | SettingsPage.Title=Window Layout Manager 21 | SettingsPage.NameColumn=Name 22 | SettingsPage.ConfiguredWindowsColumn=Configured Windows 23 | SettingsPage.ShortcutColumn=Shortcut 24 | SettingsPage.WarningShortcut=The shortcut will only update after the settings have been saved. 25 | 26 | ExportDialog.Title=Export layout 27 | ExportDialog.CopiedToClipboard=Copied to clipboard! 28 | ExportDialog.SaveFileTitle=Specify a file to save 29 | ExportDialog.SavedTo=Saved to file ''{0}'' ! 30 | ExportDialog.FailedToWriteFile=Failed to write export file: {0} 31 | 32 | ImportDialog.Title=Import layout 33 | ImportDialog.SelectFileTitle=Select layout file 34 | ImportDialog.IOException=Failed to read file ''{0}''. Reason: {1} 35 | ImportDialog.UnknownFormat=Import failed due to an unknown format 36 | ImportDialog.SuccessfullyLoadedLayout=Successfully loaded the layout ''{0}'' 37 | 38 | LayoutNameValidation.InvalidName=The layout name must not be empty or contain illegal characters. --------------------------------------------------------------------------------