diff --git a/build.gradle b/build.gradle index 16c991c..496ed6a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,35 @@ plugins { id 'java' id 'org.jetbrains.intellij' version '0.4.15' + id 'checkstyle' } + +dependencies { + compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' + compile group: 'com.github.tommyettinger', name: 'blazingchain', version: '1.4.4.2' +} group 'com.layoutmanager' -version '1.3.3' +version '1.4.0' sourceCompatibility = 1.8 buildSearchableOptions.enabled = false +repositories { + mavenLocal() + mavenCentral() +} + +// https://www.jetbrains.org/intellij/sdk/docs/reference_guide/intellij_artifacts.html +// https://www.jetbrains.com/intellij-repository/releases/ +// https://www.jetbrains.com/intellij-repository/snapshots/ intellij { type = 'RD' - version = "2020.1-SNAPSHOT" + version = "2020.2-SNAPSHOT" + // version = "2019.3.4" + // version = "2020.1.0" + // version = "2020.1-SNAPSHOT" updateSinceUntilBuild = false } @@ -23,4 +40,12 @@ publishPlugin { wrapper { gradleVersion = '6.0.1' distributionUrl = "https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" +} + +checkstyle { + project.ext.checkstyleVersion = '8.27' +} + +checkstyleMain { + source ='src/main/java' } \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..0dec546 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/layoutmanager/cleanup/EmptyLayoutRemoverService.java b/src/main/java/com/layoutmanager/cleanup/EmptyLayoutRemoverService.java index 7f1d62f..e607fc7 100644 --- a/src/main/java/com/layoutmanager/cleanup/EmptyLayoutRemoverService.java +++ b/src/main/java/com/layoutmanager/cleanup/EmptyLayoutRemoverService.java @@ -9,7 +9,7 @@ public void execute() { LayoutConfig layoutConfig = ServiceManager.getService(LayoutConfig.class); for (Layout layout : layoutConfig.getLayouts()) { - if (isEmpty(layout)) { + if (this.isEmpty(layout)) { layoutConfig.removeLayout(layout); } } diff --git a/src/main/java/com/layoutmanager/actions/DeleteLayoutAction.java b/src/main/java/com/layoutmanager/layout/delete/DeleteLayoutAction.java similarity index 80% rename from src/main/java/com/layoutmanager/actions/DeleteLayoutAction.java rename to src/main/java/com/layoutmanager/layout/delete/DeleteLayoutAction.java index 9249f7e..05791e9 100644 --- a/src/main/java/com/layoutmanager/actions/DeleteLayoutAction.java +++ b/src/main/java/com/layoutmanager/layout/delete/DeleteLayoutAction.java @@ -1,15 +1,17 @@ -package com.layoutmanager.actions; +package com.layoutmanager.layout.delete; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.components.ServiceManager; + import com.layoutmanager.localization.MessagesHelper; -import com.layoutmanager.menu.WindowMenuService; import com.layoutmanager.persistence.Layout; import com.layoutmanager.persistence.LayoutConfig; -import com.layoutmanager.ui.NotificationHelper; +import com.layoutmanager.ui.helpers.BaloonNotificationHelper; +import com.layoutmanager.ui.menu.WindowMenuService; + import org.jetbrains.annotations.NotNull; public class DeleteLayoutAction extends AnAction { @@ -25,9 +27,9 @@ public DeleteLayoutAction(Layout layout) { @Override public void actionPerformed(@NotNull AnActionEvent event) { - deleteLayout(); - updateWindowMenuItems(); - showNotification(); + this.deleteLayout(); + this.updateWindowMenuItems(); + this.showNotification(); } private void deleteLayout() { @@ -36,7 +38,7 @@ private void deleteLayout() { } private void showNotification() { - NotificationHelper.info( + BaloonNotificationHelper.info( MessagesHelper.message("DeleteLayout.Notification.Title"), MessagesHelper.message("DeleteLayout.Notification.Content", this.layout.getName())); } diff --git a/src/main/java/com/layoutmanager/actions/RestoreLayoutAction.java b/src/main/java/com/layoutmanager/layout/restore/RestoreLayoutAction.java similarity index 85% rename from src/main/java/com/layoutmanager/actions/RestoreLayoutAction.java rename to src/main/java/com/layoutmanager/layout/restore/RestoreLayoutAction.java index 49858a9..f4edbdf 100644 --- a/src/main/java/com/layoutmanager/actions/RestoreLayoutAction.java +++ b/src/main/java/com/layoutmanager/layout/restore/RestoreLayoutAction.java @@ -1,4 +1,4 @@ -package com.layoutmanager.actions; +package com.layoutmanager.layout.restore; import com.intellij.icons.AllIcons; import com.intellij.ide.ui.UISettings; @@ -13,8 +13,8 @@ import com.layoutmanager.localization.MessagesHelper; import com.layoutmanager.persistence.Layout; import com.layoutmanager.persistence.ToolWindowInfo; -import com.layoutmanager.ui.NotificationHelper; -import com.layoutmanager.ui.ToolWindowHelper; +import com.layoutmanager.ui.helpers.BaloonNotificationHelper; +import com.layoutmanager.ui.helpers.ToolWindowHelper; import org.jetbrains.annotations.NotNull; import java.util.Map; @@ -43,8 +43,8 @@ public void update(AnActionEvent e) { @Override public void actionPerformed(@NotNull AnActionEvent event) { - applyLayout(event, this.layout); - showNotification(this.layout); + this.applyLayout(event, this.layout); + this.showNotification(this.layout); } private void applyLayout(AnActionEvent event, Layout layout) { @@ -58,9 +58,9 @@ private void applyLayout(AnActionEvent event, Layout layout) { private Map getToolWindows(ToolWindowManager toolWindowManager, ToolWindowInfo[] toolWindows) { return Stream.of(toolWindows) - .map(x -> new Pair(x, (ToolWindowImpl)toolWindowManager.getToolWindow(x.getId()))) - .filter(x -> x.second != null) - .collect(Collectors.toMap(x -> x.first, x -> x.second)); + .map(x -> new Pair(x, (ToolWindowImpl)toolWindowManager.getToolWindow(x.getId()))) + .filter(x -> x.second != null) + .collect(Collectors.toMap(x -> x.first, x -> x.second)); } private void applyEditorTabPlacement(Layout layout) { @@ -102,8 +102,9 @@ private ToolWindowManager getToolWindowManager(AnActionEvent event) { Project project = event.getProject(); return ToolWindowManager.getInstance(project); } + private void showNotification(Layout updatedLayout) { - NotificationHelper.info( + BaloonNotificationHelper.info( MessagesHelper.message("RestoreLayout.Notification.Title"), MessagesHelper.message("RestoreLayout.Notification.Content", updatedLayout.getName())); } diff --git a/src/main/java/com/layoutmanager/actions/LayoutCreator.java b/src/main/java/com/layoutmanager/layout/store/LayoutCreator.java similarity index 60% rename from src/main/java/com/layoutmanager/actions/LayoutCreator.java rename to src/main/java/com/layoutmanager/layout/store/LayoutCreator.java index 832e5ec..d9e64c7 100644 --- a/src/main/java/com/layoutmanager/actions/LayoutCreator.java +++ b/src/main/java/com/layoutmanager/layout/store/LayoutCreator.java @@ -1,16 +1,18 @@ -package com.layoutmanager.actions; +package com.layoutmanager.layout.store; -import com.intellij.icons.AllIcons; import com.intellij.ide.ui.UISettings; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.openapi.wm.impl.ToolWindowImpl; +import com.layoutmanager.layout.store.smartdock.SmartDocker; +import com.layoutmanager.layout.store.smartdock.SmartDockerFactory; +import com.layoutmanager.layout.store.validation.LayoutValidationHelper; import com.layoutmanager.localization.MessagesHelper; import com.layoutmanager.persistence.Layout; +import com.layoutmanager.persistence.LayoutSettings; import com.layoutmanager.persistence.ToolWindowInfo; -import com.layoutmanager.ui.NotificationHelper; -import com.layoutmanager.ui.ToolWindowHelper; +import com.layoutmanager.ui.dialogs.LayoutNameDialog; +import com.layoutmanager.ui.helpers.BaloonNotificationHelper; +import com.layoutmanager.ui.helpers.ToolWindowHelper; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -18,30 +20,28 @@ import java.util.stream.Stream; public class LayoutCreator { + private final LayoutSettings layoutSettings; + private final SmartDockerFactory smartDockerFactory; + private final LayoutNameDialog layoutNameDialog; - public static Layout create(Project project, String defaultName) { + public LayoutCreator( + LayoutSettings layoutSettings, + SmartDockerFactory smartDockerFactory, + LayoutNameDialog layoutNameDialog) { + this.layoutSettings = layoutSettings; + this.smartDockerFactory = smartDockerFactory; + this.layoutNameDialog = layoutNameDialog; + } + + public Layout create(ToolWindowManager toolWindowManager, String defaultName) { - String name = getLayoutName(defaultName); + String name = this.layoutNameDialog.show(defaultName); return name != null ? - createLayout(ToolWindowManager.getInstance(project), name) : + this.createLayout(toolWindowManager, name) : null; } - private static String getLayoutName(String defaultName) { - String name; - do { - name = Messages.showInputDialog( - MessagesHelper.message("StoreLayout.Dialog.Title"), - MessagesHelper.message("StoreLayout.Dialog.Content"), - AllIcons.Actions.Edit, - defaultName, - null); - } while (name != null && name.isEmpty()); - - return name; - } - - private static Layout createLayout(ToolWindowManager toolWindowManager, String name) { + private Layout createLayout(ToolWindowManager toolWindowManager, String name) { List toolWindows = getToolWindows(toolWindowManager); Layout layout = new Layout( name, @@ -49,6 +49,11 @@ private static Layout createLayout(ToolWindowManager toolWindowManager, String n .stream() .toArray(ToolWindowInfo[]::new), getEditorPlacement()); + + if (this.layoutSettings.getUseSmartDock()) { + this.dock(toolWindowManager, layout); + } + validateLayout(layout); return layout; @@ -74,6 +79,11 @@ private static List getToolWindows(ToolWindowManager toolWindowM return toolWindows; } + private void dock(ToolWindowManager toolWindowManager, Layout layout) { + SmartDocker smartDocker = this.smartDockerFactory.create(toolWindowManager); + smartDocker.dock(layout); + } + private static void validateLayout(Layout layout) { ToolWindowInfo[] invalidToolWindows = LayoutValidationHelper.retrieveToolWindowsOutsideOfScreen(layout); if (invalidToolWindows.length != 0) { @@ -83,7 +93,7 @@ private static void validateLayout(Layout layout) { .map(ToolWindowInfo::getId) .toArray(String[]::new)); - NotificationHelper.warning( + BaloonNotificationHelper.warning( MessagesHelper.message("StoreLayout.Validation.ToolWindowOutOfScreen.Title"), MessagesHelper.message("StoreLayout.Validation.ToolWindowOutOfScreen.Content", invalidToolWindowNames)); } diff --git a/src/main/java/com/layoutmanager/actions/NewLayoutAction.java b/src/main/java/com/layoutmanager/layout/store/create/NewLayoutAction.java similarity index 57% rename from src/main/java/com/layoutmanager/actions/NewLayoutAction.java rename to src/main/java/com/layoutmanager/layout/store/create/NewLayoutAction.java index 43c893b..6159a9d 100644 --- a/src/main/java/com/layoutmanager/actions/NewLayoutAction.java +++ b/src/main/java/com/layoutmanager/layout/store/create/NewLayoutAction.java @@ -1,20 +1,27 @@ -package com.layoutmanager.actions; +package com.layoutmanager.layout.store.create; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.wm.ToolWindowManager; + +import com.layoutmanager.layout.store.LayoutCreator; import com.layoutmanager.localization.MessagesHelper; -import com.layoutmanager.menu.WindowMenuService; import com.layoutmanager.persistence.Layout; import com.layoutmanager.persistence.LayoutConfig; -import com.layoutmanager.ui.NotificationHelper; +import com.layoutmanager.ui.helpers.BaloonNotificationHelper; +import com.layoutmanager.ui.menu.WindowMenuService; + import org.jetbrains.annotations.NotNull; public class NewLayoutAction extends AnAction { - public NewLayoutAction() { + private final LayoutCreator layoutCreator; + + public NewLayoutAction(LayoutCreator layoutCreator) { + this.layoutCreator = layoutCreator; Presentation presentation = this.getTemplatePresentation(); presentation.setText(MessagesHelper.message("StoreLayout.New.Menu")); presentation.setIcon(AllIcons.Welcome.CreateNewProject); @@ -22,12 +29,13 @@ public NewLayoutAction() { @Override public void actionPerformed(@NotNull AnActionEvent event) { - Layout updatedLayout = LayoutCreator.create(event.getProject(), ""); + ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(event.getProject()); + Layout newLayout = this.layoutCreator.create(toolWindowManager, ""); - if (updatedLayout != null) { - this.storeLayout(updatedLayout); - updateWindowMenuItems(); - showNotification(updatedLayout); + if (newLayout != null) { + this.storeLayout(newLayout); + this.updateWindowMenuItems(); + this.showNotification(newLayout); } } @@ -40,9 +48,9 @@ private void updateWindowMenuItems() { windowMenuService.recreate(); } - private void showNotification(Layout updatedLayout) { - NotificationHelper.info( + private void showNotification(Layout newLayout) { + BaloonNotificationHelper.info( MessagesHelper.message("StoreLayout.New.Notification.Title"), - MessagesHelper.message("StoreLayout.New.Notification.Content", updatedLayout.getName())); + MessagesHelper.message("StoreLayout.New.Notification.Content", newLayout.getName())); } } diff --git a/src/main/java/com/layoutmanager/actions/OverwriteLayoutAction.java b/src/main/java/com/layoutmanager/layout/store/overwrite/OverwriteLayoutAction.java similarity index 66% rename from src/main/java/com/layoutmanager/actions/OverwriteLayoutAction.java rename to src/main/java/com/layoutmanager/layout/store/overwrite/OverwriteLayoutAction.java index 6ae4712..ea95769 100644 --- a/src/main/java/com/layoutmanager/actions/OverwriteLayoutAction.java +++ b/src/main/java/com/layoutmanager/layout/store/overwrite/OverwriteLayoutAction.java @@ -1,23 +1,30 @@ -package com.layoutmanager.actions; +package com.layoutmanager.layout.store.overwrite; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.wm.ToolWindowManager; + +import com.layoutmanager.layout.store.LayoutCreator; import com.layoutmanager.localization.MessagesHelper; -import com.layoutmanager.menu.WindowMenuService; import com.layoutmanager.persistence.Layout; import com.layoutmanager.persistence.LayoutConfig; -import com.layoutmanager.ui.NotificationHelper; +import com.layoutmanager.ui.helpers.BaloonNotificationHelper; +import com.layoutmanager.ui.menu.WindowMenuService; + import org.jetbrains.annotations.NotNull; public class OverwriteLayoutAction extends AnAction { + private final LayoutCreator layoutCreator; public final int number; - public OverwriteLayoutAction(int number) { - + public OverwriteLayoutAction( + LayoutCreator layoutCreator, + int number) { + this.layoutCreator = layoutCreator; this.number = number; Layout layout = LayoutConfig.getInstance().getLayout(number); @@ -28,20 +35,21 @@ public OverwriteLayoutAction(int number) { @Override public void update(AnActionEvent e) { - Layout layout = LayoutConfig.getInstance().getLayout(number); + Layout layout = LayoutConfig.getInstance().getLayout(this.number); e.getPresentation().setText(layout.getName()); super.update(e); } @Override public void actionPerformed(@NotNull AnActionEvent event) { - Layout previousLayout = LayoutConfig.getInstance().getLayout(number); - Layout updatedLayout = LayoutCreator.create(event.getProject(), previousLayout.getName()); + Layout previousLayout = LayoutConfig.getInstance().getLayout(this.number); + ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(event.getProject()); + Layout updatedLayout = this.layoutCreator.create(toolWindowManager, previousLayout.getName()); if (updatedLayout != null) { this.storeLayout(updatedLayout); - updateWindowMenuItems(); - showNotification(updatedLayout, previousLayout); + this.updateWindowMenuItems(); + this.showNotification(updatedLayout, previousLayout); } } @@ -55,7 +63,7 @@ private void updateWindowMenuItems() { } private void showNotification(Layout updatedLayout, Layout previousLayout) { - NotificationHelper.info( + BaloonNotificationHelper.info( MessagesHelper.message("StoreLayout.Overwrite.Notification.Title"), MessagesHelper.message("StoreLayout.Overwrite.Notification.Content", previousLayout.getName(), updatedLayout.getName())); } diff --git a/src/main/java/com/layoutmanager/layout/store/smartdock/SmartDocker.java b/src/main/java/com/layoutmanager/layout/store/smartdock/SmartDocker.java new file mode 100644 index 0000000..7f34382 --- /dev/null +++ b/src/main/java/com/layoutmanager/layout/store/smartdock/SmartDocker.java @@ -0,0 +1,59 @@ +package com.layoutmanager.layout.store.smartdock; + +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.openapi.wm.ToolWindowType; +import com.intellij.openapi.wm.impl.ToolWindowImpl; +import com.layoutmanager.layout.store.smartdock.dockers.ScreenBorderDocker; +import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowDocker; +import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowToScreenShrinker; +import com.layoutmanager.persistence.Layout; +import com.layoutmanager.persistence.ToolWindowInfo; +import com.layoutmanager.ui.helpers.ScreenSizeHelper; + +import java.util.Arrays; + +public class SmartDocker { + private static final int THRESHOLD = 20; + + private final ToolWindowManager toolWindowManager; + private final ToolWindowToScreenShrinker shrinker; + private final ToolWindowDocker toolWindowDocker; + private final ScreenBorderDocker screenBorderDocker; + + public SmartDocker( + ToolWindowManager toolWindowManager, + ToolWindowToScreenShrinker shrinker, + ToolWindowDocker toolWindowDocker, + ScreenBorderDocker screenBorderDocker) { + this.toolWindowManager = toolWindowManager; + this.shrinker = shrinker; + this.toolWindowDocker = toolWindowDocker; + this.screenBorderDocker = screenBorderDocker; + } + + public void dock(Layout layout) { + ToolWindowDocking[] floatedOrWindowsToolWindows = this.getFloatedOrWindowsToolWindows(layout); + + this.shrinker.shrink(floatedOrWindowsToolWindows); + this.toolWindowDocker.dock(floatedOrWindowsToolWindows); + this.screenBorderDocker.dock(floatedOrWindowsToolWindows, THRESHOLD); + } + + + private ToolWindowDocking[] getFloatedOrWindowsToolWindows(Layout layout) { + return Arrays.stream(layout.getToolWindows()) + .filter(this::isFloatingOrWindowedToolWindow) + .map(x -> new ToolWindowDocking( + x, + (ToolWindowImpl)this.toolWindowManager.getToolWindow(x.getId()), + ScreenSizeHelper.getContainingScreenBounds(x), + THRESHOLD)) + .toArray(ToolWindowDocking[]::new); + } + + private boolean isFloatingOrWindowedToolWindow(ToolWindowInfo toolWindow) { + return this.toolWindowManager.getToolWindow(toolWindow.getId()) != null && + toolWindow.getType() == ToolWindowType.FLOATING || + toolWindow.getType() == ToolWindowType.WINDOWED; + } +} diff --git a/src/main/java/com/layoutmanager/layout/store/smartdock/SmartDockerFactory.java b/src/main/java/com/layoutmanager/layout/store/smartdock/SmartDockerFactory.java new file mode 100644 index 0000000..d1a1525 --- /dev/null +++ b/src/main/java/com/layoutmanager/layout/store/smartdock/SmartDockerFactory.java @@ -0,0 +1,16 @@ +package com.layoutmanager.layout.store.smartdock; + +import com.intellij.openapi.wm.ToolWindowManager; +import com.layoutmanager.layout.store.smartdock.dockers.ScreenBorderDocker; +import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowDocker; +import com.layoutmanager.layout.store.smartdock.dockers.ToolWindowToScreenShrinker; + +public class SmartDockerFactory { + public SmartDocker create(ToolWindowManager toolWindowManager) { + return new SmartDocker( + toolWindowManager, + new ToolWindowToScreenShrinker(), + new ToolWindowDocker(), + new ScreenBorderDocker()); + } +} diff --git a/src/main/java/com/layoutmanager/layout/store/smartdock/ToolWindowDocking.java b/src/main/java/com/layoutmanager/layout/store/smartdock/ToolWindowDocking.java new file mode 100644 index 0000000..428f0fc --- /dev/null +++ b/src/main/java/com/layoutmanager/layout/store/smartdock/ToolWindowDocking.java @@ -0,0 +1,73 @@ +package com.layoutmanager.layout.store.smartdock; + +import com.intellij.openapi.wm.impl.ToolWindowImpl; +import com.layoutmanager.persistence.ToolWindowInfo; + +import java.awt.Rectangle; + +public class ToolWindowDocking { + + private final ToolWindowInfo toolWindowInfo; + private final ToolWindowImpl toolWindow; + private final Rectangle containingScreen; + private final int threshold; + + public ToolWindowDocking( + ToolWindowInfo toolWindowInfo, + ToolWindowImpl toolWindow, + Rectangle containingScreen, + int threshold) { + this.toolWindowInfo = toolWindowInfo; + this.toolWindow = toolWindow; + this.containingScreen = containingScreen; + this.threshold = threshold; + } + + public ToolWindowInfo getToolWindowInfo() { + return this.toolWindowInfo; + } + + public ToolWindowImpl getToolWindow() { + return this.toolWindow; + } + + public Rectangle getContainingScreen() { + return this.containingScreen; + } + + public Rectangle getBounds() { + return this.toolWindowInfo.getBounds(); + } + + public Rectangle getLeftDockingBounds() { + return new Rectangle( + (int) Math.max(0, this.toolWindowInfo.getBounds().getX() - threshold), + (int) this.toolWindowInfo.getBounds().getY(), + this.threshold * 2, + (int) this.toolWindowInfo.getBounds().getHeight()); + } + + public Rectangle getTopDockingBounds() { + return new Rectangle( + (int) this.toolWindowInfo.getBounds().getX(), + (int) Math.max(0, this.toolWindowInfo.getBounds().getY() - threshold), + (int) this.toolWindowInfo.getBounds().getWidth(), + this.threshold * 2); + } + + public Rectangle getRightDockingBounds() { + return new Rectangle( + (int) (this.toolWindowInfo.getBounds().getMaxX() - threshold), + (int) this.toolWindowInfo.getBounds().getY(), + (int) Math.min(this.threshold * 2, this.threshold + this.containingScreen.getMaxX() - this.toolWindowInfo.getBounds().getMaxX()), + (int) this.toolWindowInfo.getBounds().getHeight()); + } + + public Rectangle getBottomDockingBounds() { + return new Rectangle( + (int) this.toolWindowInfo.getBounds().getX(), + (int) this.toolWindowInfo.getBounds().getMaxY() - threshold, + (int) this.toolWindowInfo.getBounds().getWidth(), + (int) Math.min(this.threshold * 2, this.threshold + this.containingScreen.getMaxY() - this.toolWindowInfo.getBounds().getMaxY())); + } +} diff --git a/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ScreenBorderDocker.java b/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ScreenBorderDocker.java new file mode 100644 index 0000000..8cad370 --- /dev/null +++ b/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ScreenBorderDocker.java @@ -0,0 +1,78 @@ +package com.layoutmanager.layout.store.smartdock.dockers; + +import com.layoutmanager.layout.store.smartdock.ToolWindowDocking; + +import java.awt.Rectangle; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class ScreenBorderDocker { + public void dock(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { + this.pinLeft(floatedOrWindowsToolWindows, threshold); + this.pinTop(floatedOrWindowsToolWindows, threshold); + this.pinRight(floatedOrWindowsToolWindows, threshold); + this.pinBottom(floatedOrWindowsToolWindows, threshold); + } + + private void pinLeft(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { + this.pinToScreenBorder( + floatedOrWindowsToolWindows, + threshold, + toolWindowDocking -> (int)(toolWindowDocking.getBounds().getX() - toolWindowDocking.getContainingScreen().getX()), + (toolWindowDocking, difference) -> new Rectangle( + (int)toolWindowDocking.getContainingScreen().getX(), + (int)toolWindowDocking.getBounds().getY(), + (int)(toolWindowDocking.getBounds().getWidth() + difference), + (int)toolWindowDocking.getBounds().getHeight())); + } + + private void pinTop(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { + this.pinToScreenBorder( + floatedOrWindowsToolWindows, + threshold, + x -> (int)(x.getBounds().getY() - x.getContainingScreen().getY()), + (toolWindowDocking, difference) -> new Rectangle( + (int)toolWindowDocking.getBounds().getX(), + (int)toolWindowDocking.getContainingScreen().getY(), + (int)toolWindowDocking.getBounds().getWidth(), + (int)(toolWindowDocking.getBounds().getHeight() + 5))); + } + + private void pinRight(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { + this.pinToScreenBorder( + floatedOrWindowsToolWindows, + threshold, + toolWindowDocking -> (int)(toolWindowDocking.getContainingScreen().getMaxX() - toolWindowDocking.getBounds().getMaxX()), + (toolWindowDocking, difference) -> new Rectangle( + (int)toolWindowDocking.getBounds().getX(), + (int)toolWindowDocking.getBounds().getY(), + (int)(toolWindowDocking.getBounds().getWidth() + difference), + (int)toolWindowDocking.getBounds().getHeight())); + } + + private void pinBottom(ToolWindowDocking[] floatedOrWindowsToolWindows, int threshold) { + this.pinToScreenBorder( + floatedOrWindowsToolWindows, + threshold, + toolWindowDocking -> (int)(toolWindowDocking.getBounds().getY() - toolWindowDocking.getContainingScreen().getY()), + (toolWindowDocking, difference) -> new Rectangle( + (int)toolWindowDocking.getBounds().getX(), + (int)toolWindowDocking.getBounds().getY(), + (int)toolWindowDocking.getBounds().getWidth(), + (int)(toolWindowDocking.getBounds().getHeight() + difference))); + } + + private void pinToScreenBorder( + ToolWindowDocking[] toolWindowDockings, + int threshold, + Function getDifference, + BiFunction calculateBounds) { + + for (ToolWindowDocking toolWindowDocking : toolWindowDockings) { + Integer difference = getDifference.apply(toolWindowDocking); + if (difference != 0 && difference < threshold) { + toolWindowDocking.getToolWindowInfo().setBounds(calculateBounds.apply(toolWindowDocking, difference)); + } + } + } +} diff --git a/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowDocker.java b/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowDocker.java new file mode 100644 index 0000000..1f4c035 --- /dev/null +++ b/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowDocker.java @@ -0,0 +1,105 @@ +package com.layoutmanager.layout.store.smartdock.dockers; + +import com.layoutmanager.layout.store.smartdock.ToolWindowDocking; + +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; + +public class ToolWindowDocker { + public void dock(ToolWindowDocking[] floatedOrWindowsToolWindows) { + this.dockLeft(floatedOrWindowsToolWindows); + this.dockTop(floatedOrWindowsToolWindows); + this.dockRight(floatedOrWindowsToolWindows); + this.dockBottom(floatedOrWindowsToolWindows); + } + + private void dockLeft(ToolWindowDocking[] floatedOrWindowsToolWindows) { + this.dockToolWindow( + floatedOrWindowsToolWindows, + (x, y) -> (int) (x.getBounds().getX() - y.getBounds().getX()), + (x, y) -> x.getRightDockingBounds().intersects(y.getLeftDockingBounds()), + Comparator.comparingInt(x -> (int) x.getBounds().getY()), + (toolWindow, boundsToDock) -> { + int x = (int) toolWindow.getBounds().getX(); + int newX = (int) boundsToDock.getMaxX(); + return new Rectangle( + newX, + (int) toolWindow.getBounds().getY(), + (int) (toolWindow.getBounds().getWidth() + (x - newX)), + (int) toolWindow.getBounds().getHeight()); + }); + } + + private void dockTop(ToolWindowDocking[] floatedOrWindowsToolWindows) { + this.dockToolWindow( + floatedOrWindowsToolWindows, + (x, y) -> (int) (x.getBounds().getY() - y.getBounds().getY()), + (x, y) -> x.getBottomDockingBounds().intersects(y.getTopDockingBounds()), + Comparator.comparingInt(x -> (int) x.getBounds().getX()), + (toolWindow, boundsToDock) -> { + int y = (int) toolWindow.getBounds().getY(); + int newY = (int) boundsToDock.getMaxY(); + return new Rectangle( + (int) toolWindow.getBounds().getX(), + newY, + (int) toolWindow.getBounds().getWidth(), + (int) (toolWindow.getBounds().getHeight() + (y - newY))); + }); + } + + private void dockRight(ToolWindowDocking[] floatedOrWindowsToolWindows) { + this.dockToolWindow( + floatedOrWindowsToolWindows, + (x, y) -> (int) (x.getBounds().getMaxX() - y.getBounds().getMaxX()), + (x, y) -> x.getLeftDockingBounds().intersects(y.getRightDockingBounds()), + Comparator.comparingInt(x -> (int) x.getBounds().getY()), + (toolWindow, boundsToDock) -> new Rectangle( + (int) toolWindow.getBounds().getX(), + (int) toolWindow.getBounds().getY(), + (int) (boundsToDock.getX() - toolWindow.getBounds().getX()), + (int) toolWindow.getBounds().getHeight())); + } + + private void dockBottom(ToolWindowDocking[] floatedOrWindowsToolWindows) { + this.dockToolWindow( + floatedOrWindowsToolWindows, + (x, y) -> (int) (x.getBounds().getMaxY() - y.getBounds().getMaxY()), + (x, y) -> x.getTopDockingBounds().intersects(y.getBottomDockingBounds()), + Comparator.comparingInt(x -> (int) x.getBounds().getY()), + (toolWindow, boundsToDock) -> new Rectangle( + (int) toolWindow.getBounds().getX(), + (int) toolWindow.getBounds().getY(), + (int) toolWindow.getBounds().getWidth(), + (int) (boundsToDock.getY() - toolWindow.getBounds().getY()))); + } + + private void dockToolWindow( + ToolWindowDocking[] floatedOrWindowsToolWindows, + Comparator sortForProcessing, + BiPredicate isIntersecting, + Comparator sortForClosestToDock, + BiFunction calculateNewBounds) { + + ToolWindowDocking[] sortedByLeftPosition = Arrays.stream(floatedOrWindowsToolWindows) + .sorted(sortForProcessing) + .skip(1) + .toArray(ToolWindowDocking[]::new); + + for (ToolWindowDocking toolWindowDocking : sortedByLeftPosition) { + ToolWindowDocking dockingTarget = Arrays.stream(floatedOrWindowsToolWindows) + .filter(x -> + !x.getToolWindowInfo().getId().equals(toolWindowDocking.getToolWindowInfo().getId()) && + isIntersecting.test(x, toolWindowDocking)) + .min(sortForClosestToDock) + .orElse(null); + + if (dockingTarget != null) { + Rectangle newBounds = calculateNewBounds.apply(toolWindowDocking, dockingTarget.getBounds()); + toolWindowDocking.getToolWindowInfo().setBounds(newBounds); + } + } + } +} diff --git a/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowToScreenShrinker.java b/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowToScreenShrinker.java new file mode 100644 index 0000000..7a490f5 --- /dev/null +++ b/src/main/java/com/layoutmanager/layout/store/smartdock/dockers/ToolWindowToScreenShrinker.java @@ -0,0 +1,26 @@ +package com.layoutmanager.layout.store.smartdock.dockers; + +import com.layoutmanager.layout.store.smartdock.ToolWindowDocking; + +import java.awt.Rectangle; + +public class ToolWindowToScreenShrinker { + public void shrink(ToolWindowDocking[] floatedOrWindowsToolWindowDockings) { + for (ToolWindowDocking toolWindowDocking : floatedOrWindowsToolWindowDockings) { + Rectangle containingScreen = toolWindowDocking.getContainingScreen(); + Rectangle toolWindowBounds = toolWindowDocking.getBounds(); + + if (!containingScreen.contains(toolWindowBounds)) { + double x = Math.max(toolWindowBounds.getX(), containingScreen.getX()); + double y = Math.max(toolWindowBounds.getY(), containingScreen.getY()); + Rectangle newToolWindowBounds = new Rectangle( + (int)x, + (int)y, + (int)Math.min(toolWindowBounds.getWidth(), containingScreen.getMaxX() - x), + (int)Math.min(toolWindowBounds.getHeight(), containingScreen.getMaxY() - y)); + + toolWindowDocking.getToolWindowInfo().setBounds(newToolWindowBounds); + } + } + } +} diff --git a/src/main/java/com/layoutmanager/actions/LayoutValidationHelper.java b/src/main/java/com/layoutmanager/layout/store/validation/LayoutValidationHelper.java similarity index 88% rename from src/main/java/com/layoutmanager/actions/LayoutValidationHelper.java rename to src/main/java/com/layoutmanager/layout/store/validation/LayoutValidationHelper.java index 013be40..4e0e658 100644 --- a/src/main/java/com/layoutmanager/actions/LayoutValidationHelper.java +++ b/src/main/java/com/layoutmanager/layout/store/validation/LayoutValidationHelper.java @@ -1,15 +1,15 @@ -package com.layoutmanager.actions; +package com.layoutmanager.layout.store.validation; import com.intellij.openapi.wm.ToolWindowType; import com.intellij.openapi.wm.WindowManager; import com.layoutmanager.persistence.Layout; import com.layoutmanager.persistence.ToolWindowInfo; -import java.awt.*; +import java.awt.Rectangle; import java.util.stream.Stream; public class LayoutValidationHelper { - public static ToolWindowInfo[] retrieveToolWindowsOutsideOfScreen(Layout layout){ + public static ToolWindowInfo[] retrieveToolWindowsOutsideOfScreen(Layout layout) { return Stream .of(layout.getToolWindows()) .filter(x -> x.isVisible() && isWindowType(x) && !isValid(x)) diff --git a/src/main/java/com/layoutmanager/localization/MessagesHelper.java b/src/main/java/com/layoutmanager/localization/MessagesHelper.java index 46d402c..c7f9412 100644 --- a/src/main/java/com/layoutmanager/localization/MessagesHelper.java +++ b/src/main/java/com/layoutmanager/localization/MessagesHelper.java @@ -1,17 +1,17 @@ package com.layoutmanager.localization; - import com.intellij.CommonBundle; import com.intellij.reference.SoftReference; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.PropertyKey; import java.lang.ref.Reference; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + public class MessagesHelper { @NonNls public static final String BUNDLE_NAME = "com.layoutmanager.ui.messages"; diff --git a/src/main/java/com/layoutmanager/menu/WindowMenuService.java b/src/main/java/com/layoutmanager/menu/WindowMenuService.java deleted file mode 100644 index 05d8e6d..0000000 --- a/src/main/java/com/layoutmanager/menu/WindowMenuService.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.layoutmanager.menu; - -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.DefaultActionGroup; -import com.layoutmanager.actions.DeleteLayoutAction; -import com.layoutmanager.actions.NewLayoutAction; -import com.layoutmanager.actions.OverwriteLayoutAction; -import com.layoutmanager.actions.RestoreLayoutAction; -import com.layoutmanager.localization.MessagesHelper; -import com.layoutmanager.persistence.Layout; -import com.layoutmanager.persistence.LayoutConfig; - -public class WindowMenuService { - private DefaultActionGroup storeLayout; - private DefaultActionGroup restoreLayout; - private DefaultActionGroup deleteLayout; - - public void create() { - if (storeLayout != null) { - return; - } - - createMainActions( (DefaultActionGroup) ActionManager.getInstance().getAction("WindowMenu")); - createStoreRestoreActions(); - } - - public void recreate() { - this.restoreLayout.removeAll(); - this.storeLayout.removeAll(); - this.deleteLayout.removeAll(); - - createStoreRestoreActions(); - } - - private void createMainActions(DefaultActionGroup windowMenu) { - this.storeLayout = createMainAction(MessagesHelper.message("StoreLayout.Menu"), windowMenu); - this.restoreLayout = createMainAction(MessagesHelper.message("RestoreLayout.Menu"), windowMenu); - this.deleteLayout = createMainAction(MessagesHelper.message("DeleteLayout.Menu"), windowMenu); - } - - private DefaultActionGroup createMainAction(String name, DefaultActionGroup windowMenu){ - DefaultActionGroup windowMenuItem = new DefaultActionGroup(name, true); - windowMenu.add(windowMenuItem); - - return windowMenuItem; - } - - private void createStoreRestoreActions() { - LayoutConfig config = LayoutConfig.getInstance(); - addStoreLayoutActions(config); - addRestoreLayoutActions(config); - addDeleteLayoutActions(config); - } - - private void addStoreLayoutActions(LayoutConfig config) { - for (int index = 0; index < config.getLayoutCount(); index++) { - this.storeLayout.add(new OverwriteLayoutAction(index)); - } - - if (config.getLayoutCount() > 0) { - this.storeLayout.addSeparator(); - } - - this.storeLayout.add(new NewLayoutAction()); - } - - private void addRestoreLayoutActions(LayoutConfig config) { - for (Layout layout : config.getLayouts()) { - this.restoreLayout.add(new RestoreLayoutAction(layout)); - } - } - - private void addDeleteLayoutActions(LayoutConfig config) { - for (Layout layout : config.getLayouts()) { - this.deleteLayout.add(new DeleteLayoutAction(layout)); - } - } -} diff --git a/src/main/java/com/layoutmanager/persistence/Layout.java b/src/main/java/com/layoutmanager/persistence/Layout.java index c2d716d..e7675fd 100644 --- a/src/main/java/com/layoutmanager/persistence/Layout.java +++ b/src/main/java/com/layoutmanager/persistence/Layout.java @@ -1,15 +1,19 @@ package com.layoutmanager.persistence; +import java.util.Arrays; +import java.util.Objects; + public class Layout { private String name; private int editorTabPlacement; private ToolWindowInfo[] toolWindows; + @SuppressWarnings({"unused", "Used for serialization."}) public Layout() { - name = ""; - toolWindows = new ToolWindowInfo[0]; - editorTabPlacement = -1; + this.name = ""; + this.toolWindows = new ToolWindowInfo[0]; + this.editorTabPlacement = -1; } public Layout(String name, ToolWindowInfo[] toolWindows, int editorTabPlacement) { @@ -28,7 +32,7 @@ public void setName(String name) { } public int getEditorPlacement() { - return editorTabPlacement; + return this.editorTabPlacement; } @SuppressWarnings({"unused", "Used for serialization."}) @@ -37,11 +41,34 @@ public void setEditorPlacement(int editorTabPlacement) { } public ToolWindowInfo[] getToolWindows() { - return toolWindows; + return this.toolWindows; } @SuppressWarnings({"unused", "Used for serialization."}) public void setToolWindows(ToolWindowInfo[] toolWindows) { this.toolWindows = toolWindows; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + Layout that = (Layout) other; + return this.editorTabPlacement == that.editorTabPlacement && + this.name.equals(that.name) && + Arrays.equals(this.toolWindows, that.toolWindows); + } + + @Override + public int hashCode() { + int result = Objects.hash(this.name, this.editorTabPlacement); + result = 31 * result + Arrays.hashCode(toolWindows); + return result; + } } \ No newline at end of file diff --git a/src/main/java/com/layoutmanager/persistence/LayoutConfig.java b/src/main/java/com/layoutmanager/persistence/LayoutConfig.java index 04d3b4d..625b822 100644 --- a/src/main/java/com/layoutmanager/persistence/LayoutConfig.java +++ b/src/main/java/com/layoutmanager/persistence/LayoutConfig.java @@ -6,13 +6,13 @@ import com.intellij.openapi.components.Storage; import com.intellij.util.xmlb.XmlSerializerUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + @State( name = "Layout", storages = { @@ -21,6 +21,7 @@ ) public class LayoutConfig implements PersistentStateComponent { private List layouts = new ArrayList<>(); + private LayoutSettings settings = new LayoutSettings(); @Nullable @Override @@ -44,6 +45,19 @@ public Layout[] getLayouts() { .toArray(Layout[]::new); } + @SuppressWarnings({"unused", "Used for serialization."}) + public void setLayouts(Layout[] layouts) { + this.layouts = new ArrayList<>(Arrays.asList(layouts)); + } + + public LayoutSettings getSettings() { + return this.settings; + } + + public void setSettings(LayoutSettings settings) { + this.settings = settings; + } + public int getLayoutCount() { return this.layouts.size(); } @@ -64,10 +78,5 @@ public void removeLayout(Layout layout) { public static LayoutConfig getInstance() { return ServiceManager.getService(LayoutConfig.class); } - - @SuppressWarnings({"unused", "Used for serialization."}) - public void setLayouts(Layout[] layouts) { - this.layouts = new ArrayList<>(Arrays.asList(layouts)); - } } diff --git a/src/main/java/com/layoutmanager/persistence/LayoutSettings.java b/src/main/java/com/layoutmanager/persistence/LayoutSettings.java new file mode 100644 index 0000000..3e32356 --- /dev/null +++ b/src/main/java/com/layoutmanager/persistence/LayoutSettings.java @@ -0,0 +1,13 @@ +package com.layoutmanager.persistence; + +public class LayoutSettings { + private boolean useSmartDock = true; + + public boolean getUseSmartDock() { + return this.useSmartDock; + } + + public void setUseSmartDock(boolean useSmartDock) { + this.useSmartDock = useSmartDock; + } +} diff --git a/src/main/java/com/layoutmanager/persistence/ToolWindowInfo.java b/src/main/java/com/layoutmanager/persistence/ToolWindowInfo.java index cc2eec5..5190faa 100644 --- a/src/main/java/com/layoutmanager/persistence/ToolWindowInfo.java +++ b/src/main/java/com/layoutmanager/persistence/ToolWindowInfo.java @@ -2,7 +2,8 @@ import com.intellij.openapi.wm.ToolWindowType; -import java.awt.*; +import java.awt.Rectangle; +import java.util.Objects; public class ToolWindowInfo { private String id; @@ -87,7 +88,31 @@ public void setVisible(boolean visible) { } @SuppressWarnings({"unused", "Used for serialization."}) - public void setIsToolWindow(boolean isSplit) { + public void setIsToolWindow(boolean isToolWindow) { this.isToolWindow = isToolWindow; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + ToolWindowInfo that = (ToolWindowInfo) other; + return this.isVisible == that.isVisible && + this.isToolWindow == that.isToolWindow && + this.id.equals(that.id) && + this.type == that.type && + this.anchor.equals(that.anchor) && + this.bounds.equals(that.bounds); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.type, this.anchor, this.bounds, this.isVisible, this.isToolWindow); + } } diff --git a/src/main/java/com/layoutmanager/startup/PluginBootstrapper.java b/src/main/java/com/layoutmanager/startup/PluginBootstrapper.java index ce24aba..4cfe15f 100644 --- a/src/main/java/com/layoutmanager/startup/PluginBootstrapper.java +++ b/src/main/java/com/layoutmanager/startup/PluginBootstrapper.java @@ -4,7 +4,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupActivity; import com.layoutmanager.cleanup.EmptyLayoutRemoverService; -import com.layoutmanager.menu.WindowMenuService; +import com.layoutmanager.ui.menu.WindowMenuService; import org.jetbrains.annotations.NotNull; public class PluginBootstrapper implements StartupActivity { diff --git a/src/main/java/com/layoutmanager/ui/ToolWindowHelper.java b/src/main/java/com/layoutmanager/ui/ToolWindowHelper.java deleted file mode 100644 index 87a5295..0000000 --- a/src/main/java/com/layoutmanager/ui/ToolWindowHelper.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.layoutmanager.ui; - -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.wm.ToolWindowType; -import com.intellij.openapi.wm.impl.FloatingDecorator; -import com.intellij.openapi.wm.impl.InternalDecorator; -import com.intellij.openapi.wm.impl.ToolWindowImpl; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; - -public class ToolWindowHelper { - - private static final Logger log = Logger.getInstance(ToolWindowHelper.class); - - public static Rectangle getBounds(ToolWindowImpl toolWindow) { - try { - if (toolWindow.isVisible()) { - if (toolWindow.getType() == ToolWindowType.FLOATING) { - FloatingDecorator floatingDecorator = getFloatingDecorator(toolWindow); - return floatingDecorator.getBounds(); - } else if (toolWindow.getType() == ToolWindowType.WINDOWED) { - Window window = getWindow(toolWindow); - return window.getBounds(); - } - } - } - catch(Exception e) { - log.error("Could not fetch bounds of window " + toolWindow.getId() + "\n" + e.getStackTrace()); - } - - return new Rectangle(0, 0, 0, 0); - } - - public static void setBounds(ToolWindowImpl toolWindow, Rectangle bounds) { - try { - if (toolWindow.isVisible()) { - if (toolWindow.getType() == ToolWindowType.FLOATING) { - FloatingDecorator floatingDecorator = getFloatingDecorator(toolWindow); - floatingDecorator.setBounds(bounds); - } else if (toolWindow.getType() == ToolWindowType.WINDOWED) { - Window window = getWindow(toolWindow); - window.setBounds(bounds); - } - } - } - catch(Exception e) { - log.error("Could not set bounds of window " + toolWindow.getId() + "\n" + e.getStackTrace()); - } - } - - @Nullable - private static FloatingDecorator getFloatingDecorator(ToolWindowImpl toolWindow) { - InternalDecorator decorator = toolWindow.getDecorator(); - return (FloatingDecorator) SwingUtilities.getAncestorOfClass(FloatingDecorator.class, decorator); - } - - @Nullable - private static Window getWindow(ToolWindowImpl toolWindow) { - JComponent component = toolWindow.getComponent(); - return UIUtil.getWindow(component); - } -} diff --git a/src/main/java/com/layoutmanager/ui/dialogs/LayoutNameDialog.java b/src/main/java/com/layoutmanager/ui/dialogs/LayoutNameDialog.java new file mode 100644 index 0000000..3abc064 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/dialogs/LayoutNameDialog.java @@ -0,0 +1,30 @@ +package com.layoutmanager.ui.dialogs; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.ui.Messages; +import com.layoutmanager.localization.MessagesHelper; + +public class LayoutNameDialog { + + private final LayoutNameValidator layoutNameValidator; + + public LayoutNameDialog(LayoutNameValidator layoutNameValidator) { + + this.layoutNameValidator = layoutNameValidator; + } + + public String show(String defaultName) { + String name; + do { + name = Messages.showInputDialog( + MessagesHelper.message("StoreLayout.Dialog.Title"), + MessagesHelper.message("StoreLayout.Dialog.Content"), + AllIcons.Actions.Edit, + defaultName, + null); + } while (name != null && !this.layoutNameValidator.isValid(name)); + + return name; + } +} + diff --git a/src/main/java/com/layoutmanager/ui/dialogs/LayoutNameValidator.java b/src/main/java/com/layoutmanager/ui/dialogs/LayoutNameValidator.java new file mode 100644 index 0000000..971d852 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/dialogs/LayoutNameValidator.java @@ -0,0 +1,11 @@ +package com.layoutmanager.ui.dialogs; + +public class LayoutNameValidator { + public Boolean isValid(String name) { + return name != null && !this.isBlank(name); + } + + private boolean isBlank(String name) { + return name.trim().length() == 0; + } +} diff --git a/src/main/java/com/layoutmanager/ui/NotificationHelper.java b/src/main/java/com/layoutmanager/ui/helpers/BaloonNotificationHelper.java similarity index 94% rename from src/main/java/com/layoutmanager/ui/NotificationHelper.java rename to src/main/java/com/layoutmanager/ui/helpers/BaloonNotificationHelper.java index ffc8e10..b2bf895 100644 --- a/src/main/java/com/layoutmanager/ui/NotificationHelper.java +++ b/src/main/java/com/layoutmanager/ui/helpers/BaloonNotificationHelper.java @@ -1,41 +1,41 @@ -package com.layoutmanager.ui; - -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationDisplayType; -import com.intellij.notification.NotificationGroup; -import com.intellij.notification.Notifications; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; - -import java.util.Arrays; -import java.util.UUID; - -public class NotificationHelper { - private static final NotificationGroup DEFAULT_GROUP = - new NotificationGroup("demo.notifications.balloon", NotificationDisplayType.STICKY_BALLOON, true); - - public static void info(String title, String content) { - Notification notification = DEFAULT_GROUP.createNotification(title, null, content, NotificationType.INFORMATION); - notify(notification); - } - - public static void warning(String title, String content) { - String groupId = UUID.nameUUIDFromBytes(title.getBytes()).toString(); - NotificationGroup groupByNotificationTitle = new NotificationGroup(groupId, NotificationDisplayType.STICKY_BALLOON, true); - Notification notification = groupByNotificationTitle.createNotification(title, null, content, NotificationType.WARNING); - notify(notification); - } - - private static void notify(Notification notification) { - Project currentProject = getCurrentProject(); - Notifications.Bus.notify(notification, currentProject); - } - - private static Project getCurrentProject() { - Project[] openedProjects = ProjectManager.getInstance().getOpenProjects(); - return Arrays.stream(openedProjects) - .findFirst() - .orElse(null); - } -} +package com.layoutmanager.ui.helpers; + +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationDisplayType; +import com.intellij.notification.NotificationGroup; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; + +import java.util.Arrays; +import java.util.UUID; + +public class BaloonNotificationHelper { + private static final NotificationGroup DEFAULT_GROUP = + new NotificationGroup("demo.notifications.balloon", NotificationDisplayType.STICKY_BALLOON, true); + + public static void info(String title, String content) { + Notification notification = DEFAULT_GROUP.createNotification(title, null, content, NotificationType.INFORMATION); + notify(notification); + } + + public static void warning(String title, String content) { + String groupId = UUID.nameUUIDFromBytes(title.getBytes()).toString(); + NotificationGroup groupByNotificationTitle = new NotificationGroup(groupId, NotificationDisplayType.STICKY_BALLOON, true); + Notification notification = groupByNotificationTitle.createNotification(title, null, content, NotificationType.WARNING); + notify(notification); + } + + private static void notify(Notification notification) { + Project currentProject = getCurrentProject(); + Notifications.Bus.notify(notification, currentProject); + } + + private static Project getCurrentProject() { + Project[] openedProjects = ProjectManager.getInstance().getOpenProjects(); + return Arrays.stream(openedProjects) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/com/layoutmanager/ui/helpers/ComponentNotificationHelper.java b/src/main/java/com/layoutmanager/ui/helpers/ComponentNotificationHelper.java new file mode 100644 index 0000000..7a58cf1 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/helpers/ComponentNotificationHelper.java @@ -0,0 +1,29 @@ +package com.layoutmanager.ui.helpers; + +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.ui.awt.RelativePoint; + +import javax.swing.*; + +public class ComponentNotificationHelper { + + public static void info(JComponent component, String message) { + show(component, message, MessageType.INFO); + } + + public static void error(JComponent component, String message) { + show(component, message, MessageType.ERROR); + } + + private static void show(JComponent component, String message, MessageType type) { + JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(message, type, null) + .setFadeoutTime(7500) + .createBalloon() + .show( + RelativePoint.getCenterOf(component), + Balloon.Position.above); + } +} diff --git a/src/main/java/com/layoutmanager/ui/helpers/ScreenSizeHelper.java b/src/main/java/com/layoutmanager/ui/helpers/ScreenSizeHelper.java new file mode 100644 index 0000000..8524fc2 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/helpers/ScreenSizeHelper.java @@ -0,0 +1,38 @@ +package com.layoutmanager.ui.helpers; + +import com.layoutmanager.persistence.ToolWindowInfo; + +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; + +import java.util.Arrays; +import java.util.Comparator; + +import sun.java2d.SunGraphicsEnvironment; + +public class ScreenSizeHelper { + public static Rectangle getContainingScreenBounds(ToolWindowInfo toolWindow) { + return Arrays + .stream(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) + .map(ScreenSizeHelper::getScreenRectangle) + .min(Comparator.comparingInt(x -> -getIntersectionSize(x, toolWindow.getBounds()))) + .orElse(new Rectangle()); + } + + + + private static Rectangle getScreenRectangle(GraphicsDevice device) { + Rectangle defaultBounds = SunGraphicsEnvironment.getUsableBounds(device); + return new Rectangle( + (int)defaultBounds.getX(), + (int)defaultBounds.getY(), + (int)defaultBounds.getWidth(), + (int)defaultBounds.getHeight()); + } + + private static int getIntersectionSize(Rectangle screen, Rectangle window) { + return (int)(Math.max(0, Math.min(screen.getMaxX(), window.getMaxX()) - Math.max(screen.getX(), window.getX())) * + Math.max(0, Math.min(screen.getMaxY(), window.getMaxY()) - Math.max(screen.getY(), window.getY()))); + } +} diff --git a/src/main/java/com/layoutmanager/ui/helpers/ToolWindowHelper.java b/src/main/java/com/layoutmanager/ui/helpers/ToolWindowHelper.java new file mode 100644 index 0000000..9b65b35 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/helpers/ToolWindowHelper.java @@ -0,0 +1,65 @@ +package com.layoutmanager.ui.helpers; + +import com.intellij.openapi.wm.ToolWindowAnchor; +import com.intellij.openapi.wm.ToolWindowType; +import com.intellij.openapi.wm.impl.FloatingDecorator; +import com.intellij.openapi.wm.impl.InternalDecorator; +import com.intellij.openapi.wm.impl.ToolWindowImpl; +import com.intellij.util.ui.UIUtil; + +import java.awt.Rectangle; +import java.awt.Window; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; + +import org.jetbrains.annotations.Nullable; + +public class ToolWindowHelper { + public static Rectangle getBounds(ToolWindowImpl toolWindow) { + if (toolWindow.isVisible()) { + if (toolWindow.getType() == ToolWindowType.FLOATING) { + FloatingDecorator floatingDecorator = getFloatingDecorator(toolWindow); + return floatingDecorator.getBounds(); + } else if (toolWindow.getType() == ToolWindowType.WINDOWED) { + Window window = getWindow(toolWindow); + return window.getBounds(); + } else { + InternalDecorator decorator = toolWindow.getDecorator(); + return decorator.getBounds(); + } + } + + return new Rectangle(0, 0, 0, 0); + } + + public static void setBounds(ToolWindowImpl toolWindow, Rectangle bounds) { + if (toolWindow.isVisible()) { + if (toolWindow.getType() == ToolWindowType.FLOATING) { + FloatingDecorator floatingDecorator = getFloatingDecorator(toolWindow); + floatingDecorator.setBounds(bounds); + } else if (toolWindow.getType() == ToolWindowType.WINDOWED) { + Window window = getWindow(toolWindow); + window.setBounds(bounds); + } else { + Rectangle currentBounds = toolWindow.getDecorator().getBounds(); + if (toolWindow.getAnchor() == ToolWindowAnchor.TOP || toolWindow.getAnchor() == ToolWindowAnchor.BOTTOM) { + toolWindow.stretchHeight(bounds.height - currentBounds.height); + } else { + toolWindow.stretchWidth(bounds.width - currentBounds.width); + } + } + } + } + + @Nullable + private static FloatingDecorator getFloatingDecorator(ToolWindowImpl toolWindow) { + InternalDecorator decorator = toolWindow.getDecorator(); + return (FloatingDecorator) SwingUtilities.getAncestorOfClass(FloatingDecorator.class, decorator); + } + + @Nullable + private static Window getWindow(ToolWindowImpl toolWindow) { + JComponent component = toolWindow.getComponent(); + return UIUtil.getWindow(component); + } +} diff --git a/src/main/java/com/layoutmanager/ui/menu/WindowMenuService.java b/src/main/java/com/layoutmanager/ui/menu/WindowMenuService.java new file mode 100644 index 0000000..45787dd --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/menu/WindowMenuService.java @@ -0,0 +1,96 @@ +package com.layoutmanager.ui.menu; + +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DefaultActionGroup; + +import com.layoutmanager.layout.delete.DeleteLayoutAction; +import com.layoutmanager.layout.restore.RestoreLayoutAction; +import com.layoutmanager.layout.store.LayoutCreator; +import com.layoutmanager.layout.store.create.NewLayoutAction; +import com.layoutmanager.layout.store.overwrite.OverwriteLayoutAction; +import com.layoutmanager.layout.store.smartdock.SmartDockerFactory; +import com.layoutmanager.localization.MessagesHelper; +import com.layoutmanager.persistence.Layout; +import com.layoutmanager.persistence.LayoutConfig; +import com.layoutmanager.ui.dialogs.LayoutNameDialog; +import com.layoutmanager.ui.dialogs.LayoutNameValidator; + +public class WindowMenuService { + private DefaultActionGroup storeLayout; + private DefaultActionGroup restoreLayout; + private DefaultActionGroup deleteLayout; + + public void create() { + if (this.hasBeenCreated()) { + return; + } + + this.createMainActions((DefaultActionGroup) ActionManager.getInstance().getAction("WindowMenu")); + this.createStoreRestoreActions(); + } + + public void recreate() { + if (!this.hasBeenCreated()) { + return; + } + + this.restoreLayout.removeAll(); + this.storeLayout.removeAll(); + this.deleteLayout.removeAll(); + + this.createStoreRestoreActions(); + } + + private boolean hasBeenCreated() { + return this.storeLayout != null; + } + + private void createMainActions(DefaultActionGroup windowMenu) { + this.storeLayout = this.createMainAction(MessagesHelper.message("StoreLayout.Menu"), windowMenu); + this.restoreLayout = this.createMainAction(MessagesHelper.message("RestoreLayout.Menu"), windowMenu); + this.deleteLayout = this.createMainAction(MessagesHelper.message("DeleteLayout.Menu"), windowMenu); + } + + private DefaultActionGroup createMainAction(String name, DefaultActionGroup windowMenu) { + DefaultActionGroup windowMenuItem = new DefaultActionGroup(name, true); + windowMenu.add(windowMenuItem); + + return windowMenuItem; + } + + private void createStoreRestoreActions() { + LayoutConfig config = LayoutConfig.getInstance(); + this.addStoreLayoutActions(config); + this.addRestoreLayoutActions(config); + this.addDeleteLayoutActions(config); + } + + private void addStoreLayoutActions(LayoutConfig config) { + LayoutCreator layoutCreator = new LayoutCreator( + config.getSettings(), + new SmartDockerFactory(), + new LayoutNameDialog(new LayoutNameValidator())); + + for (int index = 0; index < config.getLayoutCount(); index++) { + this.storeLayout.add(new OverwriteLayoutAction(layoutCreator, index)); + } + + if (config.getLayoutCount() > 0) { + this.storeLayout.addSeparator(); + } + + this.storeLayout.add(new NewLayoutAction(layoutCreator)); + } + + private void addRestoreLayoutActions(LayoutConfig config) { + for (Layout layout : config.getLayouts()) { + this.restoreLayout.add(new RestoreLayoutAction(layout)); + } + } + + private void addDeleteLayoutActions(LayoutConfig config) { + for (Layout layout : config.getLayouts()) { + this.deleteLayout.add(new DeleteLayoutAction(layout)); + } + } +} diff --git a/src/main/java/com/layoutmanager/ui/settings/ImportExportConstants.java b/src/main/java/com/layoutmanager/ui/settings/ImportExportConstants.java new file mode 100644 index 0000000..9800e6e --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/ImportExportConstants.java @@ -0,0 +1,9 @@ +package com.layoutmanager.ui.settings; + +public class ImportExportConstants { + public static final String FILE_ENDING = "wl"; + public static final String FILE_ENDING_WITH_DOT = "." + FILE_ENDING; + + + public static final String FILE_TYPE_NAME = "Window layouts"; +} diff --git a/src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.form b/src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.form new file mode 100644 index 0000000..e01fd92 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.form @@ -0,0 +1,113 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.java b/src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.java new file mode 100644 index 0000000..d890705 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/LayoutManagerSettingsPanel.java @@ -0,0 +1,209 @@ +package com.layoutmanager.ui.settings; + +import com.intellij.openapi.components.ServiceManager; +import com.layoutmanager.localization.MessagesHelper; +import com.layoutmanager.persistence.Layout; +import com.layoutmanager.persistence.LayoutConfig; +import com.layoutmanager.ui.dialogs.LayoutNameDialog; +import com.layoutmanager.ui.dialogs.LayoutNameValidator; +import com.layoutmanager.ui.helpers.ComponentNotificationHelper; +import com.layoutmanager.ui.menu.WindowMenuService; +import com.layoutmanager.ui.settings.exporting.ExportDialog; +import com.layoutmanager.ui.settings.importing.ImportDialog; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.TableModelEvent; +import javax.swing.table.DefaultTableModel; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import static javax.swing.JComponent.WHEN_FOCUSED; + +public class LayoutManagerSettingsPanel { + private final LayoutConfig layoutConfig; + private final LayoutNameDialog layoutNameDialog; + private final LayoutNameValidator layoutNameValidator; + private final LayoutSerializer layoutSerializer; + + private ArrayList currentLayouts = new ArrayList<>(); + private JCheckBox useSmartDockingCheckbox; + private JButton deleteButton; + private JButton renameButton; + private JButton exportButton; + private JButton importButton; + private JPanel settingsPanel; + private JTable layoutsTable; + + public LayoutManagerSettingsPanel( + LayoutConfig layoutConfig, + LayoutNameDialog layoutNameDialog, + LayoutNameValidator layoutNameValidator, + LayoutSerializer layoutSerializer) { + this.layoutConfig = layoutConfig; + this.layoutNameDialog = layoutNameDialog; + this.layoutNameValidator = layoutNameValidator; + this.layoutSerializer = layoutSerializer; + + this.loadSettings(layoutConfig); + + this.layoutsTable.getSelectionModel().addListSelectionListener(listSelectionEvent -> this.selectedLayoutChanged()); + this.deleteButton.addActionListener(e -> this.deleteLayout()); + this.renameButton.addActionListener(e -> this.renameLayout()); + this.exportButton.addActionListener(actionEvent -> this.exportLayout()); + this.importButton.addActionListener(actionEvent -> this.importLayout()); + } + + private void loadSettings(LayoutConfig layoutConfig) { + Collections.addAll(this.currentLayouts, layoutConfig.getLayouts()); + + DefaultTableModel table = this.createTableContent(); + this.setKeyBindings(table); + this.layoutsTable.setModel(table); + this.layoutsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + this.useSmartDockingCheckbox.setSelected(layoutConfig.getSettings().getUseSmartDock()); + } + + private void setKeyBindings(DefaultTableModel tableModel){ + InputMap inputMap = layoutsTable.getInputMap(WHEN_FOCUSED); + ActionMap actionMap = layoutsTable.getActionMap(); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"); + actionMap.put("delete", new AbstractAction() { + public void actionPerformed(ActionEvent evt) { + int selectedRow = layoutsTable.getSelectedRow(); + if (selectedRow >= 0) { + tableModel.removeRow(selectedRow); + } + } + }); + } + + private void selectedLayoutChanged() { + this.deleteButton.setEnabled(this.layoutsTable.getSelectedRow() >= 0); + this.renameButton.setEnabled(this.layoutsTable.getSelectedRow() >= 0); + this.exportButton.setEnabled(this.layoutsTable.getSelectedRow() >= 0); + } + + private void deleteLayout() { + DefaultTableModel table = (DefaultTableModel)this.layoutsTable.getModel(); + table.removeRow(this.layoutsTable.getSelectedRow()); + } + + private void renameLayout() { + int selectedRow = this.layoutsTable.getSelectedRow(); + String newName = this.layoutNameDialog.show(this.currentLayouts.get(selectedRow).getName()); + + if (newName != null) { + this.layoutsTable.setValueAt(newName, selectedRow, 0); + } + } + + private void exportLayout() { + int selectedRow = this.layoutsTable.getSelectedRow(); + Layout selectedLayout = this.currentLayouts.get(selectedRow); + + String encodedContent = this.layoutSerializer.serialize(selectedLayout); + showExportDialog(selectedLayout, encodedContent); + } + + private void showExportDialog(Layout selectedLayout, String encodedContent) { + ExportDialog exportDialog = new ExportDialog(selectedLayout.getName(), encodedContent); + JDialog parent = this.getParentDialog(); + exportDialog.showDialogInCenterOf(parent); + } + + private void importLayout() { + ImportDialog importDialog = new ImportDialog(this.layoutNameValidator, this.layoutSerializer); + JDialog parent = this.getParentDialog(); + if (importDialog.showDialogInCenterOf(parent) == ImportDialog.OK_RESULT) { + this.currentLayouts.add(importDialog.getImportedLayout()); + ((DefaultTableModel) layoutsTable.getModel()).fireTableDataChanged(); + } + } + + @Nullable + private JDialog getParentDialog() { + return (JDialog) SwingUtilities.getAncestorOfClass(JDialog.class, this.settingsPanel); + } + + public boolean hasChanged() { + return this.useSmartDockingCheckbox.isSelected() != this.layoutConfig.getSettings().getUseSmartDock() || + !Arrays.equals( + this.layoutConfig.getLayouts(), + this.currentLayouts + .stream() + .toArray(Layout[]::new)); + } + + public void apply() { + this.layoutConfig.getSettings().setUseSmartDock(this.useSmartDockingCheckbox.isSelected()); + this.layoutConfig.setLayouts(this.currentLayouts + .stream() + .toArray(Layout[]::new)); + + WindowMenuService windowMenuService = ServiceManager.getService(WindowMenuService.class); + windowMenuService.recreate(); + } + + public JPanel getPanel() { + return this.settingsPanel; + } + + @NotNull + private DefaultTableModel createTableContent() { + return new DefaultTableModel( + new String[]{ + MessagesHelper.message("SettingsPage.NameColumn"), + MessagesHelper.message("SettingsPage.ConfiguredWindowsColumn") + }, + currentLayouts.size()) { + + @Override + public boolean isCellEditable(int row, int column) { + return column == 0; + } + + @Override + public void setValueAt(Object aValue, int row, int column) { + String newLayoutName = aValue.toString(); + if (layoutNameValidator.isValid(newLayoutName)) { + currentLayouts.get(row).setName(aValue.toString()); + this.fireTableChanged(new TableModelEvent(this, row)); + } else { + ComponentNotificationHelper.error(layoutsTable, MessagesHelper.message("LayoutNameValidation.InvalidName")); + } + } + + @Override + public int getRowCount() { + return currentLayouts.size(); + } + + @Override + public void removeRow(int row) { + currentLayouts.remove(row); + this.fireTableRowsDeleted(row, row); + } + + @Override + public Object getValueAt(int row, int column) { + Layout layout = currentLayouts.get(row); + + switch (column) { + case 0: + return layout.getName(); + case 1: + return layout.getToolWindows().length; + default: + return null; + } + } + }; + } +} diff --git a/src/main/java/com/layoutmanager/ui/settings/LayoutSerializer.java b/src/main/java/com/layoutmanager/ui/settings/LayoutSerializer.java new file mode 100644 index 0000000..1329ac7 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/LayoutSerializer.java @@ -0,0 +1,20 @@ +package com.layoutmanager.ui.settings; + +import blazing.chain.LZSEncoding; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.layoutmanager.persistence.Layout; + +public class LayoutSerializer { + public String serialize(Layout layout) { + Gson gson = new GsonBuilder().create(); + String jsonContent = gson.toJson(layout); + return LZSEncoding.compressToBase64(jsonContent); + } + + public Layout deserialize(String encodedContent) { + String jsonContent = LZSEncoding.decompressFromBase64(encodedContent); + Gson gson = new GsonBuilder().create(); + return gson.fromJson(jsonContent, Layout.class); + } +} diff --git a/src/main/java/com/layoutmanager/ui/settings/SettingsPage.java b/src/main/java/com/layoutmanager/ui/settings/SettingsPage.java new file mode 100644 index 0000000..8d6d23b --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/SettingsPage.java @@ -0,0 +1,47 @@ +package com.layoutmanager.ui.settings; + +import com.intellij.openapi.options.Configurable; + +import javax.swing.JComponent; + +import com.layoutmanager.localization.MessagesHelper; +import com.layoutmanager.persistence.LayoutConfig; +import com.layoutmanager.ui.dialogs.LayoutNameDialog; +import com.layoutmanager.ui.dialogs.LayoutNameValidator; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.Nullable; + +public class SettingsPage implements Configurable { + private final LayoutManagerSettingsPanel panel; + + public SettingsPage() { + LayoutNameValidator layoutNameValidator = new LayoutNameValidator(); + this.panel = new LayoutManagerSettingsPanel( + LayoutConfig.getInstance(), + new LayoutNameDialog(layoutNameValidator), + layoutNameValidator, + new LayoutSerializer()); + } + + @Nls(capitalization = Nls.Capitalization.Title) + @Override + public String getDisplayName() { + return MessagesHelper.message("SettingsPage.Title"); + } + + @Nullable + @Override + public JComponent createComponent() { + return this.panel.getPanel(); + } + + @Override + public boolean isModified() { + return this.panel.hasChanged(); + } + + @Override + public void apply() { + this.panel.apply(); + } +} diff --git a/src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.form b/src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.form new file mode 100644 index 0000000..34903ca --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.form @@ -0,0 +1,119 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.java b/src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.java new file mode 100644 index 0000000..abd5287 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/exporting/ExportDialog.java @@ -0,0 +1,117 @@ +package com.layoutmanager.ui.settings.exporting; + +import com.layoutmanager.localization.MessagesHelper; +import com.layoutmanager.ui.helpers.ComponentNotificationHelper; +import com.layoutmanager.ui.settings.ImportExportConstants; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.awt.datatransfer.StringSelection; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ExportDialog extends JDialog { + private JTextArea exportTextBox; + private JPanel contentPanel; + private JButton exportToFileButton; + private JButton exportToClipboardButton; + private JButton closeButton; + private JLabel layoutNameLabel; + + private String layoutName; + private String content; + + public ExportDialog(String layoutName, String content) { + this.layoutName = layoutName; + this.content = content; + + this.setContentPane(contentPanel); + this.setModal(true); + this.getRootPane().setDefaultButton(closeButton); + + this.exportToClipboardButton.addActionListener(actionEvent -> this.exportToClipboard()); + this.exportToFileButton.addActionListener(actionEvent -> this.exportToFile()); + this.closeButton.addActionListener(actionEvent -> this.onClose()); + + this.layoutNameLabel.setText(layoutName); + this.exportTextBox.setText(content); + + this.contentPanel.registerKeyboardAction(e -> onClose(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + } + + public void showDialogInCenterOf(JDialog parent) { + this.setTitle(MessagesHelper.message("ExportDialog.Title")); + this.setSize(this.getPreferredSize()); + this.setLocationRelativeTo(parent); + this.setVisible(true); + } + + public JPanel getPanel() { + return this.contentPanel; + } + + private void exportToClipboard() { + this.exportTextBox.requestFocus(); + this.exportTextBox.selectAll(); + Toolkit + .getDefaultToolkit() + .getSystemClipboard() + .setContents( + new StringSelection(this.exportTextBox.getText()), + null); + ComponentNotificationHelper.info(this.exportToClipboardButton, MessagesHelper.message("ExportDialog.CopiedToClipboard")); + } + + private void exportToFile() { + File selectedFile = this.selectFile(); + if (selectedFile != null) { + Path path = this.getPath(selectedFile); + this.writeContentToFile(path); + } + } + + private File selectFile() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(MessagesHelper.message("ExportDialog.SaveFileTitle")); + fileChooser.setSelectedFile(new File(this.layoutName + ImportExportConstants.FILE_ENDING_WITH_DOT)); + fileChooser.setAcceptAllFileFilterUsed(false); + FileNameExtensionFilter filter = new FileNameExtensionFilter(ImportExportConstants.FILE_TYPE_NAME, ImportExportConstants.FILE_ENDING); + fileChooser.setFileFilter(filter); + + int result = fileChooser.showSaveDialog(null); + return result == JFileChooser.APPROVE_OPTION ? + fileChooser.getSelectedFile() : + null; + } + + @NotNull + private Path getPath(File selectedFile) { + String fullPath = selectedFile.getAbsolutePath(); + if (!fullPath.toLowerCase().endsWith(ImportExportConstants.FILE_ENDING_WITH_DOT)) { + fullPath += ImportExportConstants.FILE_ENDING_WITH_DOT; + } + + return Paths.get(fullPath); + } + + private void writeContentToFile(Path path) { + try { + byte[] encodedContent = this.content.getBytes(); + Files.write(path, encodedContent); + ComponentNotificationHelper.info(this.exportToFileButton, MessagesHelper.message("ExportDialog.SavedTo", path.getFileName().toString())); + } catch (IOException e) { + ComponentNotificationHelper.error(this.exportToFileButton, MessagesHelper.message("ExportDialog.FailedToWriteFile", e.getMessage())); + } + } + + private void onClose() { + JDialog window = (JDialog) SwingUtilities.getAncestorOfClass(JDialog.class, this.contentPanel); + window.dispose(); + } +} diff --git a/src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.form b/src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.form new file mode 100644 index 0000000..1028af4 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.form @@ -0,0 +1,146 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.java b/src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.java new file mode 100644 index 0000000..2595a19 --- /dev/null +++ b/src/main/java/com/layoutmanager/ui/settings/importing/ImportDialog.java @@ -0,0 +1,166 @@ +package com.layoutmanager.ui.settings.importing; + +import com.layoutmanager.localization.MessagesHelper; +import com.layoutmanager.persistence.Layout; +import com.layoutmanager.ui.dialogs.LayoutNameValidator; +import com.layoutmanager.ui.helpers.ComponentNotificationHelper; +import com.layoutmanager.ui.settings.ImportExportConstants; +import com.layoutmanager.ui.settings.LayoutSerializer; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class ImportDialog extends JDialog { + public static final int OK_RESULT = 1; + public static final int ABORT_RESULT = 0; + private final LayoutNameValidator layoutNameValidator; + private final LayoutSerializer layoutSerializer; + + private JPanel contentPanel; + private JButton importButton; + private JButton abortButton; + private JButton importFromFileButton; + private JButton importFromClipboardButton; + private JLabel layoutConfiguredWindowCountLabel; + private JTextField layoutNameTextBox; + + private Layout importedLayout; + private int result; + + public ImportDialog( + LayoutNameValidator layoutNameValidator, + LayoutSerializer layoutSerializer) { + this.layoutNameValidator = layoutNameValidator; + this.layoutSerializer = layoutSerializer; + + this.setContentPane(contentPanel); + this.setModal(true); + this.setResizable(false); + this.getRootPane().setDefaultButton(importButton); + + this.importButton.addActionListener(e -> this.onOK()); + this.abortButton.addActionListener(e -> this.onCancel()); + this.importFromClipboardButton.addActionListener(actionEvent -> this.importFromClipboard()); + this.importFromFileButton.addActionListener(actionEvent -> this.importFromFile()); + + // call onCancel() when cross is clicked + this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + this.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + onCancel(); + } + }); + + this.contentPanel.registerKeyboardAction(e -> onCancel(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + } + + public Layout getImportedLayout() { + return this.importedLayout; + } + + public int showDialogInCenterOf(JDialog parent) { + this.setTitle(MessagesHelper.message("ImportDialog.Title")); + this.setSize(this.getPreferredSize()); + this.setLocationRelativeTo(parent); + this.setVisible(true); + return this.result; + } + + private void importFromFile() { + File selectedFile = this.selectFile(); + if (selectedFile != null) { + this.importFile(selectedFile); + } + } + + private File selectFile() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(MessagesHelper.message("ImportDialog.SelectFileTitle")); + fileChooser.setAcceptAllFileFilterUsed(false); + FileNameExtensionFilter filter = new FileNameExtensionFilter(ImportExportConstants.FILE_TYPE_NAME, ImportExportConstants.FILE_ENDING); + fileChooser.setFileFilter(filter); + + int result = fileChooser.showOpenDialog(null); + return result == JFileChooser.APPROVE_OPTION && fileChooser.getSelectedFile().exists() ? + fileChooser.getSelectedFile() : null; + } + + private void importFile(File file) { + try { + String encodedContent = new String(Files.readAllBytes(Paths.get(file.getPath())), StandardCharsets.UTF_8); + this.importLayout(encodedContent); + } catch (IOException e) { + ComponentNotificationHelper.error(this.importFromFileButton, MessagesHelper.message("ImportDialog.IOException", file.getName(), e.getMessage())); + this.deselectLayout(); + } catch(Exception e) { + ComponentNotificationHelper.error(this.importFromFileButton, MessagesHelper.message("ImportDialog.UnknownFormat")); + this.deselectLayout(); + } + } + + private void importFromClipboard() { + try { + String lzEncodedContent = (String) Toolkit + .getDefaultToolkit() + .getSystemClipboard() + .getData(DataFlavor.stringFlavor); + + this.importLayout(lzEncodedContent); + } catch (Exception e) { + ComponentNotificationHelper.error(this.importFromClipboardButton, MessagesHelper.message("ImportDialog.UnknownFormat")); + this.deselectLayout(); + } + } + + private void importLayout(String encodedContent) { + Layout selectedLayout = this.layoutSerializer.deserialize(encodedContent); + this.selectLayout(selectedLayout); + } + + private void selectLayout(Layout layout) { + this.importedLayout = layout; + + this.layoutNameTextBox.setText(layout.getName()); + this.layoutNameTextBox.requestFocus(); + this.layoutConfiguredWindowCountLabel.setText(Integer.toString(layout.getToolWindows().length)); + + ComponentNotificationHelper.info(this.layoutNameTextBox, MessagesHelper.message("ImportDialog.SuccessfullyLoadedLayout", layout.getName())); + + this.importButton.setEnabled(true); + this.layoutNameTextBox.setEnabled(true); + } + + private void deselectLayout() { + this.importedLayout = null; + this.importButton.setEnabled(false); + this.layoutNameTextBox.setText(""); + this.layoutNameTextBox.setEnabled(false); + this.layoutConfiguredWindowCountLabel.setText("-"); + } + + private void onOK() { + if (!this.layoutNameValidator.isValid(this.layoutNameTextBox.getText())) { + ComponentNotificationHelper.error(this.importButton, MessagesHelper.message("LayoutNameValidation.InvalidName")); + return; + } + this.importedLayout.setName(this.layoutNameTextBox.getText()); + + this.result = OK_RESULT; + dispose(); + } + + private void onCancel() { + this.result = ABORT_RESULT; + dispose(); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b4423a0..7c0142d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -27,7 +27,13 @@
  • Dec 19, 2019 (ver 1.3.0) - Store placement of editor tabs and tool window labels.
  • Dec 26, 2019 (ver 1.3.1) - Fix support of Java 8.
  • Jan 07, 2020 (ver 1.3.2) - Minor bugfixes.
  • -
  • Jan 07, 2020 (ver 1.3.3) - Issues when storing and restoring invisible windows fixed.
  • +
  • Feb 25, 2020 (ver 1.3.3) - Issues when storing and restoring invisible windows fixed.
  • +
  • + May 8, 2020 (ver 1.4.0) + - Introduced settings page. + - Introduced smart docking when saving a layout (Configurable). + - Store docked tool window sizes and restore them accordingly. +
  • ]]> @@ -39,9 +45,11 @@ - + + + \ No newline at end of file diff --git a/src/main/resources/com/layoutmanager/ui/messages.properties b/src/main/resources/com/layoutmanager/ui/messages.properties index d9e5cf8..1ec14e9 100644 --- a/src/main/resources/com/layoutmanager/ui/messages.properties +++ b/src/main/resources/com/layoutmanager/ui/messages.properties @@ -17,3 +17,20 @@ DeleteLayout.Menu=Delete Layout DeleteLayout.Notification.Title=Window layout deleted DeleteLayout.Notification.Content=The window layout ''{0}'' has been successfully deleted. +SettingsPage.Title=Window Layout Manager +SettingsPage.NameColumn=Name +SettingsPage.ConfiguredWindowsColumn=Configured Windows + +ExportDialog.Title=Export layout +ExportDialog.CopiedToClipboard=Copied to clipboard! +ExportDialog.SaveFileTitle=Specify a file to save +ExportDialog.SavedTo=Saved to file ''{0}'' ! +ExportDialog.FailedToWriteFile=Failed to write export file: {0} + +ImportDialog.Title=Import layout +ImportDialog.SelectFileTitle=Select layout file +ImportDialog.IOException=Failed to read file '{0}'. Reason: {1} +ImportDialog.UnknownFormat=Import failed due to a unknown format +ImportDialog.SuccessfullyLoadedLayout=Successfully loaded the layout ''{0}'' + +LayoutNameValidation.InvalidName=The layout name must not be empty or contain illegal characters. \ No newline at end of file